Skip to content

test: stabilize e2e runs against tanstack start - #17542

Draft
jacobsfletch wants to merge 17 commits into
mainfrom
test/tanstack-auth-e2e
Draft

test: stabilize e2e runs against tanstack start#17542
jacobsfletch wants to merge 17 commits into
mainfrom
test/tanstack-auth-e2e

Conversation

@jacobsfletch

@jacobsfletch jacobsfletch commented Jul 29, 2026

Copy link
Copy Markdown
Member

Fixes the setup races that made e2e runs against TanStack Start unreliable, so suites can be enabled for it incrementally without each one re-discovering the same failures.

The problem occurred when interactions like a click or a fill were placed immediately after page.goto: they were silently dropped. On a full document load the adapter does not hydrate the SSR'd admin view in place — AdminPage renders an RSC payload fetched by the route loader, so the server markup is torn down and re-mounted as different DOM nodes a few hundred ms later.

Three contributing factors:

  1. The hydration marker was installed by the app shell, so it reported readiness once the shell hydrated, before the loader payload mounted. Sampling 12 navigations to /admin/account, the button under test was still unowned by React 8 times out of 12.
  2. Unlike Next.js there is no dehydrated boundary for React to replay a discrete event into, so an interaction in that window is lost outright rather than deferred — the element it targeted no longer exists.
  3. toBeVisible() is no protection, since the doomed markup is in the DOM from the first byte.

page.goto and page.reload now wait on two signals: the marker, which tracks router state rather than latching on first mount, and React ownership of the admin template, which lands in the same commit as every interactive element inside it. Tests no longer need per-call hydration helpers.

Two more fixes fell out of the same suite:

  • The document-lock test waited on the framework's server-function request, which meant branching on Next.js server actions vs /_serverFn/. It polls the database instead. Its two expect.poll calls closed over an already-resolved find, so neither was ever retrying — the network wait was doing all the work.
  • The passwords teardown drove the account view to restore the dev user's password, making cleanup depend on the shared page surviving the whole spec file. It resets through the local API now.

Related:

  • Consolidates per-test setup under test/__setup/e2e. initPage takes the browser context and returns the page, so a spec cannot end up with a page that skipped the setup, and it absorbs ensureCompilationIsDone because all 78 call sites ran it immediately afterwards. catchConsoleErrors was registering its pageerror handler twice.
  • test/admin/e2e/sidebar-tabs imported test/helpers.js and test/helpers/*, which have not existed under those names for some time, so the file could not load at all.

Enables the `auth` e2e suite for the TanStack Start adapter and fixes the tests
that only passed on Next.js.

Adds `waitForElementHydration` and `clickWhenHydrated` e2e helpers. The existing
`installTanStackHydrationGotoWait` only waits for the admin root's hydration
marker, but client components streamed inside a view hydrate a few hundred ms
later while their SSR'd markup is in the DOM from the first byte. Interactions
landing in that window are silently dropped: a click focuses the element without
running `onClick`, and a `fill` sets the DOM value without React seeing the
change. `toBeVisible()` is no protection, since the markup is already there.

The document-lock test now polls the database instead of waiting on the
framework's server-function request, which had to branch on Next.js server
actions vs `/_serverFn/`. Its two `expect.poll` calls closed over an
already-resolved `find`, so neither was ever retrying — the network wait was
doing all the work.
@jacobsfletch
jacobsfletch requested a review from denolfe as a code owner July 29, 2026 18:46
@jacobsfletch jacobsfletch changed the title test(auth): run auth e2e suite against tanstack start test: run auth e2e suite against tanstack start Jul 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📦 esbuild Bundle Analysis for payload

This analysis was generated by esbuild-bundle-analyzer. 🤖
This PR introduced no changes to the esbuild bundle! 🙌

@jacobsfletch
jacobsfletch marked this pull request as draft July 29, 2026 19:31
A full document load on the TanStack Start adapter does not hydrate the SSR'd admin
view in place. `AdminPage` renders an RSC payload fetched by the route loader, so the
server markup is torn down and re-mounted as different DOM nodes a few hundred ms
later. An interaction landing in that window is lost outright — the element it
targeted no longer exists — and unlike Next.js there is no dehydrated boundary for
React to replay the event into. `toBeVisible()` is no protection, since the doomed
markup is on screen from the first byte.

`page.goto` / `page.reload` now wait on two signals, because neither alone is
sufficient. The hydration marker tracks router state rather than latching on first
shell mount, so it reports the loader settling and stays honest across later
navigations. React ownership of the admin template then covers the commit that
follows router-idle by a measured 60-135ms, and lands in the same commit as every
interactive element inside the view.

Tests no longer need per-call hydration helpers before the first interaction after a
navigation.

Consolidates the three duplicated `HydrationMarker` copies into one shared
implementation and moves the `goto` patch out of `helpers.ts` into `goTo.ts`.
Splits per-test setup into single-responsibility modules under `test/__setup` and
routes every spec through one entry point.

`initPage` now takes the browser context and returns the page, so there is no way to
end up with a page that skipped the setup. It accepts `page` instead for the handful of
suites whose page comes from Playwright's fixture. Because all 78 call sites ran
`ensureCompilationIsDone` immediately afterwards, that moves inside too — the ~60
`beforeEach` callers that re-check after `reInitializeDB` keep calling it directly.

Also fixes three things found while wiring it up:

- `catchConsoleErrors` registered its `pageerror` handler twice, so an uncaught page
  error was thrown or recorded twice.
- `test/__setup` was in no tsconfig `include`, leaving the new modules unparsed by
  eslint and unchecked by tsc.
- `test/admin/e2e/sidebar-tabs` imported `test/helpers.js` and `test/helpers/*`, which
  have not existed under those names for some time, so the file could not load at all.

Moves `initPageConsoleErrorCatch` and `ensureCompilationIsDone` out of
`__helpers/e2e/helpers.ts`, and `initPayloadInt` out of `__helpers/shared`.
Separates the page/browser setup used by Playwright specs from the Payload
bootstrapping used by vitest specs, so a spec only reaches into the half it needs.

`getSDK` and `NextRESTClient` move out of `__helpers/shared` alongside
`initPayloadInt`, which is their only non-int consumer. `runInit` stays at the test
root, since `dev.ts` shares it and it is not test-specific.
The `passwords` teardown drove the account view to restore the dev user's password,
which made cleanup depend on the shared page surviving the whole spec file and being
hydrated, plus a save toast. When the page was gone by teardown it failed with "Target
page, context or browser has been closed", and Playwright attributed that single hook
failure to every test in the describe.

Resetting via the local API needs none of that.
@jacobsfletch jacobsfletch changed the title test: run auth e2e suite against tanstack start test: stabilize e2e runs against tanstack start Jul 29, 2026
Two conflicts:

- `test/server-functions/e2e.spec.ts` — deleted on main by #17525, which consolidated the
  suite into `test/auth/server-functions`. Took the deletion; this branch had only
  rewritten its imports.
- `test/auth/removed-token/int.spec.ts` — took main's added `config` import alongside this
  branch's moved `initPayloadInt` path.
The wait was keyed off the view template, which is part of the view — so on any page
whose view had not rendered yet the guard saw nothing to wait for and skipped the wait
entirely, leaving the original race. Measured on /admin/server-functions, the template
was absent at marker-true in 3 of 3 runs while the shell was present in 3 of 3.
`patchPageMethods` gated every hydration wait on `PAYLOAD_FRAMEWORK`, which the CLI
runner sets but editor test runners do not. Running the suite from an editor against a
TanStack dev server therefore disabled the waits silently while the tests still ran, so
interactions after `page.goto` raced again and the server-function tests failed.

`HydrationMarker` renders a marker element instead of returning null, and the wrapper
looks for that. It has to be server-rendered markup rather than a global: the existing
`window.__TANSTACK_*` flags are only assigned during hydration, which is after `goto`
resolves and so too late to decide whether to wait.
Reverts the `__setup/int` half of the earlier split. Separating the vitest bootstrapping
is worth doing but is unrelated to stabilising the e2e runs, so it does not belong in this
change.

`__setup/e2e` stays: those modules are what the Playwright specs go through.
The console and pageerror filters already ignored hydration mismatches, but only
by their development message. A production build reports the error code instead,
so the prose match never fired there and every affected navigation failed its
test on a console error rather than on an assertion.

Matches the codes as well: 418 server/client markup mismatch, 422 and 423 errors
while hydrating, 425 text content mismatch.

Note this un-gates the underlying mismatch rather than fixing it — prod runs will
no longer fail on one.
The root `app-tanstack` is the pristine source that ships into user projects, but
it carried a copy of the Playwright hydration marker — a test-only component that
sets `window.__TANSTACK_HYDRATED__` so the `goto` wrapper can wait for React to
attach handlers.

Nothing needed it there. Vite resolves `srcDirectory` against the `test/` root, so
runs serve `test/app-tanstack` or a per-suite app, and each of those re-exports the
marker from `test/__helpers/components/HydrationMarker`. The root copy was also a
stale fork, predating the `data-tanstack-app` element the wrapper now looks for.

Also collapses the `_payload` route to render `payloadLayoutRoute`'s component
directly, since wrapping it existed only to mount the marker alongside.
The fallback resolves `test/app-tanstack`, not the root `app-tanstack` the comment
claimed — `dirname` is `test/`. Names the right directory and points at
`vite.tanstack.config.ts`, which picks `srcDirectory` the same way.
…ronment

The four auth server-function e2e tests failed against a production TanStack build
with "Server function info not found". A server function only enters an
environment's resolver manifest when that environment transforms the module
defining it, and the RSC environment — the one serving the server-function RPC —
registered just the three the app itself owns. The suite's own functions are
imported solely by its client components, which the RSC build swaps for client
references without traversing into them, so the client and SSR bundles resolved
them while RSC did not.

Graph membership alone is not enough: importing them from the admin view, which
arrives late through the generated import map, still left the manifest at three.
Only route modules are reached early enough, which is why `live-preview`'s
per-suite functions work — its routes import them directly.

Adds `@payload-suite-server-functions`, resolved to
`test/<suite>/tanstackServerFunctions.ts` or an empty stub, imported for its side
effect by the base app's `_payload` route. Suites needing server functions now
declare them in one place instead of shipping a whole standalone app.

Verified against a production build: the RSC manifest goes from 3 ids to 6, the
four tests pass, and `_community` (no such file, so the stub path) still passes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant