Skip to content

Commit 538285c

Browse files
fix: dev style with non runnable dev environments and basepath support (#6348)
1 parent 7cd1f82 commit 538285c

File tree

36 files changed

+560
-246
lines changed

36 files changed

+560
-246
lines changed

e2e/e2e-utils/src/fixture.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,36 @@
11
import { test as base, expect } from '@playwright/test'
22

33
export interface TestFixtureOptions {
4+
/**
5+
* List of error message patterns to ignore in console output.
6+
* Supports both strings (substring match) and RegExp patterns.
7+
*
8+
* @example
9+
* test.use({
10+
* whitelistErrors: [
11+
* 'Failed to load resource: net::ERR_NAME_NOT_RESOLVED',
12+
* /Failed to load resource/,
13+
* ],
14+
* })
15+
*/
416
whitelistErrors: Array<RegExp | string>
517
}
618
export const test = base.extend<TestFixtureOptions>({
7-
whitelistErrors: [[], { option: true }],
19+
whitelistErrors: [
20+
// eslint-disable-next-line no-empty-pattern
21+
async ({}, use) => {
22+
await use([])
23+
},
24+
{ option: true },
25+
],
826
page: async ({ page, whitelistErrors }, use) => {
927
const errorMessages: Array<string> = []
28+
// Ensure whitelistErrors is always an array (defensive fallback)
29+
const errors = Array.isArray(whitelistErrors) ? whitelistErrors : []
1030
page.on('console', (m) => {
1131
if (m.type() === 'error') {
1232
const text = m.text()
13-
for (const whitelistError of whitelistErrors) {
33+
for (const whitelistError of errors) {
1434
if (
1535
(typeof whitelistError === 'string' &&
1636
text.includes(whitelistError)) ||

e2e/react-start/basic/tests/navigation.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { test } from '@tanstack/router-e2e-utils'
44

55
test.use({
66
whitelistErrors: [
7-
/Failed to load resource: the server responded with a status of 404/,
7+
'Failed to load resource: the server responded with a status of 404',
88
],
99
})
1010
test('Navigating to post', async ({ page }) => {

e2e/react-start/basic/tests/not-found.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const combinate = (combinateImport as any).default as typeof combinateImport
88

99
test.use({
1010
whitelistErrors: [
11-
/Failed to load resource: the server responded with a status of 404/,
11+
'Failed to load resource: the server responded with a status of 404',
1212
'NotFound error during hydration for routeId',
1313
],
1414
})

e2e/react-start/basic/tests/open-redirect-prevention.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { test } from '@tanstack/router-e2e-utils'
33

44
test.use({
55
whitelistErrors: [
6-
/Failed to load resource: the server responded with a status of 404/,
6+
'Failed to load resource: the server responded with a status of 404',
77
],
88
})
99

e2e/react-start/basic/tests/params.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ test.beforeEach(async ({ page }) => {
88

99
test.use({
1010
whitelistErrors: [
11-
/Failed to load resource: the server responded with a status of 404/,
11+
'Failed to load resource: the server responded with a status of 404',
1212
],
1313
})
1414
test.describe('Unicode route rendering', () => {

e2e/react-start/css-modules/package.json

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,44 @@
66
"scripts": {
77
"dev": "vite dev --port 3000",
88
"dev:e2e": "vite dev --port $PORT",
9+
"dev:nitro": "VITE_USE_NITRO=true vite dev --port 3000",
10+
"dev:e2e:nitro": "VITE_USE_NITRO=true vite dev --port $PORT",
11+
"dev:basepath": "VITE_BASE_PATH=/my-app vite dev --port 3000",
12+
"dev:e2e:basepath": "VITE_BASE_PATH=/my-app vite dev --port $PORT",
13+
"dev:cloudflare": "echo 'Cloudflare dev mode has React duplication issues - use build+preview instead' && exit 1",
14+
"dev:e2e:cloudflare": "echo 'Cloudflare dev mode has React duplication issues - use build+preview instead' && exit 1",
915
"build": "vite build && tsc --noEmit",
1016
"preview": "vite preview",
1117
"start": "pnpx srvx --prod -s ../client dist/server/server.js",
1218
"test:e2e:dev": "MODE=dev playwright test --project=chromium",
19+
"test:e2e:dev:nitro": "MODE=dev VITE_CONFIG=nitro playwright test --project=chromium",
20+
"test:e2e:dev:basepath": "MODE=dev VITE_CONFIG=basepath playwright test --project=chromium",
21+
"_test:e2e:dev:cloudflare": "MODE=dev VITE_CONFIG=cloudflare playwright test --project=chromium",
1322
"test:e2e:prod": "playwright test --project=chromium",
14-
"test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev"
23+
"test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev",
24+
"test:e2e:nitro": "rm -rf port*.txt; pnpm run test:e2e:dev:nitro",
25+
"test:e2e:basepath": "rm -rf port*.txt; pnpm run test:e2e:dev:basepath",
26+
"_test:e2e:cloudflare": "echo 'Cloudflare dev mode disabled - React duplication issues' && exit 0"
1527
},
1628
"dependencies": {
17-
"@tanstack/react-router": "workspace:^",
18-
"@tanstack/react-start": "workspace:^",
29+
"@tanstack/react-router": "workspace:*",
30+
"@tanstack/react-start": "workspace:*",
1931
"react": "^19.0.0",
2032
"react-dom": "^19.0.0"
2133
},
2234
"devDependencies": {
35+
"@cloudflare/vite-plugin": "^1.15.1",
2336
"@playwright/test": "^1.50.1",
24-
"@tanstack/router-e2e-utils": "workspace:^",
37+
"@tanstack/router-e2e-utils": "workspace:*",
2538
"@types/node": "^22.10.2",
2639
"@types/react": "^19.0.8",
2740
"@types/react-dom": "^19.0.3",
2841
"@vitejs/plugin-react": "^4.3.4",
42+
"nitro": "npm:nitro-nightly@latest",
2943
"srvx": "^0.10.0",
3044
"typescript": "^5.7.2",
3145
"vite": "^7.1.7",
32-
"vite-tsconfig-paths": "^5.1.4"
46+
"vite-tsconfig-paths": "^5.1.4",
47+
"wrangler": "^4.22.0"
3348
}
3449
}

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,19 @@ import { defineConfig, devices } from '@playwright/test'
22
import { getTestServerPort } from '@tanstack/router-e2e-utils'
33
import packageJson from './package.json' with { type: 'json' }
44

5-
const isDev = process.env.MODE === 'dev'
6-
const PORT = await getTestServerPort(packageJson.name)
7-
const baseURL = `http://localhost:${PORT}`
5+
const mode = process.env.MODE ?? 'prod'
6+
const isDev = mode === 'dev'
7+
const viteConfig = process.env.VITE_CONFIG // 'nitro' | 'basepath' | 'cloudflare' | undefined
8+
const PORT = await getTestServerPort(
9+
viteConfig ? `${packageJson.name}-${viteConfig}` : packageJson.name,
10+
)
11+
12+
// When using basepath config, the app is served at /my-app
13+
const basePath = viteConfig === 'basepath' ? '/my-app' : ''
14+
const baseURL = `http://localhost:${PORT}${basePath}`
15+
16+
// Select the appropriate dev command based on VITE_CONFIG
17+
const devCommand = viteConfig ? `pnpm dev:e2e:${viteConfig}` : 'pnpm dev:e2e'
818

919
export default defineConfig({
1020
testDir: './tests',
@@ -19,7 +29,7 @@ export default defineConfig({
1929
},
2030

2131
webServer: {
22-
command: isDev ? `pnpm dev:e2e` : `pnpm build && PORT=${PORT} pnpm start`,
32+
command: isDev ? devCommand : `pnpm build && PORT=${PORT} pnpm start`,
2333
url: baseURL,
2434
reuseExistingServer: !process.env.CI,
2535
stdout: 'pipe',

e2e/react-start/css-modules/tests/css.spec.ts

Lines changed: 79 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,54 @@
1-
import { expect, request } from '@playwright/test'
1+
import { expect } from '@playwright/test'
22
import { test } from '@tanstack/router-e2e-utils'
33

4+
// Whitelist errors that can occur in CI:
5+
// - net::ERR_NAME_NOT_RESOLVED: transient network issues
6+
// - 504 (Outdated Optimize Dep): Vite dependency optimization reload
7+
const whitelistErrors = [
8+
'Failed to load resource: net::ERR_NAME_NOT_RESOLVED',
9+
'Failed to load resource: the server responded with a status of 504',
10+
]
11+
412
test.describe('CSS styles in SSR (dev mode)', () => {
13+
test.use({ whitelistErrors })
14+
515
// Warmup: trigger Vite's dependency optimization before running tests
6-
// This prevents "504 (Outdated Optimize Dep)" errors during actual tests
7-
test.beforeAll(async ({ baseURL }) => {
8-
const context = await request.newContext()
16+
// This prevents "optimized dependencies changed. reloading" during actual tests
17+
// We use a real browser context since dep optimization happens on JS load, not HTTP requests
18+
test.beforeAll(async ({ browser, baseURL }) => {
19+
const context = await browser.newContext()
20+
const page = await context.newPage()
921
try {
10-
// Hit both pages to trigger any dependency optimization
11-
await context.get(baseURL!)
12-
await context.get(`${baseURL}/modules`)
13-
// Give Vite time to complete optimization
14-
await new Promise((resolve) => setTimeout(resolve, 1000))
15-
// Hit again after optimization
16-
await context.get(baseURL!)
22+
// Load both pages to trigger dependency optimization
23+
await page.goto(baseURL!)
24+
await page.waitForTimeout(2000) // Wait for deps to optimize
25+
await page.goto(`${baseURL}/modules`)
26+
await page.waitForTimeout(2000)
27+
// Load again after optimization completes
28+
await page.goto(baseURL!)
29+
await page.waitForTimeout(1000)
1730
} catch {
1831
// Ignore errors during warmup
1932
} finally {
20-
await context.dispose()
33+
await context.close()
2134
}
2235
})
2336

37+
// Helper to build full URL from baseURL and path
38+
// Playwright's goto with absolute paths (like '/modules') ignores baseURL's path portion
39+
// So we need to manually construct the full URL
40+
const buildUrl = (baseURL: string, path: string) => {
41+
return baseURL.replace(/\/$/, '') + path
42+
}
43+
2444
test.describe('with JavaScript disabled', () => {
25-
test.use({ javaScriptEnabled: false })
45+
test.use({ javaScriptEnabled: false, whitelistErrors })
2646

27-
test('global CSS is applied on initial page load', async ({ page }) => {
28-
await page.goto('/')
47+
test('global CSS is applied on initial page load', async ({
48+
page,
49+
baseURL,
50+
}) => {
51+
await page.goto(buildUrl(baseURL!, '/'))
2952

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

51-
test('CSS modules are applied on initial page load', async ({ page }) => {
52-
await page.goto('/modules')
74+
test('CSS modules are applied on initial page load', async ({
75+
page,
76+
baseURL,
77+
}) => {
78+
await page.goto(buildUrl(baseURL!, '/modules'))
5379

5480
const card = page.getByTestId('module-card')
5581
await expect(card).toBeVisible()
@@ -77,8 +103,8 @@ test.describe('CSS styles in SSR (dev mode)', () => {
77103
expect(borderRadius).toBe('8px')
78104
})
79105

80-
test('global CSS class names are NOT scoped', async ({ page }) => {
81-
await page.goto('/')
106+
test('global CSS class names are NOT scoped', async ({ page, baseURL }) => {
107+
await page.goto(buildUrl(baseURL!, '/'))
82108

83109
const element = page.getByTestId('global-styled')
84110
await expect(element).toBeVisible()
@@ -89,37 +115,47 @@ test.describe('CSS styles in SSR (dev mode)', () => {
89115
})
90116
})
91117

92-
test('styles persist after hydration', async ({ page }) => {
93-
await page.goto('/')
94-
95-
// Wait for hydration
96-
await page.waitForTimeout(1000)
118+
test('styles persist after hydration', async ({ page, baseURL }) => {
119+
await page.goto(buildUrl(baseURL!, '/'))
97120

121+
// Wait for hydration and styles to be applied
98122
const element = page.getByTestId('global-styled')
99-
const backgroundColor = await element.evaluate(
100-
(el) => getComputedStyle(el).backgroundColor,
101-
)
102-
expect(backgroundColor).toBe('rgb(59, 130, 246)')
103-
})
123+
await expect(element).toBeVisible()
104124

105-
test('CSS modules styles persist after hydration', async ({ page }) => {
106-
await page.goto('/modules')
125+
// Wait for CSS to be applied (background color should not be transparent)
126+
await expect(async () => {
127+
const backgroundColor = await element.evaluate(
128+
(el) => getComputedStyle(el).backgroundColor,
129+
)
130+
expect(backgroundColor).toBe('rgb(59, 130, 246)')
131+
}).toPass({ timeout: 5000 })
132+
})
107133

108-
// Wait for hydration
109-
await page.waitForTimeout(1000)
134+
test('CSS modules styles persist after hydration', async ({
135+
page,
136+
baseURL,
137+
}) => {
138+
await page.goto(buildUrl(baseURL!, '/modules'))
110139

140+
// Wait for hydration and styles to be applied
111141
const card = page.getByTestId('module-card')
112-
const backgroundColor = await card.evaluate(
113-
(el) => getComputedStyle(el).backgroundColor,
114-
)
115-
expect(backgroundColor).toBe('rgb(240, 253, 244)')
142+
await expect(card).toBeVisible()
143+
144+
// Wait for CSS to be applied (background color should not be transparent)
145+
await expect(async () => {
146+
const backgroundColor = await card.evaluate(
147+
(el) => getComputedStyle(el).backgroundColor,
148+
)
149+
expect(backgroundColor).toBe('rgb(240, 253, 244)')
150+
}).toPass({ timeout: 5000 })
116151
})
117152

118153
test('styles work correctly after client-side navigation', async ({
119154
page,
155+
baseURL,
120156
}) => {
121157
// Start from home
122-
await page.goto('/')
158+
await page.goto(buildUrl(baseURL!, '/'))
123159
await page.waitForTimeout(1000)
124160

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

133169
// Navigate to modules page
134170
await page.getByTestId('nav-modules').click()
135-
await page.waitForURL('/modules')
171+
// Use glob pattern to match with or without basepath
172+
await page.waitForURL('**/modules')
136173

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

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

149188
// Verify global styles still work
150189
await expect(globalElement).toBeVisible()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { defineConfig } from 'vite'
2+
import tsConfigPaths from 'vite-tsconfig-paths'
3+
import { cloudflare } from '@cloudflare/vite-plugin'
4+
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
5+
import viteReact from '@vitejs/plugin-react'
6+
7+
export default defineConfig({
8+
plugins: [
9+
tsConfigPaths({
10+
projects: ['./tsconfig.json'],
11+
}),
12+
cloudflare({ viteEnvironment: { name: 'ssr' }, inspectorPort: false }),
13+
tanstackStart(),
14+
viteReact(),
15+
],
16+
})

e2e/react-start/css-modules/vite.config.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,28 @@ import tsConfigPaths from 'vite-tsconfig-paths'
33
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
44
import viteReact from '@vitejs/plugin-react'
55

6-
export default defineConfig({
7-
server: {
8-
port: 3000,
9-
},
10-
plugins: [
11-
tsConfigPaths({
12-
projects: ['./tsconfig.json'],
13-
}),
14-
tanstackStart(),
15-
viteReact(),
16-
],
6+
// Environment variables for different test configurations:
7+
// - VITE_BASE_PATH: Set to '/my-app' for basepath testing
8+
// - VITE_USE_NITRO: Set to 'true' to enable Nitro server
9+
const basePath = process.env.VITE_BASE_PATH
10+
const useNitro = process.env.VITE_USE_NITRO === 'true'
11+
12+
export default defineConfig(async () => {
13+
// Dynamically import nitro only when needed to avoid loading it when not used
14+
const nitroPlugin = useNitro ? [(await import('nitro/vite')).nitro()] : []
15+
16+
return {
17+
base: basePath,
18+
server: {
19+
port: 3000,
20+
},
21+
plugins: [
22+
tsConfigPaths({
23+
projects: ['./tsconfig.json'],
24+
}),
25+
tanstackStart(),
26+
viteReact(),
27+
...nitroPlugin,
28+
],
29+
}
1730
})

0 commit comments

Comments
 (0)