diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 8731eb8a84..8a6ccfeb6b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -41,6 +41,11 @@ jobs: - name: Install all Playwright browsers run: task e2e:install + - name: Build frontend (production bundle for vite preview) + env: + VITE_BUILD_FOR_PREVIEW: "1" + run: task frontend:build + - name: Run E2E tests (all browsers) run: task e2e:cross-browser diff --git a/frontend/editor/src/core/tests/helpers/ui-helpers.ts b/frontend/editor/src/core/tests/helpers/ui-helpers.ts index 7c08f93f60..cea44eabb5 100644 --- a/frontend/editor/src/core/tests/helpers/ui-helpers.ts +++ b/frontend/editor/src/core/tests/helpers/ui-helpers.ts @@ -37,10 +37,9 @@ export async function waitForModalClose( } /** - * Upload one or more files through the FileSidebar's "Open from computer" - * action. The button is always rendered (collapsed or expanded sidebar) and - * triggers the hidden `data-testid="file-input"` native picker directly - - * there is no modal to wait for under the post-refactor design. + * Upload one or more files through the FileSidebar's hidden native input. + * The sidebar button is still the user-facing entry point, but tests drive + * the input directly to avoid Firefox file-picker side effects. * * `setInputFiles` doesn't await the input's async onChange (which writes to * IndexedDB via `addFiles`), so without a sync point a caller that follows @@ -53,7 +52,6 @@ export async function uploadFiles( filePaths: string | string[], ): Promise { const paths = Array.isArray(filePaths) ? filePaths : [filePaths]; - await page.getByTestId("files-button").click(); await page.locator('[data-testid="file-input"]').setInputFiles(paths); // Sync point: wait until at least one file lands in the sidebar's file // list. The list only renders once `addFiles` has resolved (which awaits @@ -66,16 +64,19 @@ export async function uploadFiles( /** * Some tools (Merge in particular) park the workbench in `viewer` mode after * upload, which keeps the run button disabled. The UI exposes a "Go to file - * editor" affordance to switch out of viewer mode; this helper clicks it - * when present and is a no-op otherwise. + * editor" affordance to switch out of viewer mode; this helper clicks the + * file-editor segment when present and is a no-op otherwise. */ export async function switchToEditorIfViewerMode(page: Page): Promise { - const goToEditor = page.getByRole("button", { - name: /go to file editor/i, - }); - if (await goToEditor.isVisible({ timeout: 1_000 }).catch(() => false)) { - await goToEditor.click(); + const viewSwitcher = page.locator('[data-tour="view-switcher"]'); + const viewButtons = viewSwitcher.getByRole("button"); + const buttonCount = await viewButtons.count().catch(() => 0); + if (buttonCount < 2) { + return; } + + // The file-editor option is the last segment in the shared switcher. + await viewButtons.last().click({ timeout: 1_000 }); } /** diff --git a/frontend/editor/src/core/tests/live/encrypted-unlock-then-tool.spec.ts b/frontend/editor/src/core/tests/live/encrypted-unlock-then-tool.spec.ts index ef03c54369..512e71dc06 100644 --- a/frontend/editor/src/core/tests/live/encrypted-unlock-then-tool.spec.ts +++ b/frontend/editor/src/core/tests/live/encrypted-unlock-then-tool.spec.ts @@ -31,7 +31,6 @@ test.describe("Encrypted PDF: unlock then merge", () => { await page.goto("/merge"); await page.waitForLoadState("domcontentloaded"); - await page.getByTestId("files-button").click(); await page .locator('[data-testid="file-input"]') .setInputFiles([ENCRYPTED_PDF, SAMPLE_PDF]); diff --git a/frontend/editor/src/core/tests/stubbed/certificate-validation.spec.ts b/frontend/editor/src/core/tests/stubbed/certificate-validation.spec.ts index 1f8886bab4..09df086fc2 100644 --- a/frontend/editor/src/core/tests/stubbed/certificate-validation.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/certificate-validation.spec.ts @@ -1,5 +1,10 @@ import { test, expect, type Page } from "@playwright/test"; import path from "path"; +import { + mockAppApis, + seedCookieConsent, + skipOnboarding, +} from "@app/tests/helpers/api-stubs"; // --------------------------------------------------------------------------- // Test fixtures — pre-generated keystores in test-fixtures/certs/ @@ -77,17 +82,10 @@ async function selectCertType(page: Page, label: string) { } // --------------------------------------------------------------------------- -// Helper: upload a file into the Mantine (hidden native input) +// Helper: upload a file into the Mantine via the hidden native input. // --------------------------------------------------------------------------- async function uploadCertFile(page: Page, filePath: string) { - // Mantine FileInput uses a visually hidden . - // We click the visible button to expose it, then set files via the hidden input. - const certFileInput = page.getByTestId("cert-file-input"); - await certFileInput.click(); - // After click, the file chooser or the hidden input becomes interactive. - // Use the first file input on the page (Mantine places it near the button). - const fileInput = page.locator('input[type="file"]').first(); - await fileInput.setInputFiles(filePath); + await page.locator('input[type="file"]').first().setInputFiles(filePath); } // --------------------------------------------------------------------------- @@ -95,6 +93,9 @@ async function uploadCertFile(page: Page, filePath: string) { // --------------------------------------------------------------------------- test.describe("Certificate Validation — ParticipantView", () => { test.beforeEach(async ({ page }) => { + await seedCookieConsent(page); + await skipOnboarding(page); + await mockAppApis(page); await mockParticipantApis(page); }); diff --git a/frontend/editor/src/core/tests/stubbed/convert.spec.ts b/frontend/editor/src/core/tests/stubbed/convert.spec.ts index cd6a60884a..adf30800f2 100644 --- a/frontend/editor/src/core/tests/stubbed/convert.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/convert.spec.ts @@ -23,13 +23,11 @@ async function dismissTourTooltip(page: Page) { } // --------------------------------------------------------------------------- -// Helper: upload a file via the FileSidebar's "Open from computer" action. -// The button now triggers the native OS picker directly - no modal - and -// the hidden `data-testid="file-input"` accepts `setInputFiles` in either -// sidebar state. +// Helper: upload a file via the FileSidebar's hidden native input. +// This avoids native file-picker side effects in Firefox while still +// exercising the same upload path. // --------------------------------------------------------------------------- async function uploadFile(page: Page, filePath: string) { - await page.getByTestId("files-button").click(); await page.locator('[data-testid="file-input"]').setInputFiles(filePath); } diff --git a/frontend/editor/src/core/tests/stubbed/encrypted-pdf-unlock.spec.ts b/frontend/editor/src/core/tests/stubbed/encrypted-pdf-unlock.spec.ts index 7e220a3bda..d888c99ed0 100644 --- a/frontend/editor/src/core/tests/stubbed/encrypted-pdf-unlock.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/encrypted-pdf-unlock.spec.ts @@ -63,8 +63,7 @@ function mockRemovePasswordWrongPassword(page: Page) { } async function uploadEncryptedFile(page: Page, filePath: string) { - await page.getByTestId("files-button").click(); - // No modal flow - `files-button` triggers the native picker directly. + // Drive the hidden input directly to avoid native file-picker side effects. await page.locator('[data-testid="file-input"]').setInputFiles(filePath); } @@ -153,8 +152,6 @@ test.describe("Encrypted PDF Unlock Modal", () => { }) => { await mockRemovePasswordSuccess(page); - await page.getByTestId("files-button").click(); - // No modal flow - `files-button` triggers the native picker directly. await page.locator('[data-testid="file-input"]').setInputFiles([ { name: "encrypted-a.pdf", diff --git a/frontend/editor/src/core/tests/stubbed/files-page-screenshots.spec.ts b/frontend/editor/src/core/tests/stubbed/files-page-screenshots.spec.ts index da4d889a9d..2256f9ab50 100644 --- a/frontend/editor/src/core/tests/stubbed/files-page-screenshots.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/files-page-screenshots.spec.ts @@ -13,6 +13,7 @@ interface SeedFile { } async function seedFiles(page: Page, files: SeedFile[]): Promise { + const seedKey = "stirling-pdf-files-page-seeded"; // Build the server-side view from the cloud entries so reconcileServerFiles // sees them as still-existing on the server (otherwise they get detached // and the cloud cards vanish before the screenshot is taken). @@ -36,7 +37,10 @@ async function seedFiles(page: Page, files: SeedFile[]): Promise { route.fulfill({ json: serverFiles }), ); await page.addInitScript( - ({ records, dbVersion }) => { + ({ records, dbVersion, seedKey }) => { + if (window.localStorage.getItem(seedKey) === "1") { + return; + } const open = window.indexedDB.open("stirling-pdf-files", dbVersion); open.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; @@ -93,10 +97,20 @@ async function seedFiles(page: Page, files: SeedFile[]): Promise { remoteShareToken: null, }); } - tx.oncomplete = () => db.close(); + tx.oncomplete = () => { + db.close(); + window.localStorage.setItem(seedKey, "1"); + if (location.pathname.startsWith("/files")) { + location.reload(); + } + }; }; }, - { records: files, dbVersion: DATABASE_CONFIGS.FILES.version }, + { + records: files, + dbVersion: DATABASE_CONFIGS.FILES.version, + seedKey, + }, ); } @@ -462,6 +476,7 @@ test.describe("Files page screenshots", () => { { id: "bravo", name: "bravo.pdf", remoteStorageId: null }, ]); await page.goto("/files", { waitUntil: "domcontentloaded" }); + await page.waitForLoadState("domcontentloaded"); await expect( page.locator(".files-page-card:not(.files-page-skeleton-card)").first(), ).toBeVisible({ @@ -478,6 +493,7 @@ test.describe("Files page screenshots", () => { { id: "alpha", name: "alpha.pdf", remoteStorageId: null }, ]); await page.goto("/files", { waitUntil: "domcontentloaded" }); + await page.waitForLoadState("domcontentloaded"); await expect( page.locator(".files-page-card:not(.files-page-skeleton-card)").first(), ).toBeVisible({ @@ -506,6 +522,7 @@ test.describe("Files page screenshots", () => { { id: "alpha", name: "alpha.pdf", remoteStorageId: null }, ]); await page.goto("/files", { waitUntil: "domcontentloaded" }); + await page.waitForLoadState("domcontentloaded"); await expect( page.locator(".files-page-card:not(.files-page-skeleton-card)").first(), ).toBeVisible({ diff --git a/frontend/editor/src/core/tests/stubbed/files-page.spec.ts b/frontend/editor/src/core/tests/stubbed/files-page.spec.ts index 081d1c83f1..ec1af4dc8d 100644 --- a/frontend/editor/src/core/tests/stubbed/files-page.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/files-page.spec.ts @@ -14,6 +14,7 @@ interface SeedFile { /** Seed IDB + register the cloud entries with the server stub. */ async function seedFiles(page: Page, files: SeedFile[]): Promise { + const seedKey = "stirling-pdf-files-page-seeded"; // Build the server-side view from the cloud entries so reconcileServerFiles // sees them as still-existing on the server (otherwise they get detached). const serverFiles = files @@ -36,7 +37,10 @@ async function seedFiles(page: Page, files: SeedFile[]): Promise { route.fulfill({ json: serverFiles }), ); await page.addInitScript( - ({ records, dbVersion }) => { + ({ records, dbVersion, seedKey }) => { + if (window.localStorage.getItem(seedKey) === "1") { + return; + } const open = window.indexedDB.open("stirling-pdf-files", dbVersion); open.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; @@ -94,10 +98,20 @@ async function seedFiles(page: Page, files: SeedFile[]): Promise { remoteShareToken: null, }); } - tx.oncomplete = () => db.close(); + tx.oncomplete = () => { + db.close(); + window.localStorage.setItem(seedKey, "1"); + if (location.pathname.startsWith("/files")) { + location.reload(); + } + }; }; }, - { records: files, dbVersion: DATABASE_CONFIGS.FILES.version }, + { + records: files, + dbVersion: DATABASE_CONFIGS.FILES.version, + seedKey, + }, ); } @@ -823,10 +837,8 @@ test.describe("Files page", () => { await page.locator("#filesPage-tab-sharedByMe").click(); const sharedByMeCards = page.locator(".files-page-card:not(.is-folder)"); await expect(sharedByMeCards).toHaveCount(2, { timeout: 3_000 }); - await expect(sharedByMeCards).toContainText([ - "link-shared.pdf", - "user-shared.pdf", - ]); + await expect(sharedByMeCards.filter({ hasText: "link-shared.pdf" })).toHaveCount(1); + await expect(sharedByMeCards.filter({ hasText: "user-shared.pdf" })).toHaveCount(1); // "Shared with me" -> only from-someone-else.pdf await page.locator("#filesPage-tab-shared").click(); diff --git a/frontend/editor/src/core/tests/stubbed/pdf-text-search.spec.ts b/frontend/editor/src/core/tests/stubbed/pdf-text-search.spec.ts index 5ba1599386..56ff90e4b2 100644 --- a/frontend/editor/src/core/tests/stubbed/pdf-text-search.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/pdf-text-search.spec.ts @@ -16,9 +16,8 @@ test.describe("Reader - in-document text search", () => { await page.goto("/read"); await page.waitForLoadState("domcontentloaded"); - // Upload a PDF first so the reader has content. `files-button` now - // triggers the native picker directly - no modal flow involved. - await page.getByTestId("files-button").click(); + // Upload a PDF first so the reader has content. Drive the hidden input + // directly to avoid Firefox file-picker side effects. await page.locator('[data-testid="file-input"]').setInputFiles(SAMPLE_PDF); // The WorkbenchBar exposes a "Search PDF" button (aria-label="Search PDF")