Bump Vitest to v4 (#19216)

This PR bumps Vitest from v2 to v4. As far as I know we don't use any
Vitest specific features in our tests, but had to upgrade the
`vitest.workspace.ts` file to a `vitest.config.ts` file instead.

The only features we use are the typical `describe`, `it`, `test`, and
`expect` functions.

The only other part we use is `vi.spyOn` and `vi.fn` but those didn't
change in API either.

The test shards were removed to prevent errors. Not all suites have
enough files / tests to be broken up into 3 parts so Vitest now errors
when that happens.

### Test plan

1. All tests should pass in CI.
2. All integration tests should pass in CI.

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This commit is contained in:
Robin Malfait 2025-11-21 00:16:20 +01:00 committed by GitHub
parent 642b9b8576
commit 479b725cd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 346 additions and 565 deletions

View File

@ -103,18 +103,8 @@ jobs:
CARGO_PROFILE_RELEASE_LTO: 'off'
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: 'lld-link'
- name: Test ${{ matrix.integration }} 1/3
run: pnpm run test:integrations ./integrations/${{ matrix.integration }} --shard 1/3
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Test ${{ matrix.integration }} 2/3
run: pnpm run test:integrations ./integrations/${{ matrix.integration }} --shard 2/3
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Test ${{ matrix.integration }} 3/3
run: pnpm run test:integrations ./integrations/${{ matrix.integration }} --shard 3/3
- name: Test ${{ matrix.integration }}
run: pnpm run test:integrations ./integrations/${{ matrix.integration }}
env:
GITHUB_WORKSPACE: ${{ github.workspace }}

View File

@ -174,6 +174,8 @@ test(
test(
`upgrades a v3 project with prefixes to v4`,
{
// Somehow this test takes *way* longer than the rest (but not always?)
timeout: 120_000,
fs: {
'package.json': json`
{

View File

@ -37,6 +37,7 @@ interface TestConfig {
[filePath: string]: string | Uint8Array
}
timeout?: number
installDependencies?: boolean
}
interface TestContext {
@ -86,7 +87,7 @@ export function test(
return defaultTest(
name,
{
timeout: TEST_TIMEOUT,
timeout: config.timeout ?? TEST_TIMEOUT,
retry: process.env.CI ? 2 : 0,
only: only || (!process.env.CI && debug),
skip,

View File

@ -58,7 +58,7 @@
"tsup": "^8.5.1",
"turbo": "^2.6.1",
"typescript": "^5.5.4",
"vitest": "^2.0.5"
"vitest": "^4.0.3"
},
"packageManager": "pnpm@9.6.0",
"pnpm": {

View File

@ -13,6 +13,11 @@ async function migrate(input: string, config: Config = {}) {
let designSystem = await __unstable__loadDesignSystem(
css`
@import 'tailwindcss';
/* TODO(perf): Only here to speed up the tests */
@theme {
--*: initial;
}
`,
{ base: __dirname },
)
@ -85,35 +90,44 @@ it('should move the legacy `!` prefix, to the new `!` postfix notation', async (
`)
})
it('should apply all candidate migration when migrating with a config', async () => {
async function migrateWithPrefix(input: string) {
return postcss()
.use(
migrateAtApply({
designSystem: await __unstable__loadDesignSystem(
css`
@import 'tailwindcss' prefix(tw);
`,
{ base: __dirname },
),
userConfig: {
prefix: 'tw_',
},
}),
)
.process(input, { from: expect.getState().testPath })
.then((result) => result.css)
}
it(
'should apply all candidate migration when migrating with a config',
{ timeout: 10_000 },
async () => {
async function migrateWithPrefix(input: string) {
return postcss()
.use(
migrateAtApply({
designSystem: await __unstable__loadDesignSystem(
css`
@import 'tailwindcss' prefix(tw);
expect(
await migrateWithPrefix(css`
.foo {
@apply !tw_flex [color:--my-color] tw_bg-gradient-to-t;
}
`),
).toMatchInlineSnapshot(`
/* TODO(perf): Only here to speed up the tests */
@theme {
--*: initial;
}
`,
{ base: __dirname },
),
userConfig: {
prefix: 'tw_',
},
}),
)
.process(input, { from: expect.getState().testPath })
.then((result) => result.css)
}
expect(
await migrateWithPrefix(css`
.foo {
@apply !tw_flex [color:--my-color] tw_bg-gradient-to-t;
}
`),
).toMatchInlineSnapshot(`
".foo {
@apply tw:flex! tw:text-(--my-color) tw:bg-linear-to-t;
}"
`)
})
},
)

View File

@ -4,10 +4,22 @@ import * as versions from '../../utils/version'
import { migrateCandidate } from './migrate'
vi.spyOn(versions, 'isMajor').mockReturnValue(true)
const css = String.raw
describe('is-safe-migration', async () => {
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
})
let designSystem = await __unstable__loadDesignSystem(
css`
@import 'tailwindcss';
/* TODO(perf): Only here to speed up the tests */
@theme {
--*: initial;
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
`,
{ base: __dirname },
)
test.each([
[`let notBorder = !border \n`, '!border'],

View File

@ -6,6 +6,7 @@ import type { CanonicalizeOptions } from './intellisense'
import { DefaultMap } from './utils/default-map'
const css = String.raw
const timeout = 25_000
const defaultTheme = fs.readFileSync(path.resolve(__dirname, '../theme.css'), 'utf8')
const designSystems = new DefaultMap((base: string) => {
@ -20,6 +21,25 @@ const designSystems = new DefaultMap((base: string) => {
@tailwind utilities;
${defaultTheme}
/* TODO(perf): Only here to speed up the tests */
@theme {
--*: initial;
--breakpoint-lg: 64rem;
--breakpoint-md: 48rem;
--color-blue-200: oklch(88.2% 0.059 254.128);
--color-blue-500: oklch(62.3% 0.214 259.815);
--color-red-500: oklch(63.7% 0.237 25.331);
--color-white: #fff;
--container-md: 28rem;
--font-weight-normal: 400;
--leading-relaxed: 1.625;
--spacing: 0.25rem;
--text-sm--line-height: calc(1.25 / 0.875);
--text-sm: 0.875rem;
--text-xs--line-height: calc(1 / 0.75);
--text-xs: 0.75rem;
}
`,
}
},
@ -261,7 +281,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
['[font-weight:400]', 'font-normal'],
['[line-height:0]', 'leading-0'],
['[border-style:solid]', 'border-solid'],
])(testName, async (candidate, expected) => {
])(testName, { timeout }, async (candidate, expected) => {
await expectCanonicalization(
css`
@import 'tailwindcss';
@ -341,7 +361,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
// Arbitrary percentage value must be a whole number. Should not migrate to
// a bare value.
['from-[2.5%]', 'from-[2.5%]'],
])(testName, async (candidate, expected) => {
])(testName, { timeout }, async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
@ -358,7 +378,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
await expectCanonicalization(input, candidate, expected)
})
test('migrate with custom static utility `@utility custom {…}`', async () => {
test('migrate with custom static utility `@utility custom {…}`', { timeout }, async () => {
let candidate = '[--key:value]'
let expected = 'custom'
@ -375,39 +395,47 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
await expectCanonicalization(input, candidate, expected)
})
test('migrate with custom functional utility `@utility custom-* {…}`', async () => {
let candidate = '[--key:value]'
let expected = 'custom-value'
test(
'migrate with custom functional utility `@utility custom-* {…}`',
{ timeout },
async () => {
let candidate = '[--key:value]'
let expected = 'custom-value'
let input = css`
@import 'tailwindcss';
@theme {
--*: initial;
}
@utility custom-* {
--key: --value('value');
}
`
let input = css`
@import 'tailwindcss';
@theme {
--*: initial;
}
@utility custom-* {
--key: --value('value');
}
`
await expectCanonicalization(input, candidate, expected)
})
await expectCanonicalization(input, candidate, expected)
},
)
test('migrate with custom functional utility `@utility custom-* {…}` that supports bare values', async () => {
let candidate = '[tab-size:4]'
let expected = 'tab-4'
test(
'migrate with custom functional utility `@utility custom-* {…}` that supports bare values',
{ timeout },
async () => {
let candidate = '[tab-size:4]'
let expected = 'tab-4'
let input = css`
@import 'tailwindcss';
@theme {
--*: initial;
}
@utility tab-* {
tab-size: --value(integer);
}
`
let input = css`
@import 'tailwindcss';
@theme {
--*: initial;
}
@utility tab-* {
tab-size: --value(integer);
}
`
await expectCanonicalization(input, candidate, expected)
})
await expectCanonicalization(input, candidate, expected)
},
)
test.each([
['[tab-size:0]', 'tab-0'],
@ -462,33 +490,41 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
// the semantics of the value.
['max-w-(--breakpoint-md)', 'max-w-(--breakpoint-md)'],
['max-w-(--container-3xl)', 'max-w-3xl'],
])(`migrate arbitrary value to theme value ${testName}`, async (candidate, expected) => {
])(
`migrate arbitrary value to theme value ${testName}`,
{ timeout },
async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
${theme} {
--*: initial;
--breakpoint-md: 48rem;
--container-3xl: 48rem;
}
`
await expectCanonicalization(input, candidate, expected)
},
)
})
test(
'migrate an arbitrary property without spaces, to a theme value with spaces (canonicalization)',
{ timeout },
async () => {
let candidate = 'font-[foo,bar,baz]'
let expected = 'font-example'
let input = css`
@import 'tailwindcss';
${theme} {
@theme {
--*: initial;
--breakpoint-md: 48rem;
--container-3xl: 48rem;
--font-example: foo, bar, baz;
}
`
await expectCanonicalization(input, candidate, expected)
})
})
test('migrate an arbitrary property without spaces, to a theme value with spaces (canonicalization)', async () => {
let candidate = 'font-[foo,bar,baz]'
let expected = 'font-example'
let input = css`
@import 'tailwindcss';
@theme {
--*: initial;
--font-example: foo, bar, baz;
}
`
await expectCanonicalization(input, candidate, expected)
})
},
)
test.each([
// Default spacing scale
@ -503,7 +539,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
// Custom pixel based spacing scale
['w-[123px]', 'w-123', '1px'],
['w-[256px]', 'w-128', '2px'],
])(`${testName} (spacing = \`%s\`)`, async (candidate, expected, spacing) => {
])(`${testName} (spacing = \`%s\`)`, { timeout }, async (candidate, expected, spacing) => {
let input = css`
@import 'tailwindcss';
@ -538,13 +574,13 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
// Custom utility with bare value integer
['tab-8', 'tab-github'],
])(testName, async (candidate, expected) => {
])(testName, { timeout }, async (candidate, expected) => {
await expectCanonicalization(input, candidate, expected)
})
})
describe('deprecated utilities', () => {
test('`order-none` → `order-0`', async () => {
test('`order-none` → `order-0`', { timeout }, async () => {
let candidate = 'order-none'
let expected = 'order-0'
@ -555,7 +591,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
await expectCanonicalization(input, candidate, expected)
})
test('`order-none` → `order-none` with custom implementation', async () => {
test('`order-none` → `order-none` with custom implementation', { timeout }, async () => {
let candidate = 'order-none'
let expected = 'order-none'
@ -570,7 +606,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
await expectCanonicalization(input, candidate, expected)
})
test('`break-words` → `wrap-break-word`', async () => {
test('`break-words` → `wrap-break-word`', { timeout }, async () => {
let candidate = 'break-words'
let expected = 'wrap-break-word'
@ -581,7 +617,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
await expectCanonicalization(input, candidate, expected)
})
test('`[overflow-wrap:break-word]` → `wrap-break-word`', async () => {
test('`[overflow-wrap:break-word]` → `wrap-break-word`', { timeout }, async () => {
let candidate = '[overflow-wrap:break-word]'
let expected = 'wrap-break-word'
@ -592,7 +628,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
await expectCanonicalization(input, candidate, expected)
})
test('`break-words` → `break-words` with custom implementation', async () => {
test('`break-words` → `break-words` with custom implementation', { timeout }, async () => {
let candidate = 'break-words'
let expected = 'break-words'
@ -635,11 +671,11 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
['group-[&:focus]:flex', 'group-focus:flex'],
['peer-[&:focus]:flex', 'peer-focus:flex'],
['in-[&:focus]:flex', 'in-focus:flex'],
])(testName, async (candidate, expected) => {
])(testName, { timeout }, async (candidate, expected) => {
await expectCanonicalization(input, candidate, expected)
})
test('unsafe migrations keep the candidate as-is', async () => {
test('unsafe migrations keep the candidate as-is', { timeout }, async () => {
// `hover:` also includes an `@media` query in addition to the `&:hover`
// state. Migration is not safe because the functionality would be different.
let candidate = '[&:hover]:flex'
@ -654,7 +690,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
await expectCanonicalization(input, candidate, expected)
})
test('make unsafe migration safe (1)', async () => {
test('make unsafe migration safe (1)', { timeout }, async () => {
// Overriding the `hover:` variant to only use a selector will make the
// migration safe.
let candidate = '[&:hover]:flex'
@ -670,7 +706,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
await expectCanonicalization(input, candidate, expected)
})
test('make unsafe migration safe (2)', async () => {
test('make unsafe migration safe (2)', { timeout }, async () => {
// Overriding the `hover:` variant to only use a selector will make the
// migration safe. This time with the long-hand `@variant` syntax.
let candidate = '[&:hover]:flex'
@ -690,7 +726,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
await expectCanonicalization(input, candidate, expected)
})
test('custom selector-based variants', async () => {
test('custom selector-based variants', { timeout }, async () => {
let candidate = '[&.macos]:flex'
let expected = 'is-macos:flex'
let input = css`
@ -704,7 +740,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
await expectCanonicalization(input, candidate, expected)
})
test('custom @media-based variants', async () => {
test('custom @media-based variants', { timeout }, async () => {
let candidate = '[@media(prefers-reduced-transparency:reduce)]:flex'
let expected = 'transparency-safe:flex'
let input = css`
@ -741,7 +777,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
// A color with a known theme variable migrates to the full utility
['bg-(color:--color-red-500)', 'bg-red-500'],
])(testName, async (candidate, expected) => {
])(testName, { timeout }, async (candidate, expected) => {
await expectCanonicalization(input, candidate, expected)
})
})
@ -828,7 +864,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
'data-[selected]:aria-[selected="true"]:aspect-[12/34]',
'data-selected:aria-selected:aspect-12/34',
],
])(testName, async (candidate, expected) => {
])(testName, { timeout }, async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
`
@ -935,7 +971,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
['has-[[aria-visible]]:flex', 'has-aria-[visible]:flex'],
['has-[&:not(:nth-child(even))]:flex', 'has-odd:flex'],
])(testName, async (candidate, expected) => {
])(testName, { timeout }, async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
`
@ -968,7 +1004,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
// Keep modifiers on classes that don't _really_ exist
['group/name', 'group/name'],
])(testName, async (candidate, expected) => {
])(testName, { timeout }, async (candidate, expected) => {
await expectCanonicalization(input, candidate, expected)
})
})
@ -996,6 +1032,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
['[font-size:14px] [line-height:1.625]', 'text-sm/relaxed'],
])(
'should canonicalize multiple classes `%s` into a shorthand `%s`',
{ timeout },
async (candidates, expected) => {
let input = css`
@import 'tailwindcss';
@ -1006,7 +1043,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
})
describe('theme to var', () => {
test('extended space scale converts to var or calc', async () => {
test('extended space scale converts to var or calc', { timeout }, async () => {
let designSystem = await __unstable__loadDesignSystem(
css`
@tailwind utilities;
@ -1033,7 +1070,7 @@ describe('theme to var', () => {
])
})
test('custom space scale converts to var', async () => {
test('custom space scale converts to var', { timeout }, async () => {
let designSystem = await __unstable__loadDesignSystem(
css`
@tailwind utilities;
@ -1060,7 +1097,7 @@ describe('theme to var', () => {
})
describe('options', () => {
test('normalize `rem` units to `px`', async () => {
test('normalize `rem` units to `px`', { timeout }, async () => {
let designSystem = await __unstable__loadDesignSystem(
css`
@tailwind utilities;

616
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

8
vitest.config.ts Normal file
View File

@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: ['./packages/*', '!./packages/tsconfig.base.json'],
exclude: ['**/*.spec.?(c|m)[jt]s?(x)', 'integrations/**/*'],
},
})

View File

@ -1 +0,0 @@
export default ['packages/*']