Small refactor of canonicalization tests (#19851)

This are getting a little bit out of hand here, so this is an initial
refactor.

## Test plan

1. All tests are still there
2. All tests are still passing
This commit is contained in:
Robin Malfait 2026-03-25 15:18:14 +01:00 • committed by GitHub
parent c385fd36bc
commit 52fd421cc9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -93,30 +93,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
}
async function expectCanonicalization(
input: string,
candidate: string,
expected: string,
options: CanonicalizeOptions = DEFAULT_CANONICALIZATION_OPTIONS,
) {
candidate = prepare(candidate)
expected = prepare(expected)
if (strategy === 'prefix') {
input = input.replace("@import 'tailwindcss';", "@import 'tailwindcss' prefix(tw);")
}
let designSystem = await designSystems.get(__dirname).get(input)
let [actual] = designSystem.canonicalizeCandidates([candidate], options)
try {
expect(actual).toBe(expected)
} catch (err) {
if (err instanceof Error) Error.captureStackTrace(err, expectCanonicalization)
throw err
}
}
async function expectCombinedCanonicalization(
input: string,
candidates: string,
expected: string,
@ -135,179 +111,190 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
try {
expect(actual).toEqual(preparedExpected)
} catch (err) {
if (err instanceof Error) Error.captureStackTrace(err, expectCombinedCanonicalization)
if (err instanceof Error) Error.captureStackTrace(err, expectCanonicalization)
throw err
}
}
/// ----------------------------------
test.each([
/// Legacy bg-gradient-* → bg-linear-*
['bg-gradient-to-t', 'bg-linear-to-t'],
['bg-gradient-to-tr', 'bg-linear-to-tr'],
['bg-gradient-to-r', 'bg-linear-to-r'],
['bg-gradient-to-br', 'bg-linear-to-br'],
['bg-gradient-to-b', 'bg-linear-to-b'],
['bg-gradient-to-bl', 'bg-linear-to-bl'],
['bg-gradient-to-l', 'bg-linear-to-l'],
['bg-gradient-to-tl', 'bg-linear-to-tl'],
/// theme(…) to `var(…)`
// Keep candidates that don't contain `theme(…)` or `theme(…, …)`
['[color:red]', 'text-[red]'],
// Handle special cases around `.1` in the `theme(…)`
['[--value:theme(spacing.1)]', '[--value:--spacing(1)]'],
['[--value:theme(fontSize.xs.1.lineHeight)]', '[--value:var(--text-xs--line-height)]'],
['[--value:theme(spacing[1.25])]', '[--value:--spacing(1.25)]'],
// Should not convert invalid spacing values to calc
['[--value:theme(spacing[1.1])]', '[--value:theme(spacing[1.1])]'],
// Convert to `var(…)` if we can resolve the path
['[color:theme(colors.red.500)]', 'text-red-500'], // Arbitrary property
['[color:theme(colors.red.500)]/50', 'text-red-500/50'], // Arbitrary property + modifier
['bg-[theme(colors.red.500)]', 'bg-red-500'], // Arbitrary value
['bg-[size:theme(spacing.4)]', 'bg-size-[--spacing(4)]'], // Arbitrary value + data type hint
// Pretty print CSS functions preceded by an operator to prevent consecutive
// operator characters.
['w-[calc(100dvh-theme(spacing.2))]', 'w-[calc(100dvh-(--spacing(2)))]'],
['w-[calc(100dvh+theme(spacing.2))]', 'w-[calc(100dvh+(--spacing(2)))]'],
['w-[calc(100dvh/theme(spacing.2))]', 'w-[calc(100dvh/(--spacing(2)))]'],
['w-[calc(100dvh*theme(spacing.2))]', 'w-[calc(100dvh*(--spacing(2)))]'],
// Convert to `var(…)` if we can resolve the path, but keep fallback values
['bg-[theme(colors.red.500,red)]', 'bg-(--color-red-500,red)'],
// Keep `theme(…)` if we can't resolve the path
['bg-[theme(colors.foo.1000)]', 'bg-[theme(colors.foo.1000)]'],
// Keep `theme(…)` if we can't resolve the path, but still try to convert the
// fallback value.
['bg-[theme(colors.foo.1000,theme(colors.red.500))]', 'bg-red-500'],
// Use `theme(…)` (deeply nested) inside of a `calc(…)` function
['text-[calc(theme(fontSize.xs)*2)]', 'text-[calc(var(--text-xs)*2)]'],
// Multiple `theme(… / …)` calls should result in modern syntax of `theme(…)`
// - Can't convert to `var(…)` because that would lose the modifier.
// - Can't convert to a candidate modifier because there are multiple
// `theme(…)` calls.
//
// If we really want to, we can make a fancy migration that tries to move it
// to a candidate modifier _if_ all `theme(…)` calls use the same modifier.
[
'[color:theme(colors.red.500/50,theme(colors.blue.500/50))]',
'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]',
],
[
'[color:theme(colors.red.500/50,theme(colors.blue.500/50))]/50',
'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]/50',
],
// Convert the `theme(…)`, but try to move the inline modifier (e.g. `50%`),
// to a candidate modifier.
// Arbitrary property, with simple percentage modifier
['[color:theme(colors.red.500/75%)]', 'text-red-500/75'],
// Arbitrary property, with numbers (0-1) without a unit
['[color:theme(colors.red.500/.12)]', 'text-red-500/12'],
['[color:theme(colors.red.500/0.12)]', 'text-red-500/12'],
// Arbitrary property, with more complex modifier (we only allow whole numbers
// as bare modifiers). Convert the complex numbers to arbitrary values instead.
['[color:theme(colors.red.500/12.34%)]', 'text-red-500/[12.34%]'],
['[color:theme(colors.red.500/var(--opacity))]', 'text-red-500/(--opacity)'],
['[color:theme(colors.red.500/.12345)]', 'text-red-500/1234.5'],
['[color:theme(colors.red.500/50.25%)]', 'text-red-500/50.25'],
// Arbitrary value
['bg-[theme(colors.red.500/75%)]', 'bg-red-500/75'],
['bg-[theme(colors.red.500/12.34%)]', 'bg-red-500/[12.34%]'],
// Arbitrary property that already contains a modifier
['[color:theme(colors.red.500/50%)]/50', 'text-[--theme(--color-red-500/50%)]/50'],
// Values that don't contain only `theme(…)` calls should not be converted to
// use a modifier since the color is not the whole value.
[
'shadow-[shadow:inset_0px_1px_theme(colors.white/15%)]',
'shadow-[inset_0px_1px_--theme(--color-white/15%)]',
],
// Arbitrary value, where the candidate already contains a modifier
// This should still migrate the `theme(…)` syntax to the modern syntax.
['bg-[theme(colors.red.500/50%)]/50', 'bg-[--theme(--color-red-500/50%)]/50'],
// Variants, we can't use `var(…)` especially inside of `@media(…)`. We can
// still upgrade the `theme(…)` to the modern syntax.
['max-[theme(screens.lg)]:flex', 'max-[--theme(--breakpoint-lg)]:flex'],
// There are no variables for `--spacing` multiples, so we can't convert this
['max-[theme(spacing.4)]:flex', 'max-[theme(spacing.4)]:flex'],
// This test in itself doesn't make much sense. But we need to make sure
// that this doesn't end up as the modifier in the candidate itself.
['max-[theme(spacing.4/50)]:flex', 'max-[theme(spacing.4/50)]:flex'],
// `theme(…)` calls in another CSS function is replaced correctly.
// Additionally we remove unnecessary whitespace.
['grid-cols-[min(50%_,_theme(spacing.80))_auto]', 'grid-cols-[min(50%,--spacing(80))_auto]'],
// `calc(var(--spacing)*…)` to `--spacing(…)`
['pt-[min(20%,calc(var(--spacing)*8))]', 'pt-[min(20%,--spacing(8))]'],
['pt-[min(20%,calc(var(--spacing)*var(--other)))]', 'pt-[min(20%,--spacing(var(--other)))]'],
['pt-[calc(var(--spacing)*8)]', 'pt-8'],
['pt-[calc(var(--spacing)*var(--other))]', 'pt-[--spacing(var(--other))]'],
['[padding-top:min(20%,calc(var(--spacing)*8))]', 'pt-[min(20%,--spacing(8))]'],
[
'[padding-top:min(20%,calc(var(--spacing)*var(--other)))]',
'pt-[min(20%,--spacing(var(--other)))]',
],
['[padding-top:calc(var(--spacing)*8)]', 'pt-8'],
['[padding-top:calc(var(--spacing)*var(--other))]', 'pt-[--spacing(var(--other))]'],
// `theme(…)` calls valid in v3, but not in v4 should still be converted.
['[--foo:theme(transitionDuration.500)]', '[--foo:theme(transitionDuration.500)]'],
// Renamed theme keys
['max-w-[theme(screens.md)]', 'max-w-(--breakpoint-md)'],
['w-[theme(maxWidth.md)]', 'w-md'],
// Invalid cases
['[--foo:theme(colors.red.500/50/50)]', '[--foo:theme(colors.red.500/50/50)]'],
['[--foo:theme(colors.red.500/50/50)]/50', '[--foo:theme(colors.red.500/50/50)]/50'],
// Partially invalid cases
[
'[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]',
'[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]',
],
[
'[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]/50',
'[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]/50',
],
// If a utility sets `property` and `--tw-{property}` with the same value,
// we can ignore the `--tw-{property}`. This is just here for composition.
// This means that we should be able to upgrade the one _without_ to the one
// _with_ the variable
['[font-weight:400]', 'font-normal'],
['[line-height:0]', 'leading-0'],
['[border-style:solid]', 'border-solid'],
])(testName, { timeout }, async (candidate, expected) => {
await expectCanonicalization(
css`
describe('deprecated utilities', () => {
test.each([
/// Legacy bg-gradient-* → bg-linear-*
['bg-gradient-to-t', 'bg-linear-to-t'],
['bg-gradient-to-tr', 'bg-linear-to-tr'],
['bg-gradient-to-r', 'bg-linear-to-r'],
['bg-gradient-to-br', 'bg-linear-to-br'],
['bg-gradient-to-b', 'bg-linear-to-b'],
['bg-gradient-to-bl', 'bg-linear-to-bl'],
['bg-gradient-to-l', 'bg-linear-to-l'],
['bg-gradient-to-tl', 'bg-linear-to-tl'],
])(testName, { timeout }, async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
`,
candidate,
expected,
)
`
await expectCanonicalization(input, candidate, expected)
})
let deprecated: [string, string][] = [
['order-none', 'order-0'],
['break-words', 'wrap-break-word'],
['overflow-ellipsis', 'text-ellipsis'],
['start-full', 'inset-s-full'],
['-start-full', '-inset-s-full'],
['start-auto', 'inset-s-auto'],
['start-px', 'inset-s-px'],
['-start-px', '-inset-s-px'],
['start-8', 'inset-s-8'], // Within default spacing scale
['-start-8', '-inset-s-8'], // Within default spacing scale
['start-123', 'inset-s-123'], // Outside of default spacing scale
['-start-123', '-inset-s-123'], // Outside of default spacing scale
['end-full', 'inset-e-full'],
['-end-full', '-inset-e-full'],
['end-auto', 'inset-e-auto'],
['end-px', 'inset-e-px'],
['-end-px', '-inset-e-px'],
['end-8', 'inset-e-8'], // Within default spacing scale
['-end-8', '-inset-e-8'], // Within default spacing scale
['end-123', 'inset-e-123'], // Outside of default spacing scale
['-end-123', '-inset-e-123'], // Outside of default spacing scale
]
test.each(deprecated)(testName, { timeout }, async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
`
await expectCanonicalization(input, candidate, expected)
})
describe('With custom implementation', () => {
// Creating a shared CSS file such that we can re-use the same design
// system for all of these.
let customImplementations = deprecated
.map(
([candidate]) => css`
@utility ${candidate} {
--custom-${randomUUID()}: implementation;
}
`,
)
.join('\n')
// Keep the current utility because of the custom implementation
test.each(deprecated.map(([candidate]) => [candidate, candidate]))(
testName,
{ timeout },
async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
${customImplementations}
`
await expectCanonicalization(input, candidate, expected)
},
)
})
})
describe('arbitrary utilities', () => {
describe('arbitrary properties', () => {
test.each([
/// theme(…) to `var(…)`
// Keep candidates that don't contain `theme(…)` or `theme(…, …)`
['[color:red]', 'text-[red]'],
// Handle special cases around `.1` in the `theme(…)`
['[--value:theme(spacing.1)]', '[--value:--spacing(1)]'],
['[--value:theme(fontSize.xs.1.lineHeight)]', '[--value:var(--text-xs--line-height)]'],
['[--value:theme(spacing[1.25])]', '[--value:--spacing(1.25)]'],
// Should not convert invalid spacing values to calc
['[--value:theme(spacing[1.1])]', '[--value:theme(spacing[1.1])]'],
// Convert to `var(…)` if we can resolve the path
['[color:theme(colors.red.500)]', 'text-red-500'], // Arbitrary property
['[color:theme(colors.red.500)]/50', 'text-red-500/50'], // Arbitrary property + modifier
// Multiple `theme(… / …)` calls should result in modern syntax of `theme(…)`
// - Can't convert to `var(…)` because that would lose the modifier.
// - Can't convert to a candidate modifier because there are multiple
// `theme(…)` calls.
//
// If we really want to, we can make a fancy migration that tries to move it
// to a candidate modifier _if_ all `theme(…)` calls use the same modifier.
[
'[color:theme(colors.red.500/50,theme(colors.blue.500/50))]',
'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]',
],
[
'[color:theme(colors.red.500/50,theme(colors.blue.500/50))]/50',
'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]/50',
],
// Convert the `theme(…)`, but try to move the inline modifier (e.g. `50%`),
// to a candidate modifier.
// Arbitrary property, with simple percentage modifier
['[color:theme(colors.red.500/75%)]', 'text-red-500/75'],
// Arbitrary property, with numbers (0-1) without a unit
['[color:theme(colors.red.500/.12)]', 'text-red-500/12'],
['[color:theme(colors.red.500/0.12)]', 'text-red-500/12'],
// Arbitrary property, with more complex modifier (we only allow whole numbers
// as bare modifiers). Convert the complex numbers to arbitrary values instead.
['[color:theme(colors.red.500/12.34%)]', 'text-red-500/[12.34%]'],
['[color:theme(colors.red.500/var(--opacity))]', 'text-red-500/(--opacity)'],
['[color:theme(colors.red.500/.12345)]', 'text-red-500/1234.5'],
['[color:theme(colors.red.500/50.25%)]', 'text-red-500/50.25'],
// Arbitrary property that already contains a modifier
['[color:theme(colors.red.500/50%)]/50', 'text-[--theme(--color-red-500/50%)]/50'],
// `calc(var(--spacing)*…)` to `--spacing(…)`
['[padding-top:min(20%,calc(var(--spacing)*8))]', 'pt-[min(20%,--spacing(8))]'],
[
'[padding-top:min(20%,calc(var(--spacing)*var(--other)))]',
'pt-[min(20%,--spacing(var(--other)))]',
],
['[padding-top:calc(var(--spacing)*8)]', 'pt-8'],
['[padding-top:calc(var(--spacing)*var(--other))]', 'pt-[--spacing(var(--other))]'],
// `theme(…)` calls valid in v3, but not in v4 should still be converted.
['[--foo:theme(transitionDuration.500)]', '[--foo:theme(transitionDuration.500)]'],
// Invalid cases
['[--foo:theme(colors.red.500/50/50)]', '[--foo:theme(colors.red.500/50/50)]'],
['[--foo:theme(colors.red.500/50/50)]/50', '[--foo:theme(colors.red.500/50/50)]/50'],
// Partially invalid cases
[
'[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]',
'[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]',
],
[
'[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]/50',
'[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]/50',
],
// If a utility sets `property` and `--tw-{property}` with the same value,
// we can ignore the `--tw-{property}`. This is just here for composition.
// This means that we should be able to upgrade the one _without_ to the one
// _with_ the variable
['[font-weight:400]', 'font-normal'],
['[line-height:0]', 'leading-0'],
['[border-style:solid]', 'border-solid'],
])(testName, { timeout }, async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
`
await expectCanonicalization(input, candidate, expected)
})
test.each([
// Arbitrary property to static utility
['[text-wrap:balance]', 'text-balance'],
@ -318,10 +305,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
['[display:_flex]', 'flex'],
['[display:flex_]', 'flex'],
// Arbitrary property to static utility
// Map number to keyword-like value
['leading-[1]', 'leading-none'],
// Arbitrary property to named functional utility
['[color:var(--color-red-500)]', 'text-red-500'],
['[background-color:var(--color-red-500)]', 'bg-red-500'],
@ -349,20 +332,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
['[grid-column:2]', 'col-2'],
['[grid-column:1234]', 'col-1234'],
// Arbitrary value to bare value
['border-[2px]', 'border-2'],
['border-[1234px]', 'border-1234'],
// Arbitrary value with data type, to more specific arbitrary value
['bg-[position:123px]', 'bg-position-[123px]'],
['bg-[size:123px]', 'bg-size-[123px]'],
// Arbitrary value with inferred data type, to more specific arbitrary value
['bg-[123px]', 'bg-position-[123px]'],
// Arbitrary value with spacing mul
['w-[64rem]', 'w-256'],
// Complex arbitrary property to arbitrary value
[
'[grid-template-columns:repeat(2,minmax(100px,1fr))]',
@ -370,13 +339,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
],
// Complex arbitrary property to bare value
['[grid-template-columns:repeat(2,minmax(0,1fr))]', 'grid-cols-2'],
// Arbitrary value to bare value with percentage
['from-[25%]', 'from-25%'],
// Arbitrary percentage value must be a whole number. Should not migrate to
// a bare value.
['from-[2.5%]', 'from-[2.5%]'],
])(testName, { timeout }, async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
@ -393,7 +355,107 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
await expectCanonicalization(input, candidate, expected)
})
})
describe('arbitrary values', () => {
test.each([
// Convert to `var(…)` if we can resolve the path
['bg-[theme(colors.red.500)]', 'bg-red-500'], // Arbitrary value
['bg-[size:theme(spacing.4)]', 'bg-size-[--spacing(4)]'], // Arbitrary value + data type hint
// Pretty print CSS functions preceded by an operator to prevent consecutive
// operator characters.
['w-[calc(100dvh-theme(spacing.2))]', 'w-[calc(100dvh-(--spacing(2)))]'],
['w-[calc(100dvh+theme(spacing.2))]', 'w-[calc(100dvh+(--spacing(2)))]'],
['w-[calc(100dvh/theme(spacing.2))]', 'w-[calc(100dvh/(--spacing(2)))]'],
['w-[calc(100dvh*theme(spacing.2))]', 'w-[calc(100dvh*(--spacing(2)))]'],
// Convert to `var(…)` if we can resolve the path, but keep fallback values
['bg-[theme(colors.red.500,red)]', 'bg-(--color-red-500,red)'],
// Keep `theme(…)` if we can't resolve the path
['bg-[theme(colors.foo.1000)]', 'bg-[theme(colors.foo.1000)]'],
// Keep `theme(…)` if we can't resolve the path, but still try to convert the
// fallback value.
['bg-[theme(colors.foo.1000,theme(colors.red.500))]', 'bg-red-500'],
// Use `theme(…)` (deeply nested) inside of a `calc(…)` function
['text-[calc(theme(fontSize.xs)*2)]', 'text-[calc(var(--text-xs)*2)]'],
// Arbitrary value
['bg-[theme(colors.red.500/75%)]', 'bg-red-500/75'],
['bg-[theme(colors.red.500/12.34%)]', 'bg-red-500/[12.34%]'],
// Values that don't contain only `theme(…)` calls should not be converted to
// use a modifier since the color is not the whole value.
[
'shadow-[shadow:inset_0px_1px_theme(colors.white/15%)]',
'shadow-[inset_0px_1px_--theme(--color-white/15%)]',
],
// Arbitrary value, where the candidate already contains a modifier
// This should still migrate the `theme(…)` syntax to the modern syntax.
['bg-[theme(colors.red.500/50%)]/50', 'bg-[--theme(--color-red-500/50%)]/50'],
// Variants, we can't use `var(…)` especially inside of `@media(…)`. We can
// still upgrade the `theme(…)` to the modern syntax.
['max-[theme(screens.lg)]:flex', 'max-[--theme(--breakpoint-lg)]:flex'],
// There are no variables for `--spacing` multiples, so we can't convert this
['max-[theme(spacing.4)]:flex', 'max-[theme(spacing.4)]:flex'],
// This test in itself doesn't make much sense. But we need to make sure
// that this doesn't end up as the modifier in the candidate itself.
['max-[theme(spacing.4/50)]:flex', 'max-[theme(spacing.4/50)]:flex'],
// `theme(…)` calls in another CSS function is replaced correctly.
// Additionally we remove unnecessary whitespace.
['grid-cols-[min(50%_,_theme(spacing.80))_auto]', 'grid-cols-[min(50%,--spacing(80))_auto]'],
// `calc(var(--spacing)*…)` to `--spacing(…)`
['pt-[min(20%,calc(var(--spacing)*8))]', 'pt-[min(20%,--spacing(8))]'],
['pt-[min(20%,calc(var(--spacing)*var(--other)))]', 'pt-[min(20%,--spacing(var(--other)))]'],
['pt-[calc(var(--spacing)*8)]', 'pt-8'],
['pt-[calc(var(--spacing)*var(--other))]', 'pt-[--spacing(var(--other))]'],
// Renamed theme keys
['max-w-[theme(screens.md)]', 'max-w-(--breakpoint-md)'],
['w-[theme(maxWidth.md)]', 'w-md'],
// Arbitrary property to static utility
// Map number to keyword-like value
['leading-[1]', 'leading-none'],
// Arbitrary value to bare value
['border-[2px]', 'border-2'],
['border-[1234px]', 'border-1234'],
// Arbitrary value with data type, to more specific arbitrary value
['bg-[position:123px]', 'bg-position-[123px]'],
['bg-[size:123px]', 'bg-size-[123px]'],
// Arbitrary value with inferred data type, to more specific arbitrary value
['bg-[123px]', 'bg-position-[123px]'],
// Arbitrary value with spacing mul
['w-[64rem]', 'w-256'],
// Arbitrary value to bare value with percentage
['from-[25%]', 'from-25%'],
// Arbitrary percentage value must be a whole number. Should not migrate to
// a bare value.
['from-[2.5%]', 'from-[2.5%]'],
])(testName, { timeout }, async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
`
await expectCanonicalization(input, candidate, expected)
})
})
describe('arbitrary utilities', () => {
test('migrate with custom static utility `@utility custom {…}`', { timeout }, async () => {
let candidate = '[--key:value]'
let expected = 'custom'
@ -611,71 +673,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
})
})
describe('deprecated utilities', () => {
let deprecated: [string, string][] = [
['order-none', 'order-0'],
['break-words', 'wrap-break-word'],
['overflow-ellipsis', 'text-ellipsis'],
['start-full', 'inset-s-full'],
['-start-full', '-inset-s-full'],
['start-auto', 'inset-s-auto'],
['start-px', 'inset-s-px'],
['-start-px', '-inset-s-px'],
['start-8', 'inset-s-8'], // Within default spacing scale
['-start-8', '-inset-s-8'], // Within default spacing scale
['start-123', 'inset-s-123'], // Outside of default spacing scale
['-start-123', '-inset-s-123'], // Outside of default spacing scale
['end-full', 'inset-e-full'],
['-end-full', '-inset-e-full'],
['end-auto', 'inset-e-auto'],
['end-px', 'inset-e-px'],
['-end-px', '-inset-e-px'],
['end-8', 'inset-e-8'], // Within default spacing scale
['-end-8', '-inset-e-8'], // Within default spacing scale
['end-123', 'inset-e-123'], // Outside of default spacing scale
['-end-123', '-inset-e-123'], // Outside of default spacing scale
]
test.each(deprecated)(testName, { timeout }, async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
`
await expectCanonicalization(input, candidate, expected)
})
describe('With custom implementation', () => {
// Creating a shared CSS file such that we can re-use the same design
// system for all of these.
let customImplementations = deprecated
.map(
([candidate]) => css`
@utility ${candidate} {
--custom-${randomUUID()}: implementation;
}
`,
)
.join('\n')
// Keep the current utility because of the custom implementation
test.each(deprecated.map(([candidate]) => [candidate, candidate]))(
testName,
{ timeout },
async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
${customImplementations}
`
await expectCanonicalization(input, candidate, expected)
},
)
})
})
describe('arbitrary variants', () => {
let input = css`
@import 'tailwindcss';
@ -1096,7 +1093,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
let input = css`
@import 'tailwindcss';
`
await expectCombinedCanonicalization(input, candidates, expected)
await expectCanonicalization(input, candidates, expected)
})
})
@ -1130,7 +1127,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
let input = css`
@import 'tailwindcss';
`
await expectCombinedCanonicalization(input, candidates.trim(), expected)
await expectCanonicalization(input, candidates.trim(), expected)
})
})
@ -1242,115 +1239,120 @@ describe('options', () => {
})
})
// https://github.com/schoero/eslint-plugin-better-tailwindcss/issues/321
test('a subset of classes should be canonicalizable', { timeout }, async () => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
describe('regressions', () => {
// https://github.com/schoero/eslint-plugin-better-tailwindcss/issues/321
{
test('a subset of classes should be canonicalizable', { timeout }, async () => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
}
expect(
designSystem.canonicalizeCandidates(['underline', 'h-4', 'w-4', 'text-sm'], options),
).toEqual(['underline', 'text-sm', 'size-4'])
})
test('collapse canonicalization is not affected by previous calls', { timeout }, async () => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
}
let target = ['underline', 'h-4', 'w-4']
expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4'])
designSystem.canonicalizeCandidates(['mb-4', 'text-sm'], options)
designSystem.canonicalizeCandidates(['underline', 'mb-4'], options)
expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4'])
expect(designSystem.canonicalizeCandidates(target.concat('text-sm'), options)).toEqual([
'underline',
'text-sm',
'size-4',
])
})
}
expect(
designSystem.canonicalizeCandidates(['underline', 'h-4', 'w-4', 'text-sm'], options),
).toEqual(['underline', 'text-sm', 'size-4'])
// https://github.com/tailwindlabs/tailwindcss/pull/19727
test(
'collapse does not crash when utilities with no standard properties are present',
{ timeout },
async () => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
}
// Shadow utilities use CSS custom properties and @property rules but may
// produce empty property maps in the collapse algorithm. This should not
// crash with "Cannot read properties of null" or "X is not iterable".
expect(() =>
designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options),
).not.toThrow()
expect(() => designSystem.canonicalizeCandidates(['shadow-md', 'p-4'], options)).not.toThrow()
expect(() =>
designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options),
).not.toThrow()
// Verify the candidates are returned (not collapsed, since shadows can't
// meaningfully collapse with unrelated utilities)
expect(designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options)).toEqual(
expect.arrayContaining(['shadow-sm', 'border']),
)
expect(designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options)).toEqual(
expect.arrayContaining(['shadow-sm', 'shadow-md']),
)
},
)
// https://github.com/tailwindlabs/tailwindcss/issues/19835
test.each([
// Arbitrary values should be collapsed to another arbitrary value
[
['px-[1.2rem]', 'py-[1.2rem]', 'text-left'],
['text-left', 'p-[1.2rem]'],
],
// Arbitrary values could also be collapsed into a bare value
[
['px-[30.75rem]', 'py-[30.75rem]', 'text-left'],
['text-left', 'p-123'],
],
])(
'collapse canonicalization works for arbitrary values',
{ timeout },
async (candidates, expected) => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
}
expect(designSystem.canonicalizeCandidates(candidates, options)).toEqual(expected)
},
)
})
test('collapse canonicalization is not affected by previous calls', { timeout }, async () => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
}
let target = ['underline', 'h-4', 'w-4']
expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4'])
designSystem.canonicalizeCandidates(['mb-4', 'text-sm'], options)
designSystem.canonicalizeCandidates(['underline', 'mb-4'], options)
expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4'])
expect(designSystem.canonicalizeCandidates(target.concat('text-sm'), options)).toEqual([
'underline',
'text-sm',
'size-4',
])
})
test(
'collapse does not crash when utilities with no standard properties are present',
{ timeout },
async () => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
}
// Shadow utilities use CSS custom properties and @property rules but may
// produce empty property maps in the collapse algorithm. This should not
// crash with "Cannot read properties of null" or "X is not iterable".
expect(() =>
designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options),
).not.toThrow()
expect(() => designSystem.canonicalizeCandidates(['shadow-md', 'p-4'], options)).not.toThrow()
expect(() =>
designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options),
).not.toThrow()
// Verify the candidates are returned (not collapsed, since shadows can't
// meaningfully collapse with unrelated utilities)
expect(designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options)).toEqual(
expect.arrayContaining(['shadow-sm', 'border']),
)
expect(designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options)).toEqual(
expect.arrayContaining(['shadow-sm', 'shadow-md']),
)
},
)
// https://github.com/tailwindlabs/tailwindcss/issues/19835
test.each([
// Arbitrary values should be collapsed to another arbitrary value
[
['px-[1.2rem]', 'py-[1.2rem]', 'text-left'],
['text-left', 'p-[1.2rem]'],
],
// Arbitrary values could also be collapsed into a bare value
[
['px-[30.75rem]', 'py-[30.75rem]', 'text-left'],
['text-left', 'p-123'],
],
])(
'collapse canonicalization works for arbitrary values',
{ timeout },
async (candidates, expected) => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
}
expect(designSystem.canonicalizeCandidates(candidates, options)).toEqual(expected)
},
)