From 4fec2777ce249a6d815f7739dbe5fafe745aa18a Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sun, 22 Mar 2026 11:31:48 +0100 Subject: [PATCH] test(e2e): fail in unexpected console.error (#696) * test(e2e): fail in unexpected console.error * fix(datetime): graceful fallback during SSR when navigator is undefined --- .../__tests__/use-server-events.test.tsx | 24 ++++++- app/client/hooks/use-file-browser.ts | 5 +- app/client/hooks/use-server-events.ts | 19 +++--- app/client/lib/datetime.ts | 14 +++-- app/client/lib/logger.ts | 7 +++ app/client/modules/auth/routes/login.tsx | 5 +- app/client/modules/auth/routes/onboarding.tsx | 3 +- .../components/backup-codes-dialog.tsx | 3 +- .../components/two-factor-disable-dialog.tsx | 3 +- .../components/two-factor-setup-dialog.tsx | 5 +- .../modules/settings/routes/settings.tsx | 3 +- .../sso/components/sso-login-section.tsx | 3 +- app/lib/auth-routes.ts | 7 +++ app/middleware/auth.ts | 9 +-- app/router.tsx | 3 +- app/routes/__root.tsx | 8 ++- e2e/0001-auth.setup.ts | 2 +- e2e/0002-backup-restore.spec.ts | 3 +- e2e/0003-oidc.spec.ts | 6 +- e2e/helpers/browser-errors.ts | 63 +++++++++++++++++++ e2e/test.ts | 14 +++++ 21 files changed, 168 insertions(+), 41 deletions(-) create mode 100644 app/client/lib/logger.ts create mode 100644 app/lib/auth-routes.ts create mode 100644 e2e/helpers/browser-errors.ts create mode 100644 e2e/test.ts diff --git a/app/client/hooks/__tests__/use-server-events.test.tsx b/app/client/hooks/__tests__/use-server-events.test.tsx index f5a52a19..f6f8baf2 100644 --- a/app/client/hooks/__tests__/use-server-events.test.tsx +++ b/app/client/hooks/__tests__/use-server-events.test.tsx @@ -53,8 +53,8 @@ const createTestQueryClient = () => }, }); -const ConnectionConsumer = () => { - useServerEvents(); +const ConnectionConsumer = ({ enabled = true }: { enabled?: boolean }) => { + useServerEvents({ enabled }); return null; }; @@ -129,4 +129,24 @@ describe("useServerEvents", () => { expect(MockEventSource.instances[0]?.close).toHaveBeenCalledTimes(1); }); + + test("waits to subscribe until enabled", () => { + const queryClient = createTestQueryClient(); + const view = render( + + + , + ); + + expect(MockEventSource.instances).toHaveLength(0); + + view.rerender( + + + , + ); + + expect(MockEventSource.instances).toHaveLength(1); + expect(MockEventSource.instances[0]?.url).toBe("/api/v1/events"); + }); }); diff --git a/app/client/hooks/use-file-browser.ts b/app/client/hooks/use-file-browser.ts index e51baad1..81244638 100644 --- a/app/client/hooks/use-file-browser.ts +++ b/app/client/hooks/use-file-browser.ts @@ -1,4 +1,5 @@ import { useCallback, useMemo, useState } from "react"; +import { logger } from "~/client/lib/logger"; import type { FileEntry } from "../components/file-tree"; export type FetchFolderResult = { @@ -135,7 +136,7 @@ export const useFileBrowser = (props: UseFileBrowserOptions) => { setFetchedFolders((prev) => new Set(prev).add(folderPath)); } catch (error) { - console.error("Failed to fetch folder contents:", error); + logger.error("Failed to fetch folder contents:", error); } finally { setLoadingFolders((prev) => { const next = new Set(prev); @@ -190,7 +191,7 @@ export const useFileBrowser = (props: UseFileBrowserOptions) => { }); } } catch (error) { - console.error("Failed to load more files:", error); + logger.error("Failed to load more files:", error); setFolderPagination((prev) => { const next = new Map(prev); next.set(folderPath, { ...pagination, isLoadingMore: false }); diff --git a/app/client/hooks/use-server-events.ts b/app/client/hooks/use-server-events.ts index 62cb4ac8..b4001501 100644 --- a/app/client/hooks/use-server-events.ts +++ b/app/client/hooks/use-server-events.ts @@ -1,5 +1,6 @@ import { type QueryClient, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useRef } from "react"; +import { logger } from "~/client/lib/logger"; import { serverEventNames, type ServerEventPayloadMap } from "~/schemas/server-events"; type LifecycleEventPayloadMap = { @@ -64,7 +65,7 @@ const refreshQueriesForEvent = (eventName: ServerEventType) => { void sharedState.queryClient.invalidateQueries().catch((error) => { if (!isAbortError(error)) { - console.error(`[SSE] Failed to refresh queries after ${eventName}:`, error); + logger.error(`[SSE] Failed to refresh queries after ${eventName}:`, error); } }); }; @@ -80,7 +81,7 @@ const connectEventSource = (queryClient: QueryClient) => { eventSource.addEventListener("connected", (event) => { const data = parseEventData<"connected">(event); - console.info("[SSE] Connected to server events"); + logger.info("[SSE] Connected to server events"); emit("connected", data); }); @@ -91,7 +92,7 @@ const connectEventSource = (queryClient: QueryClient) => { for (const eventName of serverEventNames) { eventSource.addEventListener(eventName, (event) => { const data = parseEventData(event); - console.info(`[SSE] ${eventName}:`, data); + logger.info(`[SSE] ${eventName}:`, data); refreshQueriesForEvent(eventName); @@ -107,7 +108,7 @@ const connectEventSource = (queryClient: QueryClient) => { } eventSource.onerror = (error) => { - console.error("[SSE] Connection error:", error); + logger.error("[SSE] Connection error:", error); }; }; @@ -116,7 +117,7 @@ const disconnectEventSource = () => { return; } - console.info("[SSE] Disconnecting from server events"); + logger.info("[SSE] Disconnecting from server events"); sharedState.eventSource.close(); sharedState.eventSource = null; sharedState.queryClient = null; @@ -159,12 +160,16 @@ const addSharedEventListener = ( * Hook to listen to Server-Sent Events (SSE) from the backend * Automatically handles cache invalidation for backup and volume events */ -export function useServerEvents() { +export function useServerEvents({ enabled = true }: { enabled?: boolean } = {}) { const queryClient = useQueryClient(); const addEventListener = useCallback(addSharedEventListener, []); const hasMountedRef = useRef(false); useEffect(() => { + if (!enabled) { + return; + } + connectEventSource(queryClient); if (!hasMountedRef.current) { sharedState.subscribers += 1; @@ -182,7 +187,7 @@ export function useServerEvents() { disconnectEventSource(); } }; - }, [queryClient]); + }, [enabled, queryClient]); return { addEventListener }; } diff --git a/app/client/lib/datetime.ts b/app/client/lib/datetime.ts index d9bb7d60..587544ad 100644 --- a/app/client/lib/datetime.ts +++ b/app/client/lib/datetime.ts @@ -2,6 +2,8 @@ import { formatDistanceToNow, isValid } from "date-fns"; type DateInput = Date | string | number | null | undefined; +const getLocales = () => (typeof navigator !== "undefined" ? navigator.languages : undefined); + function formatValidDate(date: DateInput, formatter: (date: Date) => string): string { if (!date) return "Never"; @@ -14,7 +16,7 @@ function formatValidDate(date: DateInput, formatter: (date: Date) => string): st // 1/10/2026, 2:30 PM export function formatDateTime(date: DateInput): string { return formatValidDate(date, (validDate) => - Intl.DateTimeFormat(navigator.languages, { + Intl.DateTimeFormat(getLocales(), { month: "numeric", day: "numeric", year: "numeric", @@ -27,7 +29,7 @@ export function formatDateTime(date: DateInput): string { // Jan 10, 2026 export function formatDateWithMonth(date: DateInput): string { return formatValidDate(date, (validDate) => - Intl.DateTimeFormat(navigator.languages, { + Intl.DateTimeFormat(getLocales(), { month: "short", day: "numeric", year: "numeric", @@ -38,7 +40,7 @@ export function formatDateWithMonth(date: DateInput): string { // 1/10/2026 export function formatDate(date: DateInput): string { return formatValidDate(date, (validDate) => - Intl.DateTimeFormat(navigator.languages, { + Intl.DateTimeFormat(getLocales(), { month: "numeric", day: "numeric", year: "numeric", @@ -49,7 +51,7 @@ export function formatDate(date: DateInput): string { // 1/10 export function formatShortDate(date: DateInput): string { return formatValidDate(date, (validDate) => - Intl.DateTimeFormat(navigator.languages, { + Intl.DateTimeFormat(getLocales(), { month: "numeric", day: "numeric", }).format(validDate), @@ -59,7 +61,7 @@ export function formatShortDate(date: DateInput): string { // 1/10, 2:30 PM export function formatShortDateTime(date: DateInput): string { return formatValidDate(date, (validDate) => - Intl.DateTimeFormat(navigator.languages, { + Intl.DateTimeFormat(getLocales(), { month: "numeric", day: "numeric", hour: "numeric", @@ -71,7 +73,7 @@ export function formatShortDateTime(date: DateInput): string { // 2:30 PM export function formatTime(date: DateInput): string { return formatValidDate(date, (validDate) => - Intl.DateTimeFormat(navigator.languages, { + Intl.DateTimeFormat(getLocales(), { hour: "numeric", minute: "numeric", }).format(validDate), diff --git a/app/client/lib/logger.ts b/app/client/lib/logger.ts new file mode 100644 index 00000000..8b703acb --- /dev/null +++ b/app/client/lib/logger.ts @@ -0,0 +1,7 @@ +const PREFIX = "[APP]"; + +export const logger = { + error: (...args: unknown[]) => console.error(PREFIX, ...args), + warn: (...args: unknown[]) => console.warn(PREFIX, ...args), + info: (...args: unknown[]) => console.info(PREFIX, ...args), +}; diff --git a/app/client/modules/auth/routes/login.tsx b/app/client/modules/auth/routes/login.tsx index 58663c1b..9e9aa9f3 100644 --- a/app/client/modules/auth/routes/login.tsx +++ b/app/client/modules/auth/routes/login.tsx @@ -9,6 +9,7 @@ import { Input } from "~/client/components/ui/input"; import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "~/client/components/ui/input-otp"; import { Label } from "~/client/components/ui/label"; import { authClient } from "~/client/lib/auth-client"; +import { logger } from "~/client/lib/logger"; import { decodeLoginError, getLoginErrorDescription } from "~/client/lib/sso-errors"; import { ResetPasswordDialog } from "../components/reset-password-dialog"; import { useNavigate } from "@tanstack/react-router"; @@ -65,7 +66,7 @@ export function LoginPage({ error }: LoginPageProps = {}) { }); if (error) { - console.error(error); + logger.error(error); toast.error("Login failed", { description: error.message }); return; } @@ -103,7 +104,7 @@ export function LoginPage({ error }: LoginPageProps = {}) { }); if (error) { - console.error(error); + logger.error(error); toast.error("Verification failed", { description: error.message }); setTotpCode(""); return; diff --git a/app/client/modules/auth/routes/onboarding.tsx b/app/client/modules/auth/routes/onboarding.tsx index e477837e..c13954a6 100644 --- a/app/client/modules/auth/routes/onboarding.tsx +++ b/app/client/modules/auth/routes/onboarding.tsx @@ -15,6 +15,7 @@ import { AuthLayout } from "~/client/components/auth-layout"; import { Input } from "~/client/components/ui/input"; import { Button } from "~/client/components/ui/button"; import { authClient } from "~/client/lib/auth-client"; +import { logger } from "~/client/lib/logger"; import { useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { normalizeUsername } from "~/lib/username"; @@ -78,7 +79,7 @@ export function OnboardingPage() { toast.success("Admin user created successfully!"); void navigate({ to: "/download-recovery-key" }); } else if (error) { - console.error(error); + logger.error(error); const errorMessage = error.message ?? "Unknown error"; toast.error("Failed to create admin user", { description: errorMessage }); } diff --git a/app/client/modules/settings/components/backup-codes-dialog.tsx b/app/client/modules/settings/components/backup-codes-dialog.tsx index 6f111332..f996e2d6 100644 --- a/app/client/modules/settings/components/backup-codes-dialog.tsx +++ b/app/client/modules/settings/components/backup-codes-dialog.tsx @@ -13,6 +13,7 @@ import { import { Input } from "~/client/components/ui/input"; import { Label } from "~/client/components/ui/label"; import { authClient } from "~/client/lib/auth-client"; +import { logger } from "~/client/lib/logger"; type BackupCodesDialogProps = { open: boolean; @@ -45,7 +46,7 @@ export const BackupCodesDialog = ({ open, onOpenChange }: BackupCodesDialogProps }); if (error) { - console.error(error); + logger.error(error); toast.error("Failed to generate backup codes", { description: error.message }); return; } diff --git a/app/client/modules/settings/components/two-factor-disable-dialog.tsx b/app/client/modules/settings/components/two-factor-disable-dialog.tsx index 738e672b..c45d3539 100644 --- a/app/client/modules/settings/components/two-factor-disable-dialog.tsx +++ b/app/client/modules/settings/components/two-factor-disable-dialog.tsx @@ -12,6 +12,7 @@ import { import { Input } from "~/client/components/ui/input"; import { Label } from "~/client/components/ui/label"; import { authClient } from "~/client/lib/auth-client"; +import { logger } from "~/client/lib/logger"; type TwoFactorDisableDialogProps = { open: boolean; @@ -44,7 +45,7 @@ export const TwoFactorDisableDialog = ({ open, onOpenChange, onSuccess }: TwoFac }); if (error) { - console.error(error); + logger.error(error); toast.error("Failed to disable 2FA", { description: error.message }); return; } diff --git a/app/client/modules/settings/components/two-factor-setup-dialog.tsx b/app/client/modules/settings/components/two-factor-setup-dialog.tsx index 5266fdc9..b6c54de5 100644 --- a/app/client/modules/settings/components/two-factor-setup-dialog.tsx +++ b/app/client/modules/settings/components/two-factor-setup-dialog.tsx @@ -14,6 +14,7 @@ import { Input } from "~/client/components/ui/input"; import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "~/client/components/ui/input-otp"; import { Label } from "~/client/components/ui/label"; import { authClient } from "~/client/lib/auth-client"; +import { logger } from "~/client/lib/logger"; type TwoFactorSetupDialogProps = { open: boolean; @@ -52,7 +53,7 @@ export const TwoFactorSetupDialog = ({ open, onOpenChange, onSuccess }: TwoFacto }); if (error) { - console.error(error); + logger.error(error); toast.error("Failed to enable 2FA", { description: error.message }); return; } @@ -81,7 +82,7 @@ export const TwoFactorSetupDialog = ({ open, onOpenChange, onSuccess }: TwoFacto }); if (error) { - console.error(error); + logger.error(error); toast.error("Verification failed", { description: error.message }); setVerificationCode(""); return; diff --git a/app/client/modules/settings/routes/settings.tsx b/app/client/modules/settings/routes/settings.tsx index 908fb3a7..a968cd09 100644 --- a/app/client/modules/settings/routes/settings.tsx +++ b/app/client/modules/settings/routes/settings.tsx @@ -19,6 +19,7 @@ import { Input } from "~/client/components/ui/input"; import { Label } from "~/client/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs"; import { authClient } from "~/client/lib/auth-client"; +import { logger } from "~/client/lib/logger"; import { type AppContext } from "~/context"; import { TwoFactorSection } from "../components/two-factor-section"; import { useNavigate, useSearch } from "@tanstack/react-router"; @@ -55,7 +56,7 @@ export function SettingsPage({ appContext, initialMembers, initialSsoSettings, i void navigate({ to: "/login", replace: true }); }, onError: ({ error }) => { - console.error(error); + logger.error(error); toast.error("Logout failed", { description: error.message }); }, }, diff --git a/app/client/modules/sso/components/sso-login-section.tsx b/app/client/modules/sso/components/sso-login-section.tsx index b7629000..e4896d23 100644 --- a/app/client/modules/sso/components/sso-login-section.tsx +++ b/app/client/modules/sso/components/sso-login-section.tsx @@ -2,6 +2,7 @@ import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; import { toast } from "sonner"; import { Button } from "~/client/components/ui/button"; import { authClient } from "~/client/lib/auth-client"; +import { logger } from "~/client/lib/logger"; import { getPublicSsoProvidersOptions } from "~/client/api-client/@tanstack/react-query.gen"; export function SsoLoginSection() { @@ -25,7 +26,7 @@ export function SsoLoginSection() { window.location.href = data.url; }, onError: (error) => { - console.error(error); + logger.error(error); toast.error("SSO Login failed", { description: error.message }); }, }); diff --git a/app/lib/auth-routes.ts b/app/lib/auth-routes.ts new file mode 100644 index 00000000..982e7f2d --- /dev/null +++ b/app/lib/auth-routes.ts @@ -0,0 +1,7 @@ +export function isAuthRoute(pathname: string) { + if (pathname === "/onboarding") return true; + if (pathname === "/login") return true; + if (pathname.match(/^\/login\/[^/]+$/)) return true; + if (pathname.match(/^\/login\/[^/]+\/error$/)) return true; + return false; +} diff --git a/app/middleware/auth.ts b/app/middleware/auth.ts index 94e49201..697c4381 100644 --- a/app/middleware/auth.ts +++ b/app/middleware/auth.ts @@ -3,14 +3,7 @@ import { redirect } from "@tanstack/react-router"; import { auth } from "~/server/lib/auth"; import { getRequestHeaders } from "@tanstack/react-start/server"; import { authService } from "~/server/modules/auth/auth.service"; - -function isAuthRoute(pathname: string): boolean { - if (pathname === "/onboarding") return true; - if (pathname === "/login") return true; - if (pathname.match(/^\/login\/[^/]+$/)) return true; - if (pathname.match(/^\/login\/[^/]+\/error$/)) return true; - return false; -} +import { isAuthRoute } from "~/lib/auth-routes"; export const authMiddleware = createMiddleware().server(async ({ next, request }) => { const headers = getRequestHeaders(); diff --git a/app/router.tsx b/app/router.tsx index 3cb7f74b..b70c2340 100644 --- a/app/router.tsx +++ b/app/router.tsx @@ -2,6 +2,7 @@ import { createRouter } from "@tanstack/react-router"; import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query"; import { routeTree } from "./routeTree.gen"; import { MutationCache, QueryClient } from "@tanstack/react-query"; +import { logger } from "./client/lib/logger"; import { client } from "./client/api-client/client.gen"; import type { BreadcrumbItemData } from "./client/components/app-breadcrumb"; @@ -22,7 +23,7 @@ export function getRouter() { void queryClient.invalidateQueries(); }, onError: (error) => { - console.error("Mutation error:", error); + logger.error("Mutation error:", error); void queryClient.invalidateQueries(); }, }), diff --git a/app/routes/__root.tsx b/app/routes/__root.tsx index c0875ab7..7938de8c 100644 --- a/app/routes/__root.tsx +++ b/app/routes/__root.tsx @@ -1,4 +1,4 @@ -import { Outlet, HeadContent, Scripts, createRootRouteWithContext } from "@tanstack/react-router"; +import { Outlet, HeadContent, Scripts, createRootRouteWithContext, useRouterState } from "@tanstack/react-router"; import appCss from "../app.css?url"; import { apiClientMiddleware } from "~/middleware/api-client"; import type { QueryClient } from "@tanstack/react-query"; @@ -9,6 +9,7 @@ import { useEffect } from "react"; import { ThemeProvider, THEME_COOKIE_NAME } from "~/client/components/theme-provider"; import { createServerFn } from "@tanstack/react-start"; import { getCookie } from "@tanstack/react-start/server"; +import { isAuthRoute } from "~/lib/auth-routes"; const fetchTheme = createServerFn({ method: "GET" }).handler(async () => { const themeCookie = getCookie(THEME_COOKIE_NAME); @@ -46,9 +47,10 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()( errorComponent: (e) =>
{e.error.message}
, }); -function RootLayout() { +export function RootLayout() { const { theme } = Route.useLoaderData(); - useServerEvents(); + const pathname = useRouterState({ select: (state) => state.location.pathname }); + useServerEvents({ enabled: !isAuthRoute(pathname) }); useEffect(() => { document.body.setAttribute("data-app-ready", "true"); return () => { diff --git a/e2e/0001-auth.setup.ts b/e2e/0001-auth.setup.ts index caf679e3..61fe9379 100644 --- a/e2e/0001-auth.setup.ts +++ b/e2e/0001-auth.setup.ts @@ -1,5 +1,5 @@ import fs from "fs"; -import { test, expect } from "@playwright/test"; +import { test, expect } from "./test"; import { resetDatabase } from "./helpers/db"; import path from "node:path"; import { gotoAndWaitForAppReady } from "./helpers/page"; diff --git a/e2e/0002-backup-restore.spec.ts b/e2e/0002-backup-restore.spec.ts index 817c4061..65760e57 100644 --- a/e2e/0002-backup-restore.spec.ts +++ b/e2e/0002-backup-restore.spec.ts @@ -1,7 +1,8 @@ import fs from "node:fs"; import { randomUUID } from "node:crypto"; import path from "node:path"; -import { expect, test, type Page, type TestInfo } from "@playwright/test"; +import { type Page, type TestInfo } from "@playwright/test"; +import { expect, test } from "./test"; import { gotoAndWaitForAppReady } from "./helpers/page"; const testDataPath = path.join(process.cwd(), "playwright", "temp"); diff --git a/e2e/0003-oidc.spec.ts b/e2e/0003-oidc.spec.ts index 14629e8e..c0623042 100644 --- a/e2e/0003-oidc.spec.ts +++ b/e2e/0003-oidc.spec.ts @@ -1,4 +1,6 @@ -import { expect, test, type Browser, type Page } from "@playwright/test"; +import { type Browser, type Page } from "@playwright/test"; +import { expect, test } from "./test"; +import { trackBrowserErrors } from "./helpers/browser-errors"; import { gotoAndWaitForAppReady, waitForAppReady } from "./helpers/page"; const dexOrigin = process.env.E2E_DEX_ORIGIN ?? "http://dex:5557"; @@ -205,6 +207,7 @@ async function withOidcLoginAttempt( origins: [], }, }); + const browserErrorTracker = trackBrowserErrors(context); const page = await context.newPage(); try { @@ -221,6 +224,7 @@ async function withOidcLoginAttempt( } await assertions(page); + browserErrorTracker.assertNoBrowserErrors(); } finally { await context.close().catch(() => undefined); } diff --git a/e2e/helpers/browser-errors.ts b/e2e/helpers/browser-errors.ts new file mode 100644 index 00000000..7a1fdfda --- /dev/null +++ b/e2e/helpers/browser-errors.ts @@ -0,0 +1,63 @@ +import type { BrowserContext, ConsoleMessage, Page } from "@playwright/test"; + +const IGNORED_PATTERNS: RegExp[] = [ + /^\[APP\]/, + /Failed to load resource: the server responded with a status of 4\d{2}/, +]; + +const isIgnoredError = (text: string) => IGNORED_PATTERNS.some((pattern) => pattern.test(text)); + +const formatConsoleMessage = (message: ConsoleMessage) => { + const location = message.location(); + const hasLocation = !!location.url; + const formattedLocation = hasLocation + ? ` (${location.url}:${location.lineNumber + 1}:${location.columnNumber + 1})` + : ""; + + return `console.error${formattedLocation}\n${message.text()}`; +}; + +export const trackBrowserErrors = (context: BrowserContext) => { + const browserErrors: string[] = []; + const trackedPages = new WeakSet(); + + const trackPage = (page: Page) => { + if (trackedPages.has(page)) { + return; + } + + trackedPages.add(page); + + page.on("console", (message) => { + if (message.type() !== "error") { + return; + } + + if (isIgnoredError(message.text())) { + return; + } + + browserErrors.push(formatConsoleMessage(message)); + }); + + page.on("pageerror", (error) => { + browserErrors.push(`pageerror\n${error.stack ?? error.message}`); + }); + }; + + for (const page of context.pages()) { + trackPage(page); + } + + context.on("page", trackPage); + + return { + assertNoBrowserErrors() { + if (browserErrors.length === 0) { + return; + } + + throw new Error(`Browser console errors detected:\n\n${browserErrors.join("\n\n")}`); + }, + }; +}; diff --git a/e2e/test.ts b/e2e/test.ts new file mode 100644 index 00000000..8847fade --- /dev/null +++ b/e2e/test.ts @@ -0,0 +1,14 @@ +import { expect, test as base } from "@playwright/test"; +import { trackBrowserErrors } from "./helpers/browser-errors"; + +export const test = base.extend({ + context: async ({ context }, use) => { + const browserErrorTracker = trackBrowserErrors(context); + + await use(context); + + browserErrorTracker.assertNoBrowserErrors(); + }, +}); + +export { expect };