Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions e2e/e2e-utils/src/fixture.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
import { test as base, expect } from '@playwright/test'

export interface TestFixtureOptions {
/**
* List of error message patterns to ignore in console output.
* Supports both strings (substring match) and RegExp patterns.
*
* @example
* test.use({
* whitelistErrors: [
* 'Failed to load resource: net::ERR_NAME_NOT_RESOLVED',
* /Failed to load resource/,
* ],
* })
*/
whitelistErrors: Array<RegExp | string>
}
export const test = base.extend<TestFixtureOptions>({
whitelistErrors: [[], { option: true }],
whitelistErrors: [
// eslint-disable-next-line no-empty-pattern
async ({}, use) => {
await use([])
},
{ option: true },
],
page: async ({ page, whitelistErrors }, use) => {
const errorMessages: Array<string> = []
// Ensure whitelistErrors is always an array (defensive fallback)
const errors = Array.isArray(whitelistErrors) ? whitelistErrors : []
page.on('console', (m) => {
if (m.type() === 'error') {
const text = m.text()
for (const whitelistError of whitelistErrors) {
for (const whitelistError of errors) {
if (
(typeof whitelistError === 'string' &&
text.includes(whitelistError)) ||
Expand Down
2 changes: 1 addition & 1 deletion e2e/react-start/basic/tests/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { test } from '@tanstack/router-e2e-utils'

test.use({
whitelistErrors: [
/Failed to load resource: the server responded with a status of 404/,
'Failed to load resource: the server responded with a status of 404',
],
})
test('Navigating to post', async ({ page }) => {
Expand Down
2 changes: 1 addition & 1 deletion e2e/react-start/basic/tests/not-found.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const combinate = (combinateImport as any).default as typeof combinateImport

test.use({
whitelistErrors: [
/Failed to load resource: the server responded with a status of 404/,
'Failed to load resource: the server responded with a status of 404',
'NotFound error during hydration for routeId',
],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test } from '@tanstack/router-e2e-utils'

test.use({
whitelistErrors: [
/Failed to load resource: the server responded with a status of 404/,
'Failed to load resource: the server responded with a status of 404',
],
})

Expand Down
2 changes: 1 addition & 1 deletion e2e/react-start/basic/tests/params.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ test.beforeEach(async ({ page }) => {

test.use({
whitelistErrors: [
/Failed to load resource: the server responded with a status of 404/,
'Failed to load resource: the server responded with a status of 404',
],
})
test.describe('Unicode route rendering', () => {
Expand Down
25 changes: 20 additions & 5 deletions e2e/react-start/css-modules/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,44 @@
"scripts": {
"dev": "vite dev --port 3000",
"dev:e2e": "vite dev --port $PORT",
"dev:nitro": "VITE_USE_NITRO=true vite dev --port 3000",
"dev:e2e:nitro": "VITE_USE_NITRO=true vite dev --port $PORT",
"dev:basepath": "VITE_BASE_PATH=/my-app vite dev --port 3000",
"dev:e2e:basepath": "VITE_BASE_PATH=/my-app vite dev --port $PORT",
"dev:cloudflare": "echo 'Cloudflare dev mode has React duplication issues - use build+preview instead' && exit 1",
"dev:e2e:cloudflare": "echo 'Cloudflare dev mode has React duplication issues - use build+preview instead' && exit 1",
"build": "vite build && tsc --noEmit",
"preview": "vite preview",
"start": "pnpx srvx --prod -s ../client dist/server/server.js",
"test:e2e:dev": "MODE=dev playwright test --project=chromium",
"test:e2e:dev:nitro": "MODE=dev VITE_CONFIG=nitro playwright test --project=chromium",
"test:e2e:dev:basepath": "MODE=dev VITE_CONFIG=basepath playwright test --project=chromium",
"_test:e2e:dev:cloudflare": "MODE=dev VITE_CONFIG=cloudflare playwright test --project=chromium",
"test:e2e:prod": "playwright test --project=chromium",
"test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev"
"test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev",
"test:e2e:nitro": "rm -rf port*.txt; pnpm run test:e2e:dev:nitro",
"test:e2e:basepath": "rm -rf port*.txt; pnpm run test:e2e:dev:basepath",
"_test:e2e:cloudflare": "echo 'Cloudflare dev mode disabled - React duplication issues' && exit 0"
},
"dependencies": {
"@tanstack/react-router": "workspace:^",
"@tanstack/react-start": "workspace:^",
"@tanstack/react-router": "workspace:*",
"@tanstack/react-start": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.15.1",
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",
"@tanstack/router-e2e-utils": "workspace:*",
"@types/node": "^22.10.2",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"nitro": "npm:nitro-nightly@latest",
"srvx": "^0.10.0",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4"
"vite-tsconfig-paths": "^5.1.4",
"wrangler": "^4.22.0"
}
}
18 changes: 14 additions & 4 deletions e2e/react-start/css-modules/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ import { defineConfig, devices } from '@playwright/test'
import { getTestServerPort } from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }

const isDev = process.env.MODE === 'dev'
const PORT = await getTestServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`
const mode = process.env.MODE ?? 'prod'
const isDev = mode === 'dev'
const viteConfig = process.env.VITE_CONFIG // 'nitro' | 'basepath' | 'cloudflare' | undefined
const PORT = await getTestServerPort(
viteConfig ? `${packageJson.name}-${viteConfig}` : packageJson.name,
)

// When using basepath config, the app is served at /my-app
const basePath = viteConfig === 'basepath' ? '/my-app' : ''
const baseURL = `http://localhost:${PORT}${basePath}`

// Select the appropriate dev command based on VITE_CONFIG
const devCommand = viteConfig ? `pnpm dev:e2e:${viteConfig}` : 'pnpm dev:e2e'

Comment on lines +12 to 18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n e2e/react-start/css-modules/playwright.config.ts

Repository: TanStack/router

Length of output: 1735


🏁 Script executed:

find . -name "package.json" -path "*/e2e/react-start/css-modules/*" -exec cat {} \;

Repository: TanStack/router

Length of output: 2276


Ensure VITE_CONFIG environment variable is propagated to webServer.env for explicit contract clarity.

The baseURL and devCommand are both derived from process.env.VITE_CONFIG (lines 12, 16), creating an implicit contract that callers must set VITE_CONFIG consistently. However, VITE_CONFIG is not passed to webServer.env (lines 35–37), making this contract invisible to the spawned dev server. Add VITE_CONFIG to webServer.env to clarify the dependency and prevent accidental misconfiguration when running locally:

webServer.env block (lines 35–37)
    env: {
      VITE_NODE_ENV: 'test',
      PORT: String(PORT),
      VITE_CONFIG: viteConfig || '',
    },
🤖 Prompt for AI Agents
In @e2e/react-start/css-modules/playwright.config.ts around lines 11 - 17, The
test harness builds baseURL and devCommand from process.env.VITE_CONFIG (via the
local viteConfig variable) but does not propagate that variable into the
Playwright webServer.env, making the dev server unaware of the same config;
update the webServer.env block in playwright.config.ts (the webServer
configuration object) to include VITE_CONFIG with the same value you compute for
viteConfig (e.g. VITE_CONFIG: viteConfig || ''), so the spawned dev server
receives the same VITE_CONFIG used to construct baseURL and devCommand.

export default defineConfig({
testDir: './tests',
Expand All @@ -19,7 +29,7 @@ export default defineConfig({
},

webServer: {
command: isDev ? `pnpm dev:e2e` : `pnpm build && PORT=${PORT} pnpm start`,
command: isDev ? devCommand : `pnpm build && PORT=${PORT} pnpm start`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
Expand Down
58 changes: 44 additions & 14 deletions e2e/react-start/css-modules/tests/css.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { expect, request } from '@playwright/test'
import { test } from '@tanstack/router-e2e-utils'

// Whitelist errors that can occur in CI:
// - net::ERR_NAME_NOT_RESOLVED: transient network issues
// - 504 (Outdated Optimize Dep): Vite dependency optimization reload
const whitelistErrors = [
'Failed to load resource: net::ERR_NAME_NOT_RESOLVED',
'Failed to load resource: the server responded with a status of 504',
]

test.describe('CSS styles in SSR (dev mode)', () => {
test.use({ whitelistErrors })

// Warmup: trigger Vite's dependency optimization before running tests
// This prevents "504 (Outdated Optimize Dep)" errors during actual tests
test.beforeAll(async ({ baseURL }) => {
Expand All @@ -21,11 +31,21 @@ test.describe('CSS styles in SSR (dev mode)', () => {
}
})

// Helper to build full URL from baseURL and path
// Playwright's goto with absolute paths (like '/modules') ignores baseURL's path portion
// So we need to manually construct the full URL
const buildUrl = (baseURL: string, path: string) => {
return baseURL.replace(/\/$/, '') + path
}

test.describe('with JavaScript disabled', () => {
test.use({ javaScriptEnabled: false })
test.use({ javaScriptEnabled: false, whitelistErrors })

test('global CSS is applied on initial page load', async ({ page }) => {
await page.goto('/')
test('global CSS is applied on initial page load', async ({
page,
baseURL,
}) => {
await page.goto(buildUrl(baseURL!, '/'))

const element = page.getByTestId('global-styled')
await expect(element).toBeVisible()
Expand All @@ -48,8 +68,11 @@ test.describe('CSS styles in SSR (dev mode)', () => {
expect(borderRadius).toBe('12px')
})

test('CSS modules are applied on initial page load', async ({ page }) => {
await page.goto('/modules')
test('CSS modules are applied on initial page load', async ({
page,
baseURL,
}) => {
await page.goto(buildUrl(baseURL!, '/modules'))

const card = page.getByTestId('module-card')
await expect(card).toBeVisible()
Expand Down Expand Up @@ -77,8 +100,8 @@ test.describe('CSS styles in SSR (dev mode)', () => {
expect(borderRadius).toBe('8px')
})

test('global CSS class names are NOT scoped', async ({ page }) => {
await page.goto('/')
test('global CSS class names are NOT scoped', async ({ page, baseURL }) => {
await page.goto(buildUrl(baseURL!, '/'))

const element = page.getByTestId('global-styled')
await expect(element).toBeVisible()
Expand All @@ -89,8 +112,8 @@ test.describe('CSS styles in SSR (dev mode)', () => {
})
})

test('styles persist after hydration', async ({ page }) => {
await page.goto('/')
test('styles persist after hydration', async ({ page, baseURL }) => {
await page.goto(buildUrl(baseURL!, '/'))

// Wait for hydration
await page.waitForTimeout(1000)
Expand All @@ -102,8 +125,11 @@ test.describe('CSS styles in SSR (dev mode)', () => {
expect(backgroundColor).toBe('rgb(59, 130, 246)')
})

test('CSS modules styles persist after hydration', async ({ page }) => {
await page.goto('/modules')
test('CSS modules styles persist after hydration', async ({
page,
baseURL,
}) => {
await page.goto(buildUrl(baseURL!, '/modules'))

// Wait for hydration
await page.waitForTimeout(1000)
Expand All @@ -117,9 +143,10 @@ test.describe('CSS styles in SSR (dev mode)', () => {

test('styles work correctly after client-side navigation', async ({
page,
baseURL,
}) => {
// Start from home
await page.goto('/')
await page.goto(buildUrl(baseURL!, '/'))
await page.waitForTimeout(1000)

// Verify initial styles
Expand All @@ -132,7 +159,8 @@ test.describe('CSS styles in SSR (dev mode)', () => {

// Navigate to modules page
await page.getByTestId('nav-modules').click()
await page.waitForURL('/modules')
// Use glob pattern to match with or without basepath
await page.waitForURL('**/modules')

// Verify CSS modules styles
const card = page.getByTestId('module-card')
Expand All @@ -144,7 +172,9 @@ test.describe('CSS styles in SSR (dev mode)', () => {

// Navigate back to home
await page.getByTestId('nav-home').click()
await page.waitForURL('/')
// Match home URL with or without trailing slash and optional query string
// Matches: /, /?, /my-app, /my-app/, /my-app?foo=bar
await page.waitForURL(/\/([^/]*)(\/)?($|\?)/)

// Verify global styles still work
await expect(globalElement).toBeVisible()
Expand Down
16 changes: 16 additions & 0 deletions e2e/react-start/css-modules/vite.config.cloudflare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import tsConfigPaths from 'vite-tsconfig-paths'
import { cloudflare } from '@cloudflare/vite-plugin'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'

export default defineConfig({
plugins: [
tsConfigPaths({
projects: ['./tsconfig.json'],
}),
cloudflare({ viteEnvironment: { name: 'ssr' }, inspectorPort: false }),
tanstackStart(),
viteReact(),
],
})
35 changes: 24 additions & 11 deletions e2e/react-start/css-modules/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,28 @@ import tsConfigPaths from 'vite-tsconfig-paths'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'

export default defineConfig({
server: {
port: 3000,
},
plugins: [
tsConfigPaths({
projects: ['./tsconfig.json'],
}),
tanstackStart(),
viteReact(),
],
// Environment variables for different test configurations:
// - VITE_BASE_PATH: Set to '/my-app' for basepath testing
// - VITE_USE_NITRO: Set to 'true' to enable Nitro server
const basePath = process.env.VITE_BASE_PATH
const useNitro = process.env.VITE_USE_NITRO === 'true'

export default defineConfig(async () => {
// Dynamically import nitro only when needed to avoid loading it when not used
const nitroPlugin = useNitro ? [(await import('nitro/vite')).nitro()] : []

return {
base: basePath,
server: {
port: 3000,
},
plugins: [
tsConfigPaths({
projects: ['./tsconfig.json'],
}),
tanstackStart(),
viteReact(),
...nitroPlugin,
],
}
})
7 changes: 7 additions & 0 deletions e2e/react-start/css-modules/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "tanstack-start-css-modules",
"compatibility_date": "2025-09-24",
"compatibility_flags": ["nodejs_compat"],
"main": "@tanstack/react-start/server-entry",
}
2 changes: 1 addition & 1 deletion e2e/react-start/serialization-adapters/tests/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async function checkNestedData(page: Page) {
}
test.use({
whitelistErrors: [
/Failed to load resource: the server responded with a status of 499/,
'Failed to load resource: the server responded with a status of 499',
],
})
test.describe('SSR serialization adapters', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ test.describe('server function sets cookies', () => {
test.describe('server functions with async validation', () => {
test.use({
whitelistErrors: [
/Failed to load resource: the server responded with a status of 500/,
'Failed to load resource: the server responded with a status of 500',
],
})

Expand Down Expand Up @@ -739,7 +739,7 @@ test('redirect via server function with middleware does not cause serialization

test.describe('unhandled exception in middleware (issue #5266)', () => {
// Whitelist the expected 500 error since this test verifies error handling
test.use({ whitelistErrors: [/500/] })
test.use({ whitelistErrors: ['500'] })
Comment on lines 740 to +742

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Whitelist string '500' is too generic and may match unintended errors.

The string '500' could inadvertently match various unrelated console errors such as "Processed 500 items", "Port 5000", "Timeout after 5000ms", or "Error code: 15009". This overly permissive pattern might hide legitimate test failures.

Consider using a more specific pattern consistent with line 373.

🔍 Suggested fix
-  test.use({ whitelistErrors: ['500'] })
+  test.use({ whitelistErrors: ['Failed to load resource: the server responded with a status of 500'] })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test.describe('unhandled exception in middleware (issue #5266)', () => {
// Whitelist the expected 500 error since this test verifies error handling
test.use({ whitelistErrors: [/500/] })
test.use({ whitelistErrors: ['500'] })
test.describe('unhandled exception in middleware (issue #5266)', () => {
// Whitelist the expected 500 error since this test verifies error handling
test.use({ whitelistErrors: ['Failed to load resource: the server responded with a status of 500'] })
🤖 Prompt for AI Agents
In @e2e/react-start/server-functions/tests/server-functions.spec.ts around lines
740 - 742, Replace the overly-generic whitelist entry in the test.describe block
by changing test.use({ whitelistErrors: ['500'] }) to a specific, anchored
pattern that exactly matches the console error this test expects (e.g. the full
error string emitted by the unhandled middleware exception) or a strict regex
anchored with ^ and $; update the whitelistErrors array in the test.use call so
it uses that exact message or anchored regex instead of the bare '500'.


test('does not crash server and shows error component', async ({ page }) => {
// This test verifies that when a middleware throws an unhandled exception,
Expand Down
2 changes: 1 addition & 1 deletion e2e/solid-start/basic/tests/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test } from '@tanstack/router-e2e-utils'

test.use({
whitelistErrors: [
/Failed to load resource: the server responded with a status of 404/,
'Failed to load resource: the server responded with a status of 404',
],
})
test('Navigating to post', async ({ page }) => {
Expand Down
2 changes: 1 addition & 1 deletion e2e/solid-start/basic/tests/not-found.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const combinate = (combinateImport as any).default as typeof combinateImport

test.use({
whitelistErrors: [
/Failed to load resource: the server responded with a status of 404/,
'Failed to load resource: the server responded with a status of 404',
'NotFound error during hydration for routeId',
],
})
Expand Down
Loading
Loading