diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f2071d4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI + +on: + pull_request: + branches: + - '*' + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Use Node.js 22.12.0 + uses: actions/setup-node@v4 + with: + node-version: 22.12.0 + cache: 'npm' + cache-dependency-path: package.json + - name: Install dependencies + run: npm install --no-audit --no-fund + - name: Run unit build + run: npm run build + + e2e: + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Use Node.js 22.12.0 + uses: actions/setup-node@v4 + with: + node-version: 22.12.0 + cache: 'npm' + cache-dependency-path: package.json + - name: Install dependencies + run: npm install --no-audit --no-fund + - name: Build application + run: npm run build + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + - name: Run Playwright tests + env: + PLAYWRIGHT_TEST_PORT: 4400 + run: npx playwright test diff --git a/.gitignore b/.gitignore index 88a190b..2de942a 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,8 @@ Thumbs.db # Otros *.tgz +playwright-report/ +test-results/ # Archivo de bloqueo de paquetes package-lock.json diff --git a/e2e/pdf-annotator.e2e-spec.ts b/e2e/pdf-annotator.e2e-spec.ts new file mode 100644 index 0000000..82cd813 --- /dev/null +++ b/e2e/pdf-annotator.e2e-spec.ts @@ -0,0 +1,97 @@ +import { expect, test, type Download } from '@playwright/test'; +import { promises as fs } from 'node:fs'; +import { join } from 'node:path'; +import { Readable } from 'node:stream'; + +async function streamToBuffer(stream: Readable): Promise { + const chunks: Buffer[] = []; + for await (const chunk of stream) { + const bufferChunk = + typeof chunk === 'string' ? Buffer.from(chunk, 'utf-8') : Buffer.from(chunk); + chunks.push(bufferChunk); + } + return Buffer.concat(chunks); +} + +async function downloadToBuffer(download: Download): Promise { + const stream = await download.createReadStream(); + if (stream) { + return streamToBuffer(stream); + } + const filePath = await download.path(); + if (!filePath) { + throw new Error('Unable to read download contents'); + } + return fs.readFile(filePath); +} + +test.describe('PDF annotation end-to-end', () => { + test('creates, duplicates and exports annotations', async ({ page }) => { + await page.goto('/'); + + const fileInput = page.locator('input.input-file[type="file"]'); + await expect(fileInput).toBeVisible(); + + const samplePdfPath = join( + process.cwd(), + 'public', + 'assets', + 'test-documents', + 'sample.pdf' + ); + + await fileInput.setInputFiles(samplePdfPath); + + const viewer = page.locator('main.viewer'); + await expect(viewer).toBeVisible(); + await expect(page.locator('.hitbox')).toBeVisible(); + + const hitbox = page.locator('.hitbox'); + await hitbox.click({ position: { x: 200, y: 250 } }); + + const previewEditor = page.locator('.annotation-editor:not(.editing)'); + await expect(previewEditor).toBeVisible(); + + const previewFields = previewEditor.locator('label.field'); + await previewFields.nth(0).locator('input').fill('Test map field'); + await previewFields.nth(1).locator('select').selectOption('text'); + await previewFields.nth(2).locator('input').fill('Test annotation'); + await previewEditor.locator('button', { hasText: '✅' }).click(); + + const annotations = page.locator('.annotations-layer .annotation'); + await expect(annotations).toHaveCount(1); + await expect(annotations.first()).toContainText('Test annotation'); + + await annotations.first().click(); + + const editingPanel = page.locator('.annotation-editor.editing'); + await expect(editingPanel).toBeVisible(); + + await editingPanel.locator('button', { hasText: '⧉' }).click(); + + const duplicateEditor = page.locator('.annotation-editor.editing'); + await expect(duplicateEditor).toBeVisible(); + await duplicateEditor.locator('button', { hasText: '✅' }).click(); + await expect(duplicateEditor).toHaveCount(0); + + await expect(annotations).toHaveCount(2); + await expect(annotations).toContainText(['Test annotation', 'Test annotation']); + + const downloadJsonPromise = page.waitForEvent('download'); + await page.getByRole('button', { name: /Descargar JSON/i }).click(); + const jsonDownload = await downloadJsonPromise; + const jsonBuffer = await downloadToBuffer(jsonDownload); + const jsonData = JSON.parse(jsonBuffer.toString('utf-8')); + const pages = Array.isArray(jsonData.pages) ? jsonData.pages : []; + expect(pages.length).toBeGreaterThan(0); + const firstPage = pages[0] ?? { fields: [] }; + const fields = Array.isArray(firstPage.fields) ? firstPage.fields : []; + expect(fields.length).toBeGreaterThanOrEqual(2); + + const downloadPdfPromise = page.waitForEvent('download'); + await page.getByRole('button', { name: /Descargar PDF/i }).click(); + const pdfDownload = await downloadPdfPromise; + const pdfBuffer = await downloadToBuffer(pdfDownload); + expect(pdfBuffer.subarray(0, 4).toString()).toBe('%PDF'); + }); +}); diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 0000000..e1a69ed --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "target": "ES2022", + "types": ["node", "@playwright/test"], + "resolveJsonModule": true, + "esModuleInterop": true + }, + "include": ["./**/*.ts"] +} diff --git a/package.json b/package.json index a6d4f4b..f20d751 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build": "npm run i18n:check && ng build", "watch": "ng build --watch --configuration development", "test": "ng test", + "e2e": "playwright test", "serve:ssr:pdf-annotator": "node dist/pdf-annotator/server/server.mjs", "i18n:check": "node scripts/check-translations.mjs" }, @@ -26,6 +27,9 @@ ] }, "private": true, + "engines": { + "node": "22.12.0" + }, "dependencies": { "@angular/common": "^20.2.0", "@angular/compiler": "^20.2.0", @@ -48,6 +52,7 @@ "@angular/build": "^20.2.1", "@angular/cli": "^20.2.1", "@angular/compiler-cli": "^20.2.0", + "@playwright/test": "^1.48.2", "@types/express": "^5.0.1", "@types/jasmine": "~5.1.0", "@types/node": "^20.17.19", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..5364157 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,33 @@ +import { defineConfig, devices } from '@playwright/test'; + +const PORT = process.env.PLAYWRIGHT_TEST_PORT ? Number(process.env.PLAYWRIGHT_TEST_PORT) : 4300; + +export default defineConfig({ + testDir: './e2e', + timeout: 120_000, + expect: { + timeout: 10_000, + }, + retries: process.env.CI ? 2 : 0, + fullyParallel: true, + reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list', + use: { + baseURL: `http://127.0.0.1:${PORT}`, + headless: true, + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: `npm run start -- --host 0.0.0.0 --port ${PORT}`, + url: `http://127.0.0.1:${PORT}`, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + timeout: 180_000, + }, +}); diff --git a/public/assets/test-documents/sample.pdf b/public/assets/test-documents/sample.pdf new file mode 100644 index 0000000..a43070d Binary files /dev/null and b/public/assets/test-documents/sample.pdf differ