Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
67 changes: 18 additions & 49 deletions e2e/react-start/css-modules/tests/css.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,6 @@ const whitelistErrors = [
test.describe('CSS styles in SSR (dev mode)', () => {
test.use({ whitelistErrors })

// Warmup: trigger Vite's dependency optimization before running tests
// This prevents "optimized dependencies changed. reloading" during actual tests
// We use a real browser context since dep optimization happens on JS load, not HTTP requests
test.beforeAll(async ({ browser, baseURL }) => {
const context = await browser.newContext()
const page = await context.newPage()
try {
// Load both pages to trigger dependency optimization
await page.goto(baseURL!)
await page.waitForTimeout(2000) // Wait for deps to optimize
await page.goto(`${baseURL}/modules`)
await page.waitForTimeout(2000)
// Load again after optimization completes
await page.goto(baseURL!)
await page.waitForTimeout(1000)
} catch {
// Ignore errors during warmup
} finally {
await context.close()
}
})

// 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
Expand Down Expand Up @@ -223,44 +201,35 @@ test.describe('CSS styles in SSR (dev mode)', () => {
// Start from home
await page.goto(buildUrl(baseURL!, '/'))

// Verify initial styles with retry to handle potential Vite dep optimization reload
// Verify initial styles
const globalElement = page.getByTestId('global-styled')
await expect(async () => {
await expect(globalElement).toBeVisible()
const backgroundColor = await globalElement.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(59, 130, 246)')
}).toPass({ timeout: 10000 })
await expect(globalElement).toBeVisible()
let backgroundColor = await globalElement.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(59, 130, 246)')

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

// Verify CSS modules styles with retry to handle potential Vite dep optimization reload
// Verify CSS modules styles
const card = page.getByTestId('module-card')
await expect(async () => {
await expect(card).toBeVisible()
const backgroundColor = await card.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(240, 253, 244)')
}).toPass({ timeout: 10000 })
await expect(card).toBeVisible()
backgroundColor = await card.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(240, 253, 244)')

// Navigate back to home
await page.getByTestId('nav-home').click()
// 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 with retry
await expect(async () => {
await expect(globalElement).toBeVisible()
const backgroundColor = await globalElement.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(59, 130, 246)')
}).toPass({ timeout: 10000 })
// Verify global styles still work
await expect(globalElement).toBeVisible()
backgroundColor = await globalElement.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(59, 130, 246)')
})
})
96 changes: 95 additions & 1 deletion e2e/react-start/css-modules/tests/setup/global.setup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,100 @@
import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
import { chromium } from '@playwright/test'
import {
e2eStartDummyServer,
getTestServerPort,
} from '@tanstack/router-e2e-utils'
import packageJson from '../../package.json' with { type: 'json' }

async function waitForServer(url: string) {
const start = Date.now()
while (Date.now() - start < 30_000) {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 5_000)
try {
const res = await fetch(url, {
redirect: 'manual',
signal: controller.signal,
})
if (res.status >= 200 && res.status < 400) return
} catch {
// ignore aborted/network errors
} finally {
clearTimeout(timer)
}
await new Promise((r) => setTimeout(r, 250))
}
throw new Error(`Timed out waiting for dev server at ${url}`)
}

async function preOptimizeDevServer(baseURL: string) {
const browser = await chromium.launch()
const context = await browser.newContext()
const page = await context.newPage()

try {
await page.goto(`${baseURL}/`, { waitUntil: 'domcontentloaded' })
await page.getByTestId('global-styled').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

await page.goto(`${baseURL}/modules`, { waitUntil: 'domcontentloaded' })
await page.getByTestId('module-card').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

await page.goto(`${baseURL}/sass-mixin`, { waitUntil: 'domcontentloaded' })
await page.getByTestId('mixin-styled').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

await page.goto(`${baseURL}/quotes`, { waitUntil: 'domcontentloaded' })
await page.getByTestId('quote-styled').waitFor({ state: 'visible' })
await page.getByTestId('after-quote-styled').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

// Exercise client-side navigation so Vite discovers any remaining deps
// that only load via the client router (not full-page navigations).
await page.goto(`${baseURL}/`, { waitUntil: 'domcontentloaded' })
await page.getByTestId('global-styled').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

await page.getByTestId('nav-modules').click()
await page.waitForURL('**/modules')
await page.getByTestId('module-card').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

await page.getByTestId('nav-home').click()
await page.waitForURL(/\/([^/]*)(\/)?($|\?)/)
await page.getByTestId('global-styled').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

// Ensure we end in a stable state. Vite's optimize step triggers a reload;
// this waits until no further navigations happen for a short window.
for (let i = 0; i < 40; i++) {
const currentUrl = page.url()
await page.waitForTimeout(250)
if (page.url() === currentUrl) {
await page.waitForTimeout(250)
if (page.url() === currentUrl) return
}
}

throw new Error('Dev server did not reach a stable URL after warmup')
} finally {
await context.close()
await browser.close()
}
}

export default async function setup() {
await e2eStartDummyServer(packageJson.name)

if (process.env.MODE !== 'dev') return

const viteConfig = process.env.VITE_CONFIG // 'nitro' | 'basepath' | 'cloudflare' | undefined
const port = await getTestServerPort(
viteConfig ? `${packageJson.name}-${viteConfig}` : packageJson.name,
)
const basePath = viteConfig === 'basepath' ? '/my-app' : ''
const baseURL = `http://localhost:${port}${basePath}`

await waitForServer(baseURL)
await preOptimizeDevServer(baseURL)
}
67 changes: 18 additions & 49 deletions e2e/solid-start/css-modules/tests/css.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,6 @@ const whitelistErrors = [
test.describe('CSS styles in SSR (dev mode)', () => {
test.use({ whitelistErrors })

// Warmup: trigger Vite's dependency optimization before running tests
// This prevents "optimized dependencies changed. reloading" during actual tests
// We use a real browser context since dep optimization happens on JS load, not HTTP requests
test.beforeAll(async ({ browser, baseURL }) => {
const context = await browser.newContext()
const page = await context.newPage()
try {
// Load both pages to trigger dependency optimization
await page.goto(baseURL!)
await page.waitForTimeout(2000) // Wait for deps to optimize
await page.goto(`${baseURL}/modules`)
await page.waitForTimeout(2000)
// Load again after optimization completes
await page.goto(baseURL!)
await page.waitForTimeout(1000)
} catch {
// Ignore errors during warmup
} finally {
await context.close()
}
})

// 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
Expand Down Expand Up @@ -195,44 +173,35 @@ test.describe('CSS styles in SSR (dev mode)', () => {
// Start from home
await page.goto(buildUrl(baseURL!, '/'))

// Verify initial styles with retry to handle potential Vite dep optimization reload
// Verify initial styles
const globalElement = page.getByTestId('global-styled')
await expect(async () => {
await expect(globalElement).toBeVisible()
const backgroundColor = await globalElement.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(59, 130, 246)')
}).toPass({ timeout: 10000 })
await expect(globalElement).toBeVisible()
let backgroundColor = await globalElement.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(59, 130, 246)')

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

// Verify CSS modules styles with retry to handle potential Vite dep optimization reload
// Verify CSS modules styles
const card = page.getByTestId('module-card')
await expect(async () => {
await expect(card).toBeVisible()
const backgroundColor = await card.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(240, 253, 244)')
}).toPass({ timeout: 10000 })
await expect(card).toBeVisible()
backgroundColor = await card.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(240, 253, 244)')

// Navigate back to home
await page.getByTestId('nav-home').click()
// 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 with retry
await expect(async () => {
await expect(globalElement).toBeVisible()
const backgroundColor = await globalElement.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(59, 130, 246)')
}).toPass({ timeout: 10000 })
// Verify global styles still work
await expect(globalElement).toBeVisible()
backgroundColor = await globalElement.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(59, 130, 246)')
})
})
87 changes: 86 additions & 1 deletion e2e/solid-start/css-modules/tests/setup/global.setup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,91 @@
import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
import { chromium } from '@playwright/test'
import {
e2eStartDummyServer,
getTestServerPort,
} from '@tanstack/router-e2e-utils'
import packageJson from '../../package.json' with { type: 'json' }

async function waitForServer(url: string) {
const start = Date.now()
while (Date.now() - start < 30_000) {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 5_000)
try {
const res = await fetch(url, {
redirect: 'manual',
signal: controller.signal,
})
if (res.ok) return
} catch {
// ignore aborted/network errors
} finally {
clearTimeout(timer)
}
await new Promise((r) => setTimeout(r, 250))
}
throw new Error(`Timed out waiting for dev server at ${url}`)
Comment on lines 8 to 26
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n e2e/solid-start/css-modules/tests/setup/global.setup.ts | sed -n '1,30p'

Repository: TanStack/router

Length of output: 1211


🌐 Web query:

MDN fetch API res.ok status codes 3xx redirects

💡 Result:

  • Response.ok is only true for HTTP 200–299; 3xx (redirect) status codes are not “ok” (so ok === false for a 3xx response). [1]
  • In browsers, fetch() uses redirect: "follow" by default, so if the server returns a 3xx, fetch() typically follows the redirect and resolves with the final response (often 200), where response.ok may be true. You can detect that a redirect happened with response.redirected === true (and response.url will be the final URL). [2] [3]
  • You can control redirect handling with the request’s redirect mode: "follow" (default), "error", or "manual". Setting redirect: "error" is the recommended way to disallow redirects. [3] [4]

Sources:
[1] MDN: Response.ok — status in the range 200–299 https://developer.mozilla.org/en-US/docs/Web/API/Response/ok
[2] MDN: Response.redirected (detect redirects, final response.url) https://developer.mozilla.org/en-US/docs/Web/API/Response/redirected
[3] MDN: Request.redirect (follow/error/manual; default follow) https://developer.mozilla.org/en-US/docs/Web/API/Request/redirect
[4] MDN: fetch() resolves on HTTP error statuses; check ok/status https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch


Add per-attempt timeout to prevent hanging fetch calls in waitForServer.

With redirect: 'manual' in effect, a 3xx response is returned as-is and will fail the res.ok check, simply retrying rather than hanging. However, the fetch call itself can hang indefinitely if the server is unresponsive, stalling the loop. Add an abort timeout (e.g., 2–3 seconds) per fetch attempt to ensure robustness.

🛠️ Suggested fix
 async function waitForServer(url: string) {
   const start = Date.now()
   while (Date.now() - start < 30_000) {
+    const controller = new AbortController()
+    const timeout = setTimeout(() => controller.abort(), 2_000)
     try {
-      const res = await fetch(url, { redirect: 'manual' })
+      const res = await fetch(url, {
+        redirect: 'manual',
+        signal: controller.signal,
+      })
       if (res.ok) return
     } catch {
       // ignore
+    } finally {
+      clearTimeout(timeout)
     }
     await new Promise((r) => setTimeout(r, 250))
   }
   throw new Error(`Timed out waiting for dev server at ${url}`)
 }
📝 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
async function waitForServer(url: string) {
const start = Date.now()
while (Date.now() - start < 30_000) {
try {
const res = await fetch(url, { redirect: 'manual' })
if (res.ok) return
} catch {
// ignore
}
await new Promise((r) => setTimeout(r, 250))
}
throw new Error(`Timed out waiting for dev server at ${url}`)
async function waitForServer(url: string) {
const start = Date.now()
while (Date.now() - start < 30_000) {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 2_000)
try {
const res = await fetch(url, {
redirect: 'manual',
signal: controller.signal,
})
if (res.ok) return
} catch {
// ignore
} finally {
clearTimeout(timeout)
}
await new Promise((r) => setTimeout(r, 250))
}
throw new Error(`Timed out waiting for dev server at ${url}`)
}
🤖 Prompt for AI Agents
In `@e2e/solid-start/css-modules/tests/setup/global.setup.ts` around lines 8 - 19,
The waitForServer function can hang because fetch calls may never resolve;
modify waitForServer to create an AbortController for each iteration, pass
controller.signal into fetch(url, { redirect: 'manual', signal }), and start a
per-attempt timer (e.g., 2000–3000ms) that calls controller.abort() when
expired; ensure you clear the timer after fetch completes and keep the existing
try/catch so aborted or network errors are ignored and the loop retries, then
preserve the final timeout error behavior.

}

async function preOptimizeDevServer(baseURL: string) {
const browser = await chromium.launch()
const context = await browser.newContext()
const page = await context.newPage()

try {
await page.goto(`${baseURL}/`, { waitUntil: 'domcontentloaded' })
await page.getByTestId('global-styled').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

await page.goto(`${baseURL}/modules`, { waitUntil: 'domcontentloaded' })
await page.getByTestId('module-card').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

await page.goto(`${baseURL}/sass-mixin`, { waitUntil: 'domcontentloaded' })
await page.getByTestId('mixin-styled').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

// Exercise client-side navigation so Vite discovers any remaining deps
// that only load via the client router (not full-page navigations).
await page.goto(`${baseURL}/`, { waitUntil: 'domcontentloaded' })
await page.getByTestId('global-styled').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

await page.getByTestId('nav-modules').click()
await page.waitForURL('**/modules')
await page.getByTestId('module-card').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

await page.getByTestId('nav-home').click()
await page.waitForURL(/\/([^/]*)(\/)?($|\?)/)
await page.getByTestId('global-styled').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

// Ensure we end in a stable state. Vite's optimize step triggers a reload;
// this waits until no further navigations happen for a short window.
for (let i = 0; i < 40; i++) {
const currentUrl = page.url()
await page.waitForTimeout(250)
if (page.url() === currentUrl) {
await page.waitForTimeout(250)
if (page.url() === currentUrl) return
}
}

throw new Error('Dev server did not reach a stable URL after warmup')
} finally {
await context.close()
await browser.close()
}
}

export default async function setup() {
await e2eStartDummyServer(packageJson.name)

if (process.env.MODE !== 'dev') return

const port = await getTestServerPort(packageJson.name)
const baseURL = `http://localhost:${port}`

await waitForServer(baseURL)
await preOptimizeDevServer(baseURL)
}
Loading
Loading