Skip to content
5 changes: 5 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 13 additions & 12 deletions frontend/editor/src/core/tests/helpers/ui-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,7 +52,6 @@ export async function uploadFiles(
filePaths: string | string[],
): Promise<void> {
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
Expand All @@ -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<void> {
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 });
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
Original file line number Diff line number Diff line change
@@ -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/
Expand Down Expand Up @@ -77,24 +82,20 @@ async function selectCertType(page: Page, label: string) {
}

// ---------------------------------------------------------------------------
// Helper: upload a file into the Mantine <FileInput> (hidden native input)
// Helper: upload a file into the Mantine <FileInput> via the hidden native input.
// ---------------------------------------------------------------------------
async function uploadCertFile(page: Page, filePath: string) {
// Mantine FileInput uses a visually hidden <input type="file">.
// 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);
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test.describe("Certificate Validation — ParticipantView", () => {
test.beforeEach(async ({ page }) => {
await seedCookieConsent(page);
await skipOnboarding(page);
await mockAppApis(page);
await mockParticipantApis(page);
});

Expand Down
8 changes: 3 additions & 5 deletions frontend/editor/src/core/tests/stubbed/convert.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface SeedFile {
}

async function seedFiles(page: Page, files: SeedFile[]): Promise<void> {
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).
Expand All @@ -36,7 +37,10 @@ async function seedFiles(page: Page, files: SeedFile[]): Promise<void> {
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;
Expand Down Expand Up @@ -93,10 +97,20 @@ async function seedFiles(page: Page, files: SeedFile[]): Promise<void> {
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,
},
);
}

Expand Down Expand Up @@ -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({
Expand All @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
26 changes: 19 additions & 7 deletions frontend/editor/src/core/tests/stubbed/files-page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface SeedFile {

/** Seed IDB + register the cloud entries with the server stub. */
async function seedFiles(page: Page, files: SeedFile[]): Promise<void> {
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
Expand All @@ -36,7 +37,10 @@ async function seedFiles(page: Page, files: SeedFile[]): Promise<void> {
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;
Expand Down Expand Up @@ -94,10 +98,20 @@ async function seedFiles(page: Page, files: SeedFile[]): Promise<void> {
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,
},
);
}

Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading