tailwindcss/integrations/vite/index.test.ts
Robin Malfait 2a29c29441
Improve integration tests (stability + performance) (#15125)
This PR improves the integration tests in two ways:
1. Make the integration tests more reliable and thus less flakey
2. Make the integration tests faster (by introducing concurrency)

Tried a lot of different things to make sure that these tests are fast
and stable.

---

The biggest issue we noticed is that some tests are flakey, these are
tests with long running dev-mode processes where watchers are being used
and/or dev servers are created.
To solve this, all the tests that spawn a process look at stdout/stderr
and wait for a message from the process to know whether we can start
making changes.

For example, in case of an Astro project, you get a `watching for file
changes` message. In case of Nuxt project you can wait for an `server
warmed up in` and in case of Next.js there is a `Ready in` message.

These depend on the tools being used, so this is hardcoded per test
instead of a magically automatic solution.

These messages allow us to wait until all the initial necessary work,
internal watchers and/or dev servers are setup before we start making
changes to the files and/or request CSS stylesheets before the server(s)
are ready.

---

Another improvement is how we setup the dev servers. Before, we used to
try and get a free port on the system and use a `--port` flag or a
`PORT` environment variable. Instead of doing this (which is slow), we
rely on the process itself to show a URL with a port. Basically all
tools will try to find a free port if the default port is in use. We can
then use the stdout/stderr messages to get the URL and the port to use.

To reduce the amount of potential conflicts in ports, we used to run
every test and every file sequentially to basically guarantee that ports
are free. With this new approach where we rely on the process, I noticed
that we don't really run into this issue again (I reran the tests
multiple times and they were always stable)

<img width="316" alt="image"
src="https://github.com/user-attachments/assets/b75ddab4-f919-4995-85d0-f212b603e5c2"
/>
Note: these tests run Linux, Windows and macOS in this branch just for
testing purposes. Once this is done, we will only run Linux tests on PRs
and run all 3 of them on the `next` branch.

We do make the tests concurrent by default now, which in theory means
that there could be conflicts (which in practice means that the process
has to do a few more tries to find a free port). To reduce these
conflicts, we split up the integration tests such that Vite, PostCSS,
CLI, … tests all run in a separate job in the GitHub actions workflow.

<img width="312" alt="image"
src="https://github.com/user-attachments/assets/fe9a58a1-98eb-4d9b-8845-a7c8a7af5766"
/>

Comparing this branch against the `next` branch, this is what CI looks
like right now:

| `next` | `feat/improve-integration-tests` |
| --- | --- |
| <img width="594" alt="image"
src="https://github.com/user-attachments/assets/540d21eb-ab03-42e8-9f6f-b3a071fc7635"
/> | <img width="672" alt="image"
src="https://github.com/user-attachments/assets/8ef2e891-08a1-464b-9954-4153174ebce7"
/> |

There also was a point in time where I introduced sequential tests such
that all spawned processes still run after each other, but so far I
didn't run into issues if we keep them concurrent so I dropped that
code.

Some small changes I made to make things more reliable:
1. When relying on stdout/stderr messages, we split lines on `\n` and we
strip all the ANSI escapes which allows us to not worry about special
ANSI characters when finding the URL or a specific message to wait for.
2. Once a test is done, we `child.kill()` the spawned process. If that
doesn't work, for whatever reason, we run a `child.kill('SIGKILL')` to
force kill the process. This could technically lead to some memory or
files not being cleaned up properly, but once CI is done, everything is
thrown away anyway.
3. As you can see in the screenshots, I used some nicer names for the
workflows.

| `next` | `feat/improve-integration-tests` |
| --- | --- |
| <img width="276" alt="image"
src="https://github.com/user-attachments/assets/e574bb53-e21b-4619-9cdb-515431b255b9"
/> | <img width="179" alt="image"
src="https://github.com/user-attachments/assets/8bc75119-fb91-4500-a1d0-bd09f74c93ad"
/> |

They also look a bit nicer in the PR overview as well:
<img width="929" alt="image"
src="https://github.com/user-attachments/assets/04fc71fc-74b0-4e7c-9047-2aada664efef"
/>

The very last commit just filters out Windows and macOS tests again for
PRs (but they are executed on the `next` branch.

---

### Nest steps

I think for now we are in a pretty good state, but there are some things
we can do to further improve everything (mainly make things faster) but
aren't necessary. I also ran into issue while trying it so there is more
work to do.

1. More splits — instead of having a Vite folder and PostCSS folder, we
can go a step further and have folders for Next.js, Astro, Nuxt, Remix,
…
2. Caching — right now we have to run the build step for every OS on
every "job". We can re-use the work here by introducing a setup job that
the other jobs rely on. @thecrypticace and I tried it already, but were
running into some Bun specific Standalone CLI issues when doing that.
3. Remote caching — we could re-enable remote caching such that the
`build` step can be full turbo (e.g.: after a PR is merged in `next` and
we run everything again)
2024-12-12 13:48:56 +01:00

847 lines
25 KiB
TypeScript

import path from 'node:path'
import { describe } from 'vitest'
import {
candidate,
css,
fetchStyles,
html,
js,
json,
retryAssertion,
test,
ts,
txt,
yaml,
} from '../utils'
describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
test(
`production build`,
{
fs: {
'package.json': json`{}`,
'pnpm-workspace.yaml': yaml`
#
packages:
- project-a
`,
'project-a/package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
"vite": "^6"
}
}
`,
'project-a/vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'project-a/index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="underline m-2">Hello, world!</div>
</body>
`,
'project-a/tailwind.config.js': js`
export default {
content: ['../project-b/src/**/*.js'],
}
`,
'project-a/src/index.css': css`
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss/utilities';
@config '../tailwind.config.js';
@source '../../project-b/src/**/*.html';
`,
'project-b/src/index.html': html`
<div class="flex" />
`,
'project-b/src/index.js': js`
const className = "content-['project-b/src/index.js']"
module.exports = { className }
`,
},
},
async ({ root, fs, exec, expect }) => {
await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })
let files = await fs.glob('project-a/dist/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]
await fs.expectFileToContain(filename, [
candidate`underline`,
candidate`m-2`,
candidate`flex`,
candidate`content-['project-b/src/index.js']`,
])
},
)
test(
'dev mode',
{
fs: {
'package.json': json`{}`,
'pnpm-workspace.yaml': yaml`
#
packages:
- project-a
`,
'project-a/package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
"vite": "^6"
}
}
`,
'project-a/vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'project-a/index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="underline">Hello, world!</div>
</body>
`,
'project-a/about.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="font-bold">Tailwind Labs</div>
</body>
`,
'project-a/tailwind.config.js': js`
export default {
content: ['../project-b/src/**/*.js'],
}
`,
'project-a/src/index.css': css`
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss/utilities';
@config '../tailwind.config.js';
@source '../../project-b/src/**/*.html';
`,
'project-b/src/index.html': html`
<div class="flex" />
`,
'project-b/src/index.js': js`
const className = "content-['project-b/src/index.js']"
module.exports = { className }
`,
},
},
async ({ root, spawn, fs, expect }) => {
let process = await spawn('pnpm vite dev', {
cwd: path.join(root, 'project-a'),
})
await process.onStdout((m) => m.includes('ready in'))
let url = ''
await process.onStdout((m) => {
let match = /Local:\s*(http.*)\//.exec(m)
if (match) url = match[1]
return Boolean(url)
})
// Candidates are resolved lazily, so the first visit of index.html
// will only have candidates from this file.
await retryAssertion(async () => {
let styles = await fetchStyles(url, '/index.html')
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`flex`)
expect(styles).not.toContain(candidate`font-bold`)
})
// Going to about.html will extend the candidate list to include
// candidates from about.html.
await retryAssertion(async () => {
let styles = await fetchStyles(url, '/about.html')
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`flex`)
expect(styles).toContain(candidate`font-bold`)
})
await retryAssertion(async () => {
// Updates are additive and cause new candidates to be added.
await fs.write(
'project-a/index.html',
html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="underline m-2">Hello, world!</div>
</body>
`,
)
let styles = await fetchStyles(url)
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`flex`)
expect(styles).toContain(candidate`font-bold`)
expect(styles).toContain(candidate`m-2`)
})
await retryAssertion(async () => {
// Manually added `@source`s are watched and trigger a rebuild
await fs.write(
'project-b/src/index.js',
js`
const className = "[.changed_&]:content-['project-b/src/index.js']"
module.exports = { className }
`,
)
let styles = await fetchStyles(url)
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`flex`)
expect(styles).toContain(candidate`font-bold`)
expect(styles).toContain(candidate`m-2`)
expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
})
await retryAssertion(async () => {
// After updates to the CSS file, all previous candidates should still be in
// the generated CSS
await fs.write(
'project-a/src/index.css',
css`
${await fs.read('project-a/src/index.css')}
.red {
color: red;
}
`,
)
let styles = await fetchStyles(url)
expect(styles).toContain(candidate`red`)
expect(styles).toContain(candidate`flex`)
expect(styles).toContain(candidate`m-2`)
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
expect(styles).toContain(candidate`font-bold`)
})
},
)
test(
'watch mode',
{
fs: {
'package.json': json`{}`,
'pnpm-workspace.yaml': yaml`
#
packages:
- project-a
`,
'project-a/package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
"vite": "^6"
}
}
`,
'project-a/vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'project-a/index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="underline text-primary">Hello, world!</div>
</body>
`,
'project-a/tailwind.config.js': js`
export default {
content: ['../project-b/src/**/*.js'],
}
`,
'project-a/src/index.css': css`
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss/utilities';
@import './custom-theme.css';
@config '../tailwind.config.js';
@source '../../project-b/src/**/*.html';
`,
'project-a/src/custom-theme.css': css`
/* Will be overwritten later */
@theme {
--color-primary: black;
}
`,
'project-b/src/index.html': html`
<div class="flex" />
`,
'project-b/src/index.js': js`
const className = "content-['project-b/src/index.js']"
module.exports = { className }
`,
},
},
async ({ root, spawn, fs, expect }) => {
let process = await spawn('pnpm vite build --watch', {
cwd: path.join(root, 'project-a'),
})
await process.onStdout((m) => m.includes('built in'))
let filename = ''
await retryAssertion(async () => {
let files = await fs.glob('project-a/dist/**/*.css')
expect(files).toHaveLength(1)
filename = files[0][0]
})
await fs.expectFileToContain(filename, [
candidate`underline`,
candidate`flex`,
css`
.text-primary {
color: var(--color-primary);
}
`,
])
await retryAssertion(async () => {
await fs.write(
'project-a/src/custom-theme.css',
css`
/* Overriding the primary color */
@theme {
--color-primary: red;
}
`,
)
let files = await fs.glob('project-a/dist/**/*.css')
expect(files).toHaveLength(1)
let [, styles] = files[0]
expect(styles).toContain(css`
.text-primary {
color: var(--color-primary);
}
`)
})
await retryAssertion(async () => {
// Updates are additive and cause new candidates to be added.
await fs.write(
'project-a/index.html',
html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="underline m-2">Hello, world!</div>
</body>
`,
)
let files = await fs.glob('project-a/dist/**/*.css')
expect(files).toHaveLength(1)
let [, styles] = files[0]
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`flex`)
expect(styles).toContain(candidate`m-2`)
})
await retryAssertion(async () => {
// Manually added `@source`s are watched and trigger a rebuild
await fs.write(
'project-b/src/index.js',
js`
const className = "[.changed_&]:content-['project-b/src/index.js']"
module.exports = { className }
`,
)
let files = await fs.glob('project-a/dist/**/*.css')
expect(files).toHaveLength(1)
let [, styles] = files[0]
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`flex`)
expect(styles).toContain(candidate`m-2`)
expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
})
await retryAssertion(async () => {
// After updates to the CSS file, all previous candidates should still be in
// the generated CSS
await fs.write(
'project-a/src/index.css',
css`
${await fs.read('project-a/src/index.css')}
.red {
color: red;
}
`,
)
let files = await fs.glob('project-a/dist/**/*.css')
expect(files).toHaveLength(1)
let [, styles] = files[0]
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`flex`)
expect(styles).toContain(candidate`m-2`)
expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
expect(styles).toContain(candidate`red`)
})
},
)
test(
`source(none) disables looking at the module graph`,
{
fs: {
'package.json': json`{}`,
'pnpm-workspace.yaml': yaml`
#
packages:
- project-a
`,
'project-a/package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
"vite": "^6"
}
}
`,
'project-a/vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'project-a/index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="underline m-2">Hello, world!</div>
</body>
`,
'project-a/src/index.css': css`
@import 'tailwindcss' source(none);
@source '../../project-b/src/**/*.html';
`,
'project-b/src/index.html': html`
<div class="flex" />
`,
'project-b/src/index.js': js`
const className = "content-['project-b/src/index.js']"
module.exports = { className }
`,
},
},
async ({ root, fs, exec, expect }) => {
await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })
let files = await fs.glob('project-a/dist/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]
// `underline` and `m-2` are only present from files in the module graph
// which we've explicitly disabled with source(none) so they should not
// be present
await fs.expectFileNotToContain(filename, [
//
candidate`underline`,
candidate`m-2`,
])
// The files from `project-b` should be included because there is an
// explicit `@source` directive for it
await fs.expectFileToContain(filename, [
//
candidate`flex`,
])
// The explicit source directive only covers HTML files, so the JS file
// should not be included
await fs.expectFileNotToContain(filename, [
//
candidate`content-['project-b/src/index.js']`,
])
},
)
test(
`source("…") filters the module graph`,
{
fs: {
'package.json': json`{}`,
'pnpm-workspace.yaml': yaml`
#
packages:
- project-a
`,
'project-a/package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
"vite": "^6"
}
}
`,
'project-a/vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'project-a/index.html': html`
<head>
<link rel="stylesheet" href="/src/index.css" />
</head>
<body>
<div class="underline m-2 content-['project-a/index.html']">Hello, world!</div>
<script type="module" src="/app/index.js"></script>
</body>
`,
'project-a/app/index.js': js`
const className = "content-['project-a/app/index.js']"
export default { className }
`,
'project-a/src/index.css': css`
@import 'tailwindcss' source('../app');
@source '../../project-b/src/**/*.html';
`,
'project-b/src/index.html': html`
<div
class="content-['project-b/src/index.html']"
/>
`,
'project-b/src/index.js': js`
const className = "content-['project-b/src/index.js']"
module.exports = { className }
`,
},
},
async ({ root, fs, exec, expect }) => {
await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })
let files = await fs.glob('project-a/dist/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]
// `underline` and `m-2` are present in files in the module graph but
// we've filtered the module graph such that we only look in
// `./app/**/*` so they should not be present
await fs.expectFileNotToContain(filename, [
//
candidate`underline`,
candidate`m-2`,
candidate`content-['project-a/index.html']`,
])
// We've filtered the module graph to only look in ./app/**/* so the
// candidates from that project should be present
await fs.expectFileToContain(filename, [
//
candidate`content-['project-a/app/index.js']`,
])
// Even through we're filtering the module graph explicit sources are
// additive and as such files from `project-b` should be included
// because there is an explicit `@source` directive for it
await fs.expectFileToContain(filename, [
//
candidate`content-['project-b/src/index.html']`,
])
// The explicit source directive only covers HTML files, so the JS file
// should not be included
await fs.expectFileNotToContain(filename, [
//
candidate`content-['project-b/src/index.js']`,
])
},
)
test(
`source("…") must be a directory`,
{
fs: {
'package.json': json`{}`,
'pnpm-workspace.yaml': yaml`
#
packages:
- project-a
`,
'project-a/package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
"vite": "^6"
}
}
`,
'project-a/vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'project-a/index.html': html`
<head>
<link rel="stylesheet" href="/src/index.css" />
</head>
<body>
<div class="underline m-2 content-['project-a/index.html']">Hello, world!</div>
<script type="module" src="/app/index.js"></script>
</body>
`,
'project-a/app/index.js': js`
const className = "content-['project-a/app/index.js']"
export default { className }
`,
'project-a/src/index.css': css`
@import 'tailwindcss' source('../i-do-not-exist');
@source '../../project-b/src/**/*.html';
`,
'project-b/src/index.html': html`
<div
class="content-['project-b/src/index.html']"
/>
`,
'project-b/src/index.js': js`
const className = "content-['project-b/src/index.js']"
module.exports = { className }
`,
},
},
async ({ root, fs, exec, expect }) => {
await expect(() =>
exec('pnpm vite build', { cwd: path.join(root, 'project-a') }, { ignoreStdErr: true }),
).rejects.toThrowError('The `source(../i-do-not-exist)` does not exist')
let files = await fs.glob('project-a/dist/**/*.css')
expect(files).toHaveLength(0)
},
)
})
test(
`demote Tailwind roots to regular CSS files and back to Tailwind roots while restoring all candidates`,
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"vite": "^6"
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="underline">Hello, world!</div>
</body>
`,
'about.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="font-bold">Tailwind Labs</div>
</body>
`,
'src/index.css': css`@import 'tailwindcss';`,
},
},
async ({ spawn, fs, expect }) => {
let process = await spawn('pnpm vite dev')
await process.onStdout((m) => m.includes('ready in'))
let url = ''
await process.onStdout((m) => {
let match = /Local:\s*(http.*)\//.exec(m)
if (match) url = match[1]
return Boolean(url)
})
// Candidates are resolved lazily, so the first visit of index.html
// will only have candidates from this file.
await retryAssertion(async () => {
let styles = await fetchStyles(url, '/index.html')
expect(styles).toContain(candidate`underline`)
expect(styles).not.toContain(candidate`font-bold`)
})
// Going to about.html will extend the candidate list to include
// candidates from about.html.
await retryAssertion(async () => {
let styles = await fetchStyles(url, '/about.html')
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`font-bold`)
})
await retryAssertion(async () => {
// We change the CSS file so it is no longer a valid Tailwind root.
await fs.write('src/index.css', css`@import 'tailwindcss';`)
let styles = await fetchStyles(url)
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`font-bold`)
})
},
)
test(
`does not interfere with ?raw and ?url static asset handling`,
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"vite": "^6"
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'index.html': html`
<head>
<script type="module" src="./src/index.js"></script>
</head>
`,
'src/index.js': js`
import url from './index.css?url'
import raw from './index.css?raw'
`,
'src/index.css': css`@import 'tailwindcss';`,
},
},
async ({ spawn, expect }) => {
let process = await spawn('pnpm vite dev')
await process.onStdout((m) => m.includes('ready in'))
let baseUrl = ''
await process.onStdout((m) => {
let match = /Local:\s*(http.*)\//.exec(m)
if (match) baseUrl = match[1]
return Boolean(baseUrl)
})
await retryAssertion(async () => {
// We have to load the .js file first so that the static assets are
// resolved
await fetch(`${baseUrl}/src/index.js`).then((r) => r.text())
let [raw, url] = await Promise.all([
fetch(`${baseUrl}/src/index.css?raw`).then((r) => r.text()),
fetch(`${baseUrl}/src/index.css?url`).then((r) => r.text()),
])
expect(firstLine(raw)).toBe(`export default "@import 'tailwindcss';"`)
expect(firstLine(url)).toBe(`export default "/src/index.css"`)
})
},
)
function firstLine(str: string) {
return str.split('\n')[0]
}