Merge branch 'main' into fix_win_desktop_task
This commit is contained in:
@@ -285,5 +285,5 @@ jobs:
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report-enterprise-${{ github.run_id }}
|
||||
path: frontend/editor/playwright-report/
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
@@ -202,6 +202,9 @@ jobs:
|
||||
contents: read
|
||||
uses: ./.github/workflows/coverage-aggregate.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
frontend-validation-result: ${{ needs.frontend-validation.result }}
|
||||
playwright-e2e-live-result: ${{ needs.playwright-e2e-live.result }}
|
||||
|
||||
# Single status check that branch protection should mark as required.
|
||||
# Succeeds when every upstream job is either `success` or `skipped` (path-
|
||||
|
||||
@@ -13,6 +13,17 @@ name: Aggregate backend coverage
|
||||
# producers themselves
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
frontend-validation-result:
|
||||
description: Result of the frontend-validation producer job
|
||||
required: false
|
||||
type: string
|
||||
default: skipped
|
||||
playwright-e2e-live-result:
|
||||
description: Result of the playwright-e2e-live producer job
|
||||
required: false
|
||||
type: string
|
||||
default: skipped
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -196,9 +207,9 @@ jobs:
|
||||
# --------------------------------------------------------------
|
||||
- name: Download vitest coverage artifact
|
||||
# frontend-validation uploads as `frontend-coverage`. Tolerate
|
||||
# absence so a backend-only PR still produces the matrix with
|
||||
# just backend rows populated.
|
||||
if: always()
|
||||
# absence on backend-only runs by skipping the download entirely
|
||||
# when the producer job was not part of this workflow run.
|
||||
if: inputs.frontend-validation-result == 'success'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0
|
||||
with:
|
||||
name: frontend-coverage
|
||||
@@ -206,12 +217,12 @@ jobs:
|
||||
continue-on-error: true
|
||||
|
||||
- name: Download Playwright frontend coverage artifact
|
||||
# e2e-live uploads as `playwright-frontend-coverage-<run_id>`.
|
||||
# Same tolerance as vitest - matrix script handles missing inputs.
|
||||
if: always()
|
||||
# e2e-live uploads the artifact with a stable name. Skip the
|
||||
# download entirely when the producer job did not run.
|
||||
if: inputs.playwright-e2e-live-result == 'success'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0
|
||||
with:
|
||||
name: playwright-frontend-coverage-${{ github.run_id }}
|
||||
name: playwright-frontend-coverage
|
||||
path: matrix-inputs/playwright/
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
if: always() && steps.pw-frontend-coverage.outputs.summary == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-frontend-coverage-${{ github.run_id }}
|
||||
name: playwright-frontend-coverage
|
||||
path: |
|
||||
.test-state/playwright/coverage-pw-summary/
|
||||
.test-state/playwright/coverage-pw/
|
||||
|
||||
@@ -50,5 +50,5 @@ jobs:
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report-stubbed-${{ github.run_id }}
|
||||
path: frontend/editor/playwright-report/
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
@@ -98,6 +98,13 @@ jobs:
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Generate frontend license report (Push only)
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
PR_IS_FORK: "false"
|
||||
run: task frontend:licenses:generate
|
||||
|
||||
- name: Generate frontend license report (internal PR)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
env:
|
||||
@@ -353,6 +360,7 @@ jobs:
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Check licenses and generate report
|
||||
id: license-check
|
||||
run: task backend:licenses:generate || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV
|
||||
|
||||
@@ -53,8 +53,8 @@ jobs:
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-nightly-${{ github.run_id }}
|
||||
path: frontend/editor/playwright-report/
|
||||
name: playwright-report-nightly-${{ github.run_id }}
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 14
|
||||
|
||||
# Builds all desktop platforms on a schedule so the Rust dependency cache is
|
||||
|
||||
@@ -80,10 +80,18 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Apache License Version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Apache License version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Apache License, Version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Apache License, version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "The Apache License, Version 2.0"
|
||||
@@ -108,6 +116,10 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Mozilla Public License 2.0 (MPL-2.0)"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Mozilla Public License Version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "CDDL+GPL License"
|
||||
@@ -172,6 +184,14 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Eclipse Public License, Version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "EPL-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "LGPL-2.1-only"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Ubuntu Font Licence 1.0"
|
||||
|
||||
@@ -27,6 +27,7 @@ import { AdminTourOrchestrationProvider } from "@app/contexts/AdminTourOrchestra
|
||||
import { PageEditorProvider } from "@app/contexts/PageEditorContext";
|
||||
import { BannerProvider } from "@app/contexts/BannerContext";
|
||||
import ErrorBoundary from "@app/components/shared/ErrorBoundary";
|
||||
import { usePosthogTracking } from "@app/hooks/usePosthogTracking";
|
||||
import { useScarfTracking } from "@app/hooks/useScarfTracking";
|
||||
import { useAppInitialization } from "@app/hooks/useAppInitialization";
|
||||
import { useLogoAssets } from "@app/hooks/useLogoAssets";
|
||||
@@ -43,6 +44,11 @@ function ScarfTrackingInitializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function PosthogTrackingInitializer() {
|
||||
usePosthogTracking();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Component to run app-level initialization (must be inside AppProviders for context access)
|
||||
function AppInitializer() {
|
||||
useAppInitialization();
|
||||
@@ -122,6 +128,7 @@ export function AppProviders({
|
||||
retryOptions={appConfigRetryOptions}
|
||||
{...appConfigProviderProps}
|
||||
>
|
||||
<PosthogTrackingInitializer />
|
||||
<ScarfTrackingInitializer />
|
||||
<AppConfigLoader />
|
||||
<ServerDefaultsSync />
|
||||
|
||||
+5
@@ -27,6 +27,7 @@ const AddPageNumbersAppearanceSettings = ({
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Tooltip
|
||||
position="left"
|
||||
content={t(
|
||||
"marginTooltip",
|
||||
"Distance between the page number and the edge of the page.",
|
||||
@@ -56,6 +57,7 @@ const AddPageNumbersAppearanceSettings = ({
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
position="left"
|
||||
content={t(
|
||||
"fontSizeTooltip",
|
||||
"Size of the page number text in points. Larger numbers create bigger text.",
|
||||
@@ -73,6 +75,7 @@ const AddPageNumbersAppearanceSettings = ({
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
position="left"
|
||||
content={t(
|
||||
"addPageNumbers.zeroPadTooltip",
|
||||
"Zero-pad (Bates Stamp) page numbers to this width (e.g. 3 => 001). Set 0 to disable.",
|
||||
@@ -90,6 +93,7 @@ const AddPageNumbersAppearanceSettings = ({
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
position="left"
|
||||
content={t(
|
||||
"fontTypeTooltip",
|
||||
"Font family for the page numbers. Choose based on your document style.",
|
||||
@@ -118,6 +122,7 @@ const AddPageNumbersAppearanceSettings = ({
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
position="left"
|
||||
content={t(
|
||||
"customTextTooltip",
|
||||
'Optional custom format for page numbers. Use {n} as placeholder for the number. Example: "Page {n}" will show "Page 1", "Page 2", etc.',
|
||||
|
||||
+2
@@ -49,6 +49,7 @@ const AddPageNumbersPositionSettings = ({
|
||||
</Text>
|
||||
|
||||
<Tooltip
|
||||
position="left"
|
||||
content={t(
|
||||
"pageSelectionPrompt",
|
||||
"Custom Page Selection (Enter a comma-separated list of page numbers 1,5,6 or Functions like 2n+1)",
|
||||
@@ -69,6 +70,7 @@ const AddPageNumbersPositionSettings = ({
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
position="left"
|
||||
content={t(
|
||||
"startingNumberTooltip",
|
||||
"The first number to display. Subsequent pages will increment from this number.",
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { ReactNode } from "react";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AppConfigProvider } from "@app/contexts/AppConfigContext";
|
||||
|
||||
const posthogState = vi.hoisted(() => ({ loaded: false }));
|
||||
const posthogMock = vi.hoisted(() => ({
|
||||
get __loaded() {
|
||||
return posthogState.loaded;
|
||||
},
|
||||
init: vi.fn(() => {
|
||||
posthogState.loaded = true;
|
||||
}),
|
||||
opt_out_capturing: vi.fn(),
|
||||
opt_in_capturing: vi.fn(),
|
||||
set_config: vi.fn(),
|
||||
has_opted_in_capturing: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
vi.mock("posthog-js", () => ({
|
||||
default: posthogMock,
|
||||
}));
|
||||
|
||||
import { usePosthogTracking } from "@app/hooks/usePosthogTracking";
|
||||
|
||||
describe("usePosthogTracking", () => {
|
||||
beforeEach(() => {
|
||||
posthogState.loaded = false;
|
||||
posthogMock.init.mockClear();
|
||||
posthogMock.opt_out_capturing.mockClear();
|
||||
posthogMock.opt_in_capturing.mockClear();
|
||||
posthogMock.set_config.mockClear();
|
||||
vi.stubEnv("VITE_PUBLIC_POSTHOG_KEY", "test-key");
|
||||
vi.stubEnv("VITE_PUBLIC_POSTHOG_HOST", "https://eu.i.posthog.com");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("does not initialize PostHog when analytics is disabled", async () => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<AppConfigProvider
|
||||
initialConfig={{ enableAnalytics: false }}
|
||||
bootstrapMode="non-blocking"
|
||||
autoFetch={false}
|
||||
>
|
||||
{children}
|
||||
</AppConfigProvider>
|
||||
);
|
||||
|
||||
renderHook(() => usePosthogTracking(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(posthogMock.init).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("initializes PostHog when analytics is enabled", async () => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<AppConfigProvider
|
||||
initialConfig={{ enableAnalytics: true }}
|
||||
bootstrapMode="non-blocking"
|
||||
autoFetch={false}
|
||||
>
|
||||
{children}
|
||||
</AppConfigProvider>
|
||||
);
|
||||
|
||||
renderHook(() => usePosthogTracking(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(posthogMock.init).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useEffect } from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||
|
||||
function applyPosthogConsent(): void {
|
||||
if (typeof window === "undefined" || !posthog.__loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optedIn =
|
||||
window.CookieConsent?.acceptedService?.("posthog", "analytics") || false;
|
||||
|
||||
if (optedIn) {
|
||||
posthog.set_config({ persistence: "localStorage+cookie" });
|
||||
posthog.opt_in_capturing();
|
||||
return;
|
||||
}
|
||||
|
||||
posthog.opt_out_capturing();
|
||||
posthog.set_config({ persistence: "memory" });
|
||||
}
|
||||
|
||||
function ensurePosthogInitialized(): boolean {
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const posthogKey = import.meta.env.VITE_PUBLIC_POSTHOG_KEY;
|
||||
const posthogHost = import.meta.env.VITE_PUBLIC_POSTHOG_HOST;
|
||||
|
||||
if (!posthogKey || !posthogHost) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!posthog.__loaded) {
|
||||
posthog.init(posthogKey, {
|
||||
api_host: posthogHost,
|
||||
defaults: "2025-05-24",
|
||||
capture_exceptions: true,
|
||||
debug: false,
|
||||
opt_out_capturing_by_default: true,
|
||||
persistence: "memory",
|
||||
cross_subdomain_cookie: false,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function usePosthogTracking(): void {
|
||||
const { config } = useAppConfig();
|
||||
|
||||
useEffect(() => {
|
||||
const analyticsEnabled = config?.enableAnalytics === true;
|
||||
const posthogEnabled = analyticsEnabled && config?.enablePosthog !== false;
|
||||
|
||||
if (!posthogEnabled) {
|
||||
if (posthog.__loaded) {
|
||||
posthog.opt_out_capturing();
|
||||
posthog.set_config({ persistence: "memory" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensurePosthogInitialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyPosthogConsent();
|
||||
|
||||
const handleConsentChange = () => {
|
||||
applyPosthogConsent();
|
||||
};
|
||||
|
||||
window.addEventListener("cc:onConsent", handleConsentChange);
|
||||
window.addEventListener("cc:onChange", handleConsentChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("cc:onConsent", handleConsentChange);
|
||||
window.removeEventListener("cc:onChange", handleConsentChange);
|
||||
};
|
||||
}, [config?.enableAnalytics, config?.enablePosthog]);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
skipOnboarding,
|
||||
type MockAppApiOptions,
|
||||
} from "@app/tests/helpers/api-stubs";
|
||||
import { suppressNativeFilePicker } from "@app/tests/helpers/ui-helpers";
|
||||
|
||||
/**
|
||||
* Custom Playwright fixture for backend-free specs.
|
||||
@@ -57,6 +58,7 @@ export const test = base.extend<StubFixtures>({
|
||||
seedJwt: [false, { option: true }],
|
||||
|
||||
page: async ({ page, stubOptions, autoGoto, seedJwt }, use) => {
|
||||
suppressNativeFilePicker(page);
|
||||
await seedCookieConsent(page);
|
||||
if (seedJwt) {
|
||||
// Logged-in users hit the orchestrator path that surfaces the
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test as base, expect } from "@playwright/test";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { suppressNativeFilePicker } from "@app/tests/helpers/ui-helpers";
|
||||
|
||||
/**
|
||||
* Custom test fixture that:
|
||||
@@ -40,6 +41,7 @@ const COVERAGE_DIR = path.resolve(
|
||||
|
||||
export const test = base.extend({
|
||||
page: async ({ page }, use, testInfo) => {
|
||||
suppressNativeFilePicker(page);
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: "cc_cookie",
|
||||
|
||||
@@ -11,6 +11,37 @@ import { expect, type Page, type Locator } from "@playwright/test";
|
||||
|
||||
const MANTINE_MODAL_OVERLAY = ".mantine-Modal-overlay";
|
||||
|
||||
/**
|
||||
* Suppress the native OS file picker for the whole page, on every browser.
|
||||
*
|
||||
* Several upload entry points (the FileSidebar "Open from computer" button,
|
||||
* the Mantine `<FileInput>`, AddFileCard, etc.) open a file dialog by clicking
|
||||
* a hidden `<input type="file">`. On firefox/webkit Playwright only intercepts
|
||||
* that dialog while the page has a `filechooser` listener - it toggles
|
||||
* `Page.setInterceptFileChooserDialog` off the event subscription. With no
|
||||
* listener the real OS picker leaks onto the host and hangs the nightly run.
|
||||
*
|
||||
* Registering a (no-op) `filechooser` listener flips that interception on for
|
||||
* every browser, so the dialog is suppressed at the browser level however it
|
||||
* was triggered - a programmatic `.click()`, a `<label>` activation, or
|
||||
* Playwright's own click. We deliberately don't set files in the handler: specs
|
||||
* still drive uploads explicitly via `setInputFiles()`, which sets files
|
||||
* through the protocol regardless of the pending intercepted chooser. This lets
|
||||
* a spec click the real entry-point button while the picker stays mocked
|
||||
* cross-browser - unlike a global `HTMLInputElement.prototype.click` override,
|
||||
* which misses `<label>`-triggered pickers and never enables Playwright's own
|
||||
* interception.
|
||||
*
|
||||
* Installed once per page by the shared test fixtures (stub + live), so no spec
|
||||
* has to opt in.
|
||||
*/
|
||||
export function suppressNativeFilePicker(page: Page): void {
|
||||
page.on("filechooser", () => {
|
||||
// Interception alone suppresses the native dialog; specs provide the files
|
||||
// themselves via setInputFiles() on the hidden input.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a Mantine Modal overlay to appear or disappear. Most file pickers,
|
||||
* settings dialogs, encrypted-PDF unlock prompts and so on render through
|
||||
@@ -39,8 +70,10 @@ 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.
|
||||
* fires the hidden `data-testid="file-input"`. Its native OS picker is mocked
|
||||
* globally by `suppressNativeFilePicker` (installed by the test fixtures), so
|
||||
* the click is safe on every browser; we then set the files directly on the
|
||||
* input via `setInputFiles`.
|
||||
*
|
||||
* `setInputFiles` doesn't await the input's async onChange (which writes to
|
||||
* IndexedDB via `addFiles`), so without a sync point a caller that follows
|
||||
@@ -73,8 +106,15 @@ export async function switchToEditorIfViewerMode(page: Page): Promise<void> {
|
||||
const goToEditor = page.getByRole("button", {
|
||||
name: /go to file editor/i,
|
||||
});
|
||||
// The affordance only exists while the workbench is transiently in viewer
|
||||
// mode after an upload. The app can auto-leave viewer mode and detach the
|
||||
// button between our visibility check and the click - the transition timing
|
||||
// differs on firefox/webkit, where the detached button hangs a plain
|
||||
// `click()` for the full actionability timeout. Treat a vanished button as
|
||||
// "already in editor mode": swallow the click failure and let the caller's
|
||||
// run-button assertion catch any genuine regression.
|
||||
if (await goToEditor.isVisible({ timeout: 1_000 }).catch(() => false)) {
|
||||
await goToEditor.click();
|
||||
await goToEditor.click({ timeout: 5_000 }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ test.describe("Encrypted PDF: unlock then merge", () => {
|
||||
await page.goto("/merge");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
// `files-button`'s native picker is mocked globally
|
||||
// (suppressNativeFilePicker), so the click is safe cross-browser; set the
|
||||
// files on the hidden input directly.
|
||||
await page.getByTestId("files-button").click();
|
||||
await page
|
||||
.locator('[data-testid="file-input"]')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import path from "path";
|
||||
import { suppressNativeFilePicker } from "@app/tests/helpers/ui-helpers";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test fixtures — pre-generated keystores in test-fixtures/certs/
|
||||
@@ -95,6 +96,10 @@ async function uploadCertFile(page: Page, filePath: string) {
|
||||
// ---------------------------------------------------------------------------
|
||||
test.describe("Certificate Validation — ParticipantView", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Raw @playwright/test fixture: install the picker suppression directly so
|
||||
// clicking the Mantine <FileInput> button is intercepted cross-browser
|
||||
// (firefox/webkit otherwise leak the native dialog and close the page).
|
||||
suppressNativeFilePicker(page);
|
||||
await mockParticipantApis(page);
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import path from "path";
|
||||
import { mockAppApis } from "@app/tests/helpers/api-stubs";
|
||||
import { suppressNativeFilePicker } from "@app/tests/helpers/ui-helpers";
|
||||
|
||||
const FIXTURES_DIR = path.join(__dirname, "../test-fixtures");
|
||||
const SAMPLE_PDF = path.join(FIXTURES_DIR, "sample.pdf");
|
||||
@@ -23,10 +24,10 @@ 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 "Open from computer" action. The
|
||||
// button's native OS picker is mocked globally by `suppressNativeFilePicker`
|
||||
// (test fixtures), so the click is safe cross-browser; the hidden
|
||||
// `data-testid="file-input"` then accepts `setInputFiles` in either state.
|
||||
// ---------------------------------------------------------------------------
|
||||
async function uploadFile(page: Page, filePath: string) {
|
||||
await page.getByTestId("files-button").click();
|
||||
@@ -60,6 +61,11 @@ async function selectToFormat(page: Page, toValue: string) {
|
||||
// ---------------------------------------------------------------------------
|
||||
test.describe("Convert Tool", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// These specs use the raw @playwright/test fixture, so they don't get the
|
||||
// shared stub-test-base suppression - install it here so the picker click
|
||||
// is intercepted cross-browser (firefox/webkit otherwise leak the native
|
||||
// dialog onto the host and close the page).
|
||||
suppressNativeFilePicker(page);
|
||||
await mockAppApis(page);
|
||||
await page.goto("/?bypassOnboarding=true");
|
||||
await page.waitForSelector('[data-testid="files-button"]', {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { test, expect, type Page } from "@playwright/test";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { mockAppApis } from "@app/tests/helpers/api-stubs";
|
||||
import { suppressNativeFilePicker } from "@app/tests/helpers/ui-helpers";
|
||||
|
||||
const FIXTURES_DIR = path.join(__dirname, "../test-fixtures");
|
||||
const ENCRYPTED_PDF = path.join(FIXTURES_DIR, "encrypted.pdf");
|
||||
@@ -63,8 +64,10 @@ function mockRemovePasswordWrongPassword(page: Page) {
|
||||
}
|
||||
|
||||
async function uploadEncryptedFile(page: Page, filePath: string) {
|
||||
// `files-button`'s native picker is mocked globally
|
||||
// (suppressNativeFilePicker), so the click is safe cross-browser; set the
|
||||
// files on the hidden input directly.
|
||||
await page.getByTestId("files-button").click();
|
||||
// No modal flow - `files-button` triggers the native picker directly.
|
||||
await page.locator('[data-testid="file-input"]').setInputFiles(filePath);
|
||||
}
|
||||
|
||||
@@ -76,6 +79,10 @@ test.describe.configure({ mode: "serial" });
|
||||
|
||||
test.describe("Encrypted PDF Unlock Modal", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Raw @playwright/test fixture: install the picker suppression directly so
|
||||
// the files-button click is intercepted cross-browser (firefox/webkit
|
||||
// otherwise leak the native dialog onto the host and close the page).
|
||||
suppressNativeFilePicker(page);
|
||||
await mockAppApis(page);
|
||||
await page.goto("/?bypassOnboarding=true");
|
||||
await page.waitForSelector('[data-testid="files-button"]', {
|
||||
@@ -153,8 +160,9 @@ test.describe("Encrypted PDF Unlock Modal", () => {
|
||||
}) => {
|
||||
await mockRemovePasswordSuccess(page);
|
||||
|
||||
// `files-button`'s native picker is mocked globally; click it, then set
|
||||
// the files on the hidden input directly.
|
||||
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",
|
||||
|
||||
@@ -445,8 +445,17 @@ test.describe("Files page screenshots", () => {
|
||||
localStorage.setItem("i18nextLng", "ar-AR");
|
||||
localStorage.setItem("stirling-language", "ar-AR");
|
||||
localStorage.setItem("stirling-language-source", "user");
|
||||
document.documentElement.setAttribute("dir", "rtl");
|
||||
document.documentElement.setAttribute("lang", "ar-AR");
|
||||
// On webkit, `document.documentElement` is still null when Playwright
|
||||
// runs init scripts, so calling setAttribute directly throws - and that
|
||||
// uncaught error aborts the *following* init script (the IndexedDB seed
|
||||
// in seedFiles), leaving the grid stuck on skeletons. Guard the access
|
||||
// and defer to DOMContentLoaded when the element isn't there yet.
|
||||
const applyDir = () => {
|
||||
document.documentElement.setAttribute("dir", "rtl");
|
||||
document.documentElement.setAttribute("lang", "ar-AR");
|
||||
};
|
||||
if (document.documentElement) applyDir();
|
||||
else document.addEventListener("DOMContentLoaded", applyDir);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,9 @@ 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.
|
||||
// Upload a PDF first so the reader has content. The `files-button` native
|
||||
// picker is mocked globally (suppressNativeFilePicker), so the click is
|
||||
// safe cross-browser; set the files on the hidden input directly.
|
||||
await page.getByTestId("files-button").click();
|
||||
await page.locator('[data-testid="file-input"]').setInputFiles(SAMPLE_PDF);
|
||||
|
||||
|
||||
@@ -96,7 +96,15 @@ test("hovering over text changes the cursor to an I-beam", async ({ page }) => {
|
||||
test("Ctrl+C copies selected text to the clipboard", async ({
|
||||
page,
|
||||
context,
|
||||
browserName,
|
||||
}) => {
|
||||
// Reading the clipboard requires the `clipboard-read` permission, which only
|
||||
// chromium supports via `grantPermissions` (firefox throws "Unknown
|
||||
// permission"; webkit can't expose `navigator.clipboard.readText` in tests).
|
||||
test.skip(
|
||||
browserName !== "chromium",
|
||||
"clipboard read/permissions are chromium-only in Playwright",
|
||||
);
|
||||
test.setTimeout(60_000);
|
||||
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||
const firstPage = await loadSampleAndOpenViewer(page);
|
||||
@@ -169,7 +177,15 @@ test("right-click on the page does not surface the browser context menu", async
|
||||
test("floating Copy menu appears after drag-select and copies", async ({
|
||||
page,
|
||||
context,
|
||||
browserName,
|
||||
}) => {
|
||||
// Verifying the copy result reads the clipboard, which needs the
|
||||
// `clipboard-read` permission - chromium-only in Playwright. The Copy menu's
|
||||
// appearance is covered cross-browser by the right-click test above.
|
||||
test.skip(
|
||||
browserName !== "chromium",
|
||||
"clipboard read/permissions are chromium-only in Playwright",
|
||||
);
|
||||
test.setTimeout(60_000);
|
||||
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||
const firstPage = await loadSampleAndOpenViewer(page);
|
||||
|
||||
@@ -13,8 +13,6 @@ import { ColorSchemeScript } from "@mantine/core";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import App from "@app/App";
|
||||
import "@app/i18n"; // Initialize i18next
|
||||
import posthog from "posthog-js";
|
||||
import { PostHogProvider } from "@posthog/react";
|
||||
import { BASE_PATH } from "@app/constants/app";
|
||||
|
||||
import { startEagerWasmCompilation } from "@app/services/wasmPrecompiler";
|
||||
@@ -35,33 +33,6 @@ if (typeof window !== "undefined") {
|
||||
}
|
||||
}
|
||||
|
||||
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
|
||||
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
|
||||
defaults: "2025-05-24",
|
||||
capture_exceptions: true, // This enables capturing exceptions using Error Tracking, set to false if you don't want this
|
||||
debug: false,
|
||||
opt_out_capturing_by_default: true, // Opt-out by default, controlled by cookie consent
|
||||
persistence: "memory", // No cookies/localStorage written until user opts in
|
||||
cross_subdomain_cookie: false,
|
||||
});
|
||||
|
||||
function updatePosthogConsent() {
|
||||
if (!posthog.__loaded) return;
|
||||
const optIn =
|
||||
window.CookieConsent?.acceptedService?.("posthog", "analytics") || false;
|
||||
if (optIn) {
|
||||
posthog.set_config({ persistence: "localStorage+cookie" });
|
||||
posthog.opt_in_capturing();
|
||||
} else {
|
||||
posthog.opt_out_capturing();
|
||||
posthog.set_config({ persistence: "memory" });
|
||||
}
|
||||
console.log("Updated PostHog consent: ", optIn ? "opted in" : "opted out");
|
||||
}
|
||||
|
||||
window.addEventListener("cc:onConsent", updatePosthogConsent);
|
||||
window.addEventListener("cc:onChange", updatePosthogConsent);
|
||||
|
||||
const container = document.getElementById("root");
|
||||
if (!container) {
|
||||
throw new Error("Root container missing in index.html");
|
||||
@@ -71,10 +42,8 @@ const root = ReactDOM.createRoot(container); // Finds the root DOM element
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ColorSchemeScript />
|
||||
<PostHogProvider client={posthog}>
|
||||
<BrowserRouter basename={BASE_PATH}>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</PostHogProvider>
|
||||
<BrowserRouter basename={BASE_PATH}>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user