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
7 changes: 7 additions & 0 deletions e2e/react-start/css-modules/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
dist
.routeTree.gen.ts
src/routeTree.gen.ts
test-results
playwright-report
port*.txt
1 change: 1 addition & 0 deletions e2e/react-start/css-modules/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/routeTree.gen.ts
34 changes: 34 additions & 0 deletions e2e/react-start/css-modules/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "tanstack-react-start-e2e-css-modules",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"dev": "vite dev --port 3000",
"dev:e2e": "vite dev --port $PORT",
"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:prod": "playwright test --project=chromium",
"test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev"
},
"dependencies": {
"@tanstack/react-router": "workspace:^",
"@tanstack/react-start": "workspace:^",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@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",
"srvx": "^0.10.0",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4"
}
}
40 changes: 40 additions & 0 deletions e2e/react-start/css-modules/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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}`

export default defineConfig({
testDir: './tests',
workers: 1,
reporter: [['line']],

globalSetup: './tests/setup/global.setup.ts',
globalTeardown: './tests/setup/global.teardown.ts',

use: {
baseURL,
},

webServer: {
command: isDev ? `pnpm dev:e2e` : `pnpm build && PORT=${PORT} pnpm start`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
env: {
VITE_NODE_ENV: 'test',
PORT: String(PORT),
},
},

projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],
})
12 changes: 12 additions & 0 deletions e2e/react-start/css-modules/src/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function getRouter() {
const router = createRouter({
routeTree,
scrollRestoration: true,
defaultPreload: false,
})

return router
}
60 changes: 60 additions & 0 deletions e2e/react-start/css-modules/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
HeadContent,
Link,
Outlet,
Scripts,
createRootRoute,
} from '@tanstack/react-router'

export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
],
}),
component: RootComponent,
})

function RootComponent() {
return (
<html>
<head>
<HeadContent />
</head>
<body>
<nav
data-testid="main-nav"
style={{
padding: '12px 20px',
borderBottom: '1px solid #e2e8f0',
display: 'flex',
alignItems: 'center',
gap: '16px',
backgroundColor: '#fff',
}}
>
<Link
to="/"
style={{ color: '#0284c7', textDecoration: 'none' }}
data-testid="nav-home"
>
Home (Global CSS)
</Link>
<Link
to="/modules"
style={{ color: '#0284c7', textDecoration: 'none' }}
data-testid="nav-modules"
>
CSS Modules
</Link>
</nav>

<main style={{ padding: '20px' }}>
<Outlet />
</main>
<Scripts />
</body>
</html>
)
}
25 changes: 25 additions & 0 deletions e2e/react-start/css-modules/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createFileRoute } from '@tanstack/react-router'
import '~/styles/global.css'

export const Route = createFileRoute('/')({
component: Home,
})

function Home() {
return (
<div>
<h1>CSS Collection Test - Global CSS</h1>
<p>This page tests that global CSS is collected and served during SSR.</p>

<div className="global-container" data-testid="global-styled">
<div className="global-title" data-testid="global-title">
Global CSS Applied
</div>
<div className="global-description" data-testid="global-description">
This container should have a blue background, white text, and rounded
corners even with JavaScript disabled.
</div>
</div>
</div>
)
}
28 changes: 28 additions & 0 deletions e2e/react-start/css-modules/src/routes/modules.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/// <reference types="vite/client" />
import { createFileRoute } from '@tanstack/react-router'
import styles from '~/styles/card.module.css'

export const Route = createFileRoute('/modules')({
component: Modules,
})

function Modules() {
return (
<div>
<h1>CSS Collection Test - CSS Modules</h1>
<p>
This page tests that CSS modules are collected and served during SSR.
</p>

<div className={styles.card} data-testid="module-card">
<div className={styles.title} data-testid="module-title">
CSS Module Applied
</div>
<div className={styles.content} data-testid="module-content">
This card should have a green theme with scoped class names even with
JavaScript disabled.
</div>
</div>
</div>
)
}
20 changes: 20 additions & 0 deletions e2e/react-start/css-modules/src/styles/card.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* CSS Module for testing scoped styles in dev mode */

.card {
background-color: #f0fdf4; /* green-50 */
padding: 16px;
border-radius: 8px;
border: 1px solid #22c55e; /* green-500 */
}

.title {
font-size: 18px;
font-weight: 600;
color: #166534; /* green-800 */
margin-bottom: 8px;
}

.content {
font-size: 14px;
color: #15803d; /* green-700 */
}
19 changes: 19 additions & 0 deletions e2e/react-start/css-modules/src/styles/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* Global styles for testing CSS collection in dev mode */

.global-container {
background-color: #3b82f6; /* blue-500 */
padding: 24px;
border-radius: 12px;
color: white;
}

.global-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 16px;
}

.global-description {
font-size: 16px;
opacity: 0.9;
}
137 changes: 137 additions & 0 deletions e2e/react-start/css-modules/tests/css.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { expect } from '@playwright/test'
import { test } from '@tanstack/router-e2e-utils'

test.describe('CSS styles in SSR (dev mode)', () => {
test.describe('with JavaScript disabled', () => {
test.use({ javaScriptEnabled: false })

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

const element = page.getByTestId('global-styled')
await expect(element).toBeVisible()

// Verify the CSS is applied by checking computed styles
// #3b82f6 (blue-500) in RGB is rgb(59, 130, 246)
const backgroundColor = await element.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(59, 130, 246)')

const padding = await element.evaluate(
(el) => getComputedStyle(el).padding,
)
expect(padding).toBe('24px')

const borderRadius = await element.evaluate(
(el) => getComputedStyle(el).borderRadius,
)
expect(borderRadius).toBe('12px')
})

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

const card = page.getByTestId('module-card')
await expect(card).toBeVisible()

// Verify class is scoped (hashed)
const className = await card.getAttribute('class')
expect(className).toBeTruthy()
expect(className).not.toBe('card')
// The class should contain some hash characters (CSS modules add a hash)
expect(className!.length).toBeGreaterThan(5)

// Verify computed styles from card.module.css
// #f0fdf4 (green-50) in RGB is rgb(240, 253, 244)
const backgroundColor = await card.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(240, 253, 244)')

const padding = await card.evaluate((el) => getComputedStyle(el).padding)
expect(padding).toBe('16px')

const borderRadius = await card.evaluate(
(el) => getComputedStyle(el).borderRadius,
)
expect(borderRadius).toBe('8px')
})

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

const element = page.getByTestId('global-styled')
await expect(element).toBeVisible()

// Get the class attribute - it should be the plain class name (not hashed)
const className = await element.getAttribute('class')
expect(className).toBe('global-container')
})
})

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

// Wait for hydration
await page.waitForTimeout(1000)

const element = page.getByTestId('global-styled')
const backgroundColor = await element.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(59, 130, 246)')
})

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

// Wait for hydration
await page.waitForTimeout(1000)

const card = page.getByTestId('module-card')
const backgroundColor = await card.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(240, 253, 244)')
})

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

// Verify initial styles
const globalElement = page.getByTestId('global-styled')
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()
await page.waitForURL('/modules')

// Verify CSS modules styles
const card = page.getByTestId('module-card')
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()
await page.waitForURL('/')

// Verify global styles still work
await expect(globalElement).toBeVisible()
backgroundColor = await globalElement.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(59, 130, 246)')
})
})
Loading
Loading