Merge branch 'main' into fix_win_desktop_task

This commit is contained in:
Ludy
2026-07-06 13:52:37 +02:00
committed by GitHub
24 changed files with 335 additions and 59 deletions
+1 -1
View File
@@ -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
+3
View File
@@ -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-
+18 -7
View File
@@ -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
+1 -1
View File
@@ -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/
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+20
View File
@@ -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 />
@@ -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.',
@@ -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);
+3 -34
View File
@@ -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>,
);