mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
f5bee1a26f
* fix(app-router): preserve unstable_catchError boundaries
* test(e2e): add Next-parity test for shouldRerenderAppPageWithGlobalError (#1906)
Add a test proving that for uncaught post-shell RSC errors (with no
client boundary), the server preserves the original HTML stream and the
client renders global-error. This is the Next.js behavior that justifies
keeping shouldRerenderAppPageWithGlobalError unconditional at false.
The server cannot detect userland client boundaries like
unstable_catchError from the route manifest, so the only safe way to
get a global-error rerender is to make a targeted error-boundary test.
The test uses a loading.tsx boundary so the error is truly post-shell,
not caught by the pre-shell error-recovery catch block.
* fix(error): restore sentinel wrapper for falsy thrown values in unstable_catchError
Restores the _CatchErrorState sentinel wrapper ({ thrownValue: unknown }) so
falsy thrown values (null, undefined, 0, "") are routed to the fallback
instead of being treated as no error. Also restores ErrorInfo.error as unknown.
Adds back unit tests for falsy values, class-component lifecycle, and
unstable_retry behavior parity with the component-rendered fallback shape.
Refs: #1906
* fix(error): align ambient ErrorInfo.error type with runtime implementation
* refactor(app-router): remove shouldRerenderAppPageWithGlobalError dead code
The function always returned false and its _options parameter and
ShouldRerenderAppPageWithGlobalErrorOptions type were vestigial.
The call site at app-page-render.ts:892-904 was therefore never taken.
Remove the function, type, import, call site, and the now-redundant
unit test, leaving the shell-level recovery path in
renderAppPageHtmlStreamWithRecovery as the sole error recovery seam.
Addresses ask-bonk review comment on #1906.
* fix(test): restore typed globalAny view and document Fallback as any intent
Addresses two non-blocking review findings from ask-bonk on PR #1906:
- Restored the typed `globalThis as unknown as { window?: unknown }`
pattern for window stubs in the unstable_catchError test section,
replacing the `(globalThis as any).window` pattern that was a type-safety
regression from the prior rewrite.
- Added an inline comment explaining why `Fallback as any` is necessary
in the class-component lifecycle test: the test probes the inner
_CatchError class directly, where the fallback function is used as a
React component type (receiving a single `{ props, errorInfo }` object)
rather than the public API signature `(props, errorInfo)`.
* chore: trigger CI
* fix(test): rename forwardedProps to props in unstable-catchError test
Addresses ask-bonk review finding on PR #1906: the test was seeding
the inner _CatchError class with the stale key name forwardedProps,
but the production wrapper now passes props (and isPagesRouter).
Since this test only exercises the App Router unstable_retry path
(which reads neither key), behavior was unaffected, but renaming
aligns the test with the current boundary shape for future readers.
* refactor(server): remove dead routeHasLocalBoundary field
Removing shouldRerenderAppPageWithGlobalError (commit abb416b)
deleted its only reader of routeHasLocalBoundary. The field is still
declared in the options type and populated on every render, but
nothing consumes it anymore. Removes:
- routeHasLocalBoundary from AppPageRenderOptions type
- The computation in app-page-dispatch.ts
- Two test stubs in app-page-render.test.ts
Addresses ask-bonk review finding on PR #1906.
268 lines
9.1 KiB
TypeScript
268 lines
9.1 KiB
TypeScript
import { defineConfig } from "@playwright/test";
|
|
|
|
const appRouterBrowserSpecificTests = "**/app-router/**/*.browser.spec.ts";
|
|
const appRouterServer = {
|
|
command: "npx vp dev --port 4174",
|
|
cwd: "./tests/fixtures/app-basic",
|
|
port: 4174,
|
|
reuseExistingServer: !process.env.CI,
|
|
timeout: 30_000,
|
|
};
|
|
|
|
// Dedicated server for BFCache/cacheComponents coverage. cacheComponents is an
|
|
// opt-in semantic mode that retains inactive route trees as hidden Activity DOM.
|
|
// Running it on the shared app-basic fixture would change the DOM contract for
|
|
// every unrelated app-router test, so it gets its own isolated fixture instead.
|
|
const appRouterBfcacheServer = {
|
|
command: "npx vp dev --port 4183",
|
|
cwd: "./tests/fixtures/app-bfcache",
|
|
port: 4183,
|
|
reuseExistingServer: !process.env.CI,
|
|
timeout: 30_000,
|
|
};
|
|
|
|
/**
|
|
* Each project maps to a single webServer. Some browser-specific projects share
|
|
* a server with the base project, and shared servers are de-duped by port.
|
|
* Browser-specific production tests may opt out with server: null when they own
|
|
* their build/server lifecycle inside the test fixture.
|
|
* When PLAYWRIGHT_PROJECT is set
|
|
* (e.g. in CI matrix jobs), only that project and its server are configured,
|
|
* so each CI runner only starts the one server it needs.
|
|
*/
|
|
const projectServers = {
|
|
"pages-router": {
|
|
testDir: "./tests/e2e/pages-router",
|
|
use: { baseURL: "http://localhost:4173" },
|
|
server: {
|
|
command: "npx vp run vinext#build && npx vp dev --port 4173",
|
|
cwd: "./tests/fixtures/pages-basic",
|
|
port: 4173,
|
|
reuseExistingServer: !process.env.CI,
|
|
timeout: 30_000,
|
|
},
|
|
},
|
|
"app-router": {
|
|
testDir: "./tests/e2e",
|
|
testMatch: ["**/app-router/**/*.spec.ts", "**/og-image.spec.ts"],
|
|
testIgnore: appRouterBrowserSpecificTests,
|
|
use: { baseURL: "http://localhost:4174" },
|
|
server: appRouterServer,
|
|
},
|
|
"app-router-chrome-browser-specific": {
|
|
testDir: "./tests/e2e",
|
|
testMatch: [appRouterBrowserSpecificTests],
|
|
use: {
|
|
browserName: "chromium" as const,
|
|
channel: "chrome" as const,
|
|
},
|
|
server: null,
|
|
},
|
|
"app-router-webkit-browser-specific": {
|
|
testDir: "./tests/e2e",
|
|
testMatch: [appRouterBrowserSpecificTests],
|
|
use: { browserName: "webkit" as const },
|
|
server: null,
|
|
},
|
|
"app-router-bfcache": {
|
|
testDir: "./tests/e2e/app-router-bfcache",
|
|
use: { baseURL: "http://localhost:4183" },
|
|
server: appRouterBfcacheServer,
|
|
},
|
|
"catch-error": {
|
|
testDir: "./tests/e2e/catch-error",
|
|
use: { baseURL: "http://localhost:4185" },
|
|
server: {
|
|
command:
|
|
"npx vp run vinext#build && node ../../../packages/vinext/dist/cli.js build && node ../../../packages/vinext/dist/cli.js start --port 4185",
|
|
cwd: "./tests/fixtures/global-not-found-basic",
|
|
port: 4185,
|
|
reuseExistingServer: false,
|
|
timeout: 60_000,
|
|
},
|
|
},
|
|
"cloudflare-pages-router": {
|
|
testDir: "./tests/e2e",
|
|
testMatch: [
|
|
"**/cloudflare-pages-router/**/*.spec.ts",
|
|
"**/pages-router/instrumentation-startup.spec.ts",
|
|
],
|
|
use: { baseURL: "http://localhost:4177" },
|
|
server: {
|
|
command: "npx vp build && npx wrangler dev --port 4177",
|
|
cwd: "./examples/pages-router-cloudflare",
|
|
port: 4177,
|
|
reuseExistingServer: !process.env.CI,
|
|
timeout: 30_000,
|
|
},
|
|
},
|
|
"pages-router-prod": {
|
|
testDir: "./tests/e2e/pages-router-prod",
|
|
server: {
|
|
// Use node to invoke the CLI directly — npx vinext may not be on PATH
|
|
// in fixture subdirectories since vinext is a workspace dependency.
|
|
command:
|
|
"npx tsc -p ../../../packages/vinext/tsconfig.json && node ../../../packages/vinext/dist/cli.js build && node ../../../packages/vinext/dist/cli.js start --port 4175",
|
|
cwd: "./tests/fixtures/pages-basic",
|
|
port: 4175,
|
|
reuseExistingServer: !process.env.CI,
|
|
timeout: 60_000,
|
|
},
|
|
},
|
|
"cloudflare-workers": {
|
|
testDir: "./tests/e2e",
|
|
testMatch: [
|
|
"**/cloudflare-workers/**/*.spec.ts",
|
|
"**/app-router/instrumentation.spec.ts",
|
|
"**/og-image.spec.ts",
|
|
],
|
|
use: { baseURL: "http://localhost:4176" },
|
|
server: {
|
|
// Build app-router-cloudflare with Vite, then serve with wrangler dev (miniflare)
|
|
command: "npx vp build && npx wrangler dev --config dist/server/wrangler.json --port 4176",
|
|
cwd: "./examples/app-router-cloudflare",
|
|
port: 4176,
|
|
reuseExistingServer: !process.env.CI,
|
|
timeout: 60_000,
|
|
},
|
|
},
|
|
"cloudflare-dev": {
|
|
testDir: "./tests/e2e",
|
|
testMatch: [
|
|
"**/cloudflare-dev/**/*.spec.ts",
|
|
"**/app-router/instrumentation.spec.ts",
|
|
"**/og-image.spec.ts",
|
|
],
|
|
use: { baseURL: "http://localhost:4178" },
|
|
server: {
|
|
// Run vite dev (not wrangler) against the cloudflare example so that
|
|
// configureServer() is exercised with @cloudflare/vite-plugin loaded.
|
|
command: "npx vp dev --port 4178",
|
|
cwd: "./examples/app-router-cloudflare",
|
|
port: 4178,
|
|
reuseExistingServer: !process.env.CI,
|
|
timeout: 30_000,
|
|
},
|
|
},
|
|
"cloudflare-pages-router-dev": {
|
|
testDir: "./tests/e2e",
|
|
testMatch: [
|
|
"**/cloudflare-pages-router-dev/**/*.spec.ts",
|
|
"**/pages-router/instrumentation-startup.spec.ts",
|
|
],
|
|
use: { baseURL: "http://localhost:4179" },
|
|
server: {
|
|
command: "npx vp dev --port 4179",
|
|
cwd: "./examples/pages-router-cloudflare",
|
|
port: 4179,
|
|
reuseExistingServer: !process.env.CI,
|
|
timeout: 30_000,
|
|
},
|
|
},
|
|
"static-export": {
|
|
testDir: "./tests/e2e/static-export",
|
|
use: { baseURL: "http://localhost:4180" },
|
|
server: {
|
|
// Build the static export fixture, then serve the output with a
|
|
// lightweight static file server. No vinext runtime is needed —
|
|
// the output is pure pre-rendered HTML files.
|
|
command:
|
|
"npx tsc -p ../../../packages/vinext/tsconfig.json && node ../../../packages/vinext/dist/cli.js build && node ../../../tests/e2e/static-export/serve-static.mjs dist/client 4180",
|
|
cwd: "./tests/fixtures/static-export",
|
|
port: 4180,
|
|
reuseExistingServer: !process.env.CI,
|
|
timeout: 60_000,
|
|
},
|
|
},
|
|
"app-with-src": {
|
|
testDir: "./tests/e2e/app-with-src",
|
|
use: { baseURL: "http://localhost:4181" },
|
|
server: {
|
|
command: "npx vp dev --port 4181",
|
|
cwd: "./tests/fixtures/app-with-src",
|
|
port: 4181,
|
|
reuseExistingServer: !process.env.CI,
|
|
timeout: 30_000,
|
|
},
|
|
},
|
|
"standalone-output": {
|
|
testDir: "./tests/e2e/standalone-output",
|
|
use: { baseURL: "http://localhost:4182" },
|
|
server: {
|
|
// Build vinext CLI, then build the fixture, then start the standalone
|
|
// server. The standalone server.js reads PORT from the environment.
|
|
command:
|
|
"npx tsc -p ../../../packages/vinext/tsconfig.json && node ../../../packages/vinext/dist/cli.js build && PORT=4182 node dist/standalone/server.js",
|
|
cwd: "./tests/fixtures/standalone-output",
|
|
port: 4182,
|
|
reuseExistingServer: !process.env.CI,
|
|
timeout: 60_000,
|
|
},
|
|
},
|
|
"root-layout-redirect": {
|
|
testDir: "./tests/e2e/root-layout-redirect",
|
|
use: { baseURL: "http://localhost:4184" },
|
|
server: {
|
|
// Build vinext CLI, then build the fixture, then start the production
|
|
// server. This exercises prodOnCaughtError (the fixed code path) rather
|
|
// than devOnCaughtError, which already filtered navigation-signal errors
|
|
// before this PR.
|
|
command:
|
|
"npx tsc -p ../../../packages/vinext/tsconfig.json && node ../../../packages/vinext/dist/cli.js build && node ../../../packages/vinext/dist/cli.js start --port 4184",
|
|
cwd: "./tests/fixtures/root-layout-redirect",
|
|
port: 4184,
|
|
reuseExistingServer: !process.env.CI,
|
|
timeout: 60_000,
|
|
},
|
|
},
|
|
};
|
|
|
|
type ProjectName = keyof typeof projectServers;
|
|
|
|
const selected = process.env.PLAYWRIGHT_PROJECT;
|
|
|
|
if (selected && !(selected in projectServers)) {
|
|
throw new Error(
|
|
`Unknown PLAYWRIGHT_PROJECT: "${selected}". ` +
|
|
`Valid: ${Object.keys(projectServers).join(", ")}`,
|
|
);
|
|
}
|
|
|
|
const activeProjects: ProjectName[] = selected
|
|
? [selected as ProjectName]
|
|
: (Object.keys(projectServers) as ProjectName[]);
|
|
|
|
export default defineConfig({
|
|
testDir: "./tests/e2e",
|
|
timeout: 30_000,
|
|
retries: 1,
|
|
// GitHub reporter adds inline failure annotations in PR diffs.
|
|
reporter: process.env.CI ? [["list"], ["github"]] : [["list"]],
|
|
use: {
|
|
headless: true,
|
|
// Most projects use Chromium by default. Browser-specific projects override this.
|
|
browserName: "chromium",
|
|
},
|
|
projects: activeProjects.map((name) => {
|
|
const p = projectServers[name];
|
|
return {
|
|
name,
|
|
testDir: p.testDir,
|
|
...("testMatch" in p ? { testMatch: p.testMatch } : {}),
|
|
...("testIgnore" in p ? { testIgnore: p.testIgnore } : {}),
|
|
...("use" in p ? { use: p.use } : {}),
|
|
};
|
|
}),
|
|
webServer: [
|
|
...new Map(
|
|
activeProjects
|
|
.map((name) => projectServers[name].server)
|
|
.filter(
|
|
(server): server is NonNullable<(typeof projectServers)[ProjectName]["server"]> =>
|
|
server !== null,
|
|
)
|
|
.map((server) => [server.port, server]),
|
|
).values(),
|
|
],
|
|
});
|