From e973c237a84339d59d35f461632b6c4c29e92ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alvaro=20Amor=C3=B3s?= <39102625+ElMaxter99@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:35:18 +0100 Subject: [PATCH 1/3] Add Playwright E2E setup and CI workflow --- .github/workflows/ci.yml | 47 +++++++++++++ .gitignore | 2 + e2e/pdf-annotator.e2e-spec.ts | 88 ++++++++++++++++++++++++ e2e/tsconfig.json | 11 +++ package.json | 2 + playwright.config.ts | 33 +++++++++ public/assets/test-documents/sample.pdf | Bin 0 -> 585 bytes 7 files changed, 183 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 e2e/pdf-annotator.e2e-spec.ts create mode 100644 e2e/tsconfig.json create mode 100644 playwright.config.ts create mode 100644 public/assets/test-documents/sample.pdf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5693410 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +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 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + - 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 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + - 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..93aec29 --- /dev/null +++ b/e2e/pdf-annotator.e2e-spec.ts @@ -0,0 +1,88 @@ +import { expect, test, type Download } from '@playwright/test'; +import { promises as fs } from 'node:fs'; +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(); + + await fileInput.setInputFiles('public/assets/test-documents/sample.pdf'); + + 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 358aed2..333b22f 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,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" }, @@ -46,6 +47,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 0000000000000000000000000000000000000000..a43070dd4c693aefc4fd448caa79d830bd56a37c GIT binary patch literal 585 zcmZWnO;5r=6uj@RmwB3H7CWHeDF;Sy|-iU{VKCq;8NxK;I*ZW>S1ZC4EyYuF~ znapnQ?O}4IuOp@5!!9;zFo2hQ?FinzaVEEGcyDG+6#^zqF&ru?QX$IuZ$rcLp0c#U zJQVsnPqF*LL7y~A<-Ad3N@8lG!0UMsj~ZW$%e$~#BxWy(TGKn zb>w5&!tt~UQHyrA<=m-1Q?Yg*h+}bAxsuFQjS`-!5kyE Date: Thu, 30 Oct 2025 13:45:20 +0100 Subject: [PATCH 2/3] Align CI with Node 22.12.0 and harden e2e fixture path --- .github/workflows/ci.yml | 8 ++++---- e2e/pdf-annotator.e2e-spec.ts | 11 ++++++++++- package.json | 3 +++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5693410..a52c38c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,10 +14,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Use Node.js 20 + - name: Use Node.js 22.12.0 uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22.12.0 cache: 'npm' - name: Install dependencies run: npm install --no-audit --no-fund @@ -30,10 +30,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Use Node.js 20 + - name: Use Node.js 22.12.0 uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22.12.0 cache: 'npm' - name: Install dependencies run: npm install --no-audit --no-fund diff --git a/e2e/pdf-annotator.e2e-spec.ts b/e2e/pdf-annotator.e2e-spec.ts index 93aec29..82cd813 100644 --- a/e2e/pdf-annotator.e2e-spec.ts +++ b/e2e/pdf-annotator.e2e-spec.ts @@ -1,5 +1,6 @@ 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 { @@ -31,7 +32,15 @@ test.describe('PDF annotation end-to-end', () => { const fileInput = page.locator('input.input-file[type="file"]'); await expect(fileInput).toBeVisible(); - await fileInput.setInputFiles('public/assets/test-documents/sample.pdf'); + 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(); diff --git a/package.json b/package.json index 333b22f..0cd3baa 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,9 @@ ] }, "private": true, + "engines": { + "node": "22.12.0" + }, "dependencies": { "@angular/common": "^20.2.0", "@angular/compiler": "^20.2.0", From 937147c7dd77c746a48c3d991c319535c1d487bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alvaro=20Amor=C3=B3s?= <39102625+ElMaxter99@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:51:41 +0100 Subject: [PATCH 3/3] Fix CI cache to use package manifest --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a52c38c..f2071d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ jobs: 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 @@ -35,6 +36,7 @@ jobs: 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