diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/__root.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/__root.tsx index 0a268a350e34..204276e1bdf2 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/__root.tsx +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/__root.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from 'react'; +import { useEffect, type ReactNode } from 'react'; import { Outlet, createRootRoute, HeadContent, Scripts } from '@tanstack/react-router'; export const Route = createRootRoute({ @@ -20,6 +20,14 @@ export const Route = createRootRoute({ }); function RootComponent() { + // Mark the document as hydrated so tests can wait for React to attach event + // handlers before clicking. `toBeVisible()` only confirms the SSR HTML is + // rendered; on slow CI runs Playwright can click before hydration completes, + // the onClick handler isn't yet attached, and tests asserting on the + // resulting client-side error time out (see #20641, #20685, #20867). + useEffect(() => { + document.documentElement.setAttribute('data-hydrated', 'true'); + }, []); return ( diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts index a49b77a293b1..235ae6af5dca 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts @@ -13,6 +13,11 @@ test('Sends client-side error to Sentry with auto-instrumentation', async ({ pag await page.goto(`/`); + // Wait for React to hydrate (see __root.tsx) before clicking — the SSR HTML + // renders the button before the onClick handler is attached, and clicking + // pre-hydration would fire no handler and produce no error. + await page.locator('html[data-hydrated="true"]').waitFor(); + await expect(page.locator('button').filter({ hasText: 'Break the client' })).toBeVisible(); await page.locator('button').filter({ hasText: 'Break the client' }).click(); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts index 27f200b8ef62..4024f668cde1 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts @@ -22,6 +22,11 @@ test('Sends client-side errors through the configured tunnel route', async ({ pa await page.goto('/'); const pageOrigin = new URL(page.url()).origin; + // Wait for React to hydrate (see __root.tsx) before clicking — the SSR HTML + // renders the button before the onClick handler is attached, and clicking + // pre-hydration would fire no handler and produce no error. + await page.locator('html[data-hydrated="true"]').waitFor(); + await expect(page.locator('button').filter({ hasText: 'Break the client' })).toBeVisible(); const managedTunnelResponsePromise = page.waitForResponse(response => {