tailwindcss/integrations/vite/react-router.test.ts
Robin Malfait faa5e8849b
Cleanup inconsistencies related to (regex) escapes (#19804)
This PR does some generic cleanup to the codebase.

I'm playing with oxfmt and oxlint and noticed some unnecessary escapes.
Might add these dependencies to the project later (and rolldown for
building). But baby steps for now.

## Test plan

1. All tests should still pass
2026-03-15 22:04:58 +01:00

266 lines
7.6 KiB
TypeScript

import { candidate, css, fetchStyles, json, retryAssertion, test, ts, txt } from '../utils'
const WORKSPACE = {
'package.json': json`
{
"type": "module",
"dependencies": {
"@react-router/dev": "^7",
"@react-router/node": "^7",
"@react-router/serve": "^7",
"@tailwindcss/vite": "workspace:^",
"@types/node": "^20",
"@types/react-dom": "^19",
"@types/react": "^19",
"isbot": "^5",
"react-dom": "^19",
"react-router": "^7",
"react": "^19",
"tailwindcss": "workspace:^",
"vite": "^5"
}
}
`,
'react-router.config.ts': ts`
import type { Config } from '@react-router/dev/config'
export default { ssr: true } satisfies Config
`,
'vite.config.ts': ts`
import { defineConfig } from 'vite'
import { reactRouter } from '@react-router/dev/vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss(), reactRouter()],
})
`,
'app/routes/home.tsx': ts`
export default function Home() {
return <h1 className="font-bold">Welcome to React Router</h1>
}
`,
'app/app.css': css`@import 'tailwindcss';`,
'app/routes.ts': ts`
import { type RouteConfig, index } from '@react-router/dev/routes'
export default [index('routes/home.tsx')] satisfies RouteConfig
`,
'app/root.tsx': ts`
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'
import './app.css'
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
)
}
export default function App() {
return <Outlet />
}
`,
}
test('dev mode', { fs: WORKSPACE }, async ({ fs, spawn, expect }) => {
let process = await spawn('pnpm react-router dev')
let url = ''
await process.onStdout((m) => {
let match = /Local:\s*(http.*)\//.exec(m)
if (match) url = match[1]
return Boolean(url)
})
await retryAssertion(async () => {
let css = await fetchStyles(url)
expect(css).toContain(candidate`font-bold`)
})
await retryAssertion(async () => {
await fs.write(
'app/routes/home.tsx',
ts`
export default function Home() {
return <h1 className="font-bold underline">Welcome to React Router</h1>
}
`,
)
let css = await fetchStyles(url)
expect(css).toContain(candidate`underline`)
expect(css).toContain(candidate`font-bold`)
})
})
test(
// cf. https://github.com/remix-run/react-router/blob/00cb4d7b310663b2e84152700c05d3b503005e83/integration/vite-hmr-hdr-test.ts#L311-L318
'dev mode, editing a server-only loader dependency triggers HDR instead of a full reload',
{
fs: {
...WORKSPACE,
'package.json': json`
{
"type": "module",
"dependencies": {
"@react-router/dev": "^7",
"@react-router/node": "^7",
"@react-router/serve": "^7",
"@tailwindcss/vite": "workspace:^",
"@types/node": "^20",
"@types/react-dom": "^19",
"@types/react": "^19",
"isbot": "^5",
"react-dom": "^19",
"react-router": "^7",
"react": "^19",
"tailwindcss": "workspace:^",
"vite": "^7"
}
}
`,
'app/routes/home.tsx': ts`
import type { Route } from './+types/home'
import { direct } from '../direct-hdr-dep'
export async function loader() {
return { message: direct }
}
export default function Home({ loaderData }: Route.ComponentProps) {
return (
<div>
<h1 className="font-bold">{loaderData.message}</h1>
<input data-testinput />
</div>
)
}
`,
'app/direct-hdr-dep.ts': ts` export const direct = 'HDR: 0' `,
},
},
async ({ fs, spawn, expect }) => {
let process = await spawn('pnpm react-router dev')
let url = ''
await process.onStdout((m) => {
let match = /Local:\s*(http.*)\//.exec(m)
if (match) url = match[1]
return Boolean(url)
})
// check initial state
await retryAssertion(async () => {
let html = await (await fetch(url)).text()
expect(html).toContain('HDR: 0')
let css = await fetchStyles(url)
expect(css).toContain(candidate`font-bold`)
})
// Flush stdout so we only see messages triggered by the edit below.
process.flush()
// Edit the server-only module. The client environment watches this file
// but it only exists in the server module graph. Without the fix, the
// Tailwind CSS plugin would trigger a full page reload on the client
// instead of letting react-router handle HDR.
await fs.write('app/direct-hdr-dep.ts', ts` export const direct = 'HDR: 1' `)
// check update
await retryAssertion(async () => {
let html = await (await fetch(url)).text()
expect(html).toContain('HDR: 1')
let css = await fetchStyles(url)
expect(css).toContain(candidate`font-bold`)
})
// Assert the client receives an HMR update (not a full page reload).
await process.onStdout((m) => m.includes('(client) hmr update'))
},
)
test('build mode', { fs: WORKSPACE }, async ({ spawn, exec, expect }) => {
await exec('pnpm react-router build')
let process = await spawn('pnpm react-router-serve ./build/server/index.js')
let url = ''
await process.onStdout((m) => {
let match = /\[react-router-serve\]\s*(http.*) \/?/.exec(m)
if (match) url = match[1]
return url != ''
})
await retryAssertion(async () => {
let css = await fetchStyles(url)
expect(css).toContain(candidate`font-bold`)
})
})
test(
'build mode using ?url stylesheet imports should only build one stylesheet (requires `file-system` scanner)',
{
fs: {
...WORKSPACE,
'app/root.tsx': ts`
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'
import styles from './app.css?url'
export const links: Route.LinksFunction = () => [{ rel: 'stylesheet', href: styles }]
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body class="dark">
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
)
}
export default function App() {
return <Outlet />
}
`,
'vite.config.ts': ts`
import { defineConfig } from 'vite'
import { reactRouter } from '@react-router/dev/vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss(), reactRouter()],
})
`,
'.gitignore': txt`
node_modules/
build/
`,
},
},
async ({ fs, exec, expect }) => {
await exec('pnpm react-router build')
let files = await fs.glob('build/client/assets/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]
await fs.expectFileToContain(filename, [candidate`font-bold`])
},
)