test: stabilize e2e runs against tanstack start - #17542
Draft
jacobsfletch wants to merge 17 commits into
Draft
Conversation
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.
Contributor
📦 esbuild Bundle Analysis for payloadThis analysis was generated by esbuild-bundle-analyzer. 🤖 |
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 —AdminPagerenders 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:
/admin/account, the button under test was still unowned by React 8 times out of 12.toBeVisible()is no protection, since the doomed markup is in the DOM from the first byte.page.gotoandpage.reloadnow 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:
/_serverFn/. It polls the database instead. Its twoexpect.pollcalls closed over an already-resolvedfind, so neither was ever retrying — the network wait was doing all the work.passwordsteardown 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:
test/__setup/e2e.initPagetakes the browser context and returns the page, so a spec cannot end up with a page that skipped the setup, and it absorbsensureCompilationIsDonebecause all 78 call sites ran it immediately afterwards.catchConsoleErrorswas registering itspageerrorhandler twice.test/admin/e2e/sidebar-tabsimportedtest/helpers.jsandtest/helpers/*, which have not existed under those names for some time, so the file could not load at all.