tailwindcss/tests/format-variant-selector.test.js
Jordan Pittman 7d8eb21de6
Don't prefix classes in arbitrary variants (#10214)
* Add tests

* Refactor

refactor

* Allow `prefixSelector` to take an AST

* Consider multiple formats in `finalizeSelector`

The functions `finalizeSelector` and `formatVariantSelector` together were using a mix for AST and string-based parsing. This now does the full transformation using the selector AST. This also parses the format strings AST as early as possible and is set up to parse them only once for a given set of rules.

All of this will allow considering metadata per format string. For instance, we now know if the format string `.foo &` was produced by a normal variant or by an arbitrary variant. We use this information to control the prefixing behavior for individual format strings.

* Update changelog

* Cleanup code a bit
2023-01-03 09:40:47 -05:00

357 lines
13 KiB
JavaScript

import { finalizeSelector } from '../src/util/formatVariantSelector'
it('should be possible to add a simple variant to a simple selector', () => {
let selector = '.text-center'
let candidate = 'hover:text-center'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual('.hover\\:text-center:hover')
})
it('should be possible to add a multiple simple variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'focus:hover:text-center'
let formats = [
{ format: '&:hover', isArbitraryVariant: false },
{ format: '&:focus', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.focus\\:hover\\:text-center:hover:focus'
)
})
it('should be possible to add a simple variant to a selector containing escaped parts', () => {
let selector = '.bg-\\[rgba\\(0\\,0\\,0\\)\\]'
let candidate = 'hover:bg-[rgba(0,0,0)]'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]:hover'
)
})
it('should be possible to add a simple variant to a selector containing escaped parts (escape is slightly different)', () => {
let selector = '.bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]'
let candidate = 'hover:bg-[rgba(0,0,0)]'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]:hover'
)
})
it('should be possible to add a simple variant to a more complex selector', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'hover:space-x-4'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:space-x-4:hover > :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple simple variants to a more complex selector', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'disabled:focus:hover:space-x-4'
let formats = [
{ format: '&:hover', isArbitraryVariant: false },
{ format: '&:focus', isArbitraryVariant: false },
{ format: '&:disabled', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.disabled\\:focus\\:hover\\:space-x-4:hover:focus:disabled > :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add a single merge variant to a simple selector', () => {
let selector = '.text-center'
let candidate = 'group-hover:text-center'
let formats = [{ format: ':merge(.group):hover &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .group-hover\\:text-center'
)
})
it('should be possible to add multiple merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'group-focus:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:focus:hover .group-focus\\:group-hover\\:text-center'
)
})
it('should be possible to add a single merge variant to a more complex selector', () => {
let selector = '.space-x-4 ~ :not([hidden]) ~ :not([hidden])'
let candidate = 'group-hover:space-x-4'
let formats = [{ format: ':merge(.group):hover &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .group-hover\\:space-x-4 ~ :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple merge variants to a more complex selector', () => {
let selector = '.space-x-4 ~ :not([hidden]) ~ :not([hidden])'
let candidate = 'group-focus:group-hover:space-x-4'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:focus:hover .group-focus\\:group-hover\\:space-x-4 ~ :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple unique merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'peer-focus:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.peer):focus ~ &' },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:focus ~ .group:hover .peer-focus\\:group-hover\\:text-center'
)
})
it('should be possible to add multiple unique merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'group-hover:peer-focus:text-center'
let formats = [
{ format: ':merge(.peer):focus ~ &', isArbitraryVariant: false },
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .peer:focus ~ .group-hover\\:peer-focus\\:text-center'
)
})
it('should be possible to use multiple :merge() calls with different "arguments"', () => {
let selector = '.foo'
let candidate = 'peer-focus:group-focus:peer-hover:group-hover:foo'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.peer):hover ~ &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
{ format: ':merge(.peer):focus ~ &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:focus:hover ~ .group:focus:hover .peer-focus\\:group-focus\\:peer-hover\\:group-hover\\:foo'
)
})
it('group hover and prose headings combination', () => {
let selector = '.text-center'
let candidate = 'group-hover:prose-headings:text-center'
let formats = [
{ format: ':where(&) :is(h1, h2, h3, h4)', isArbitraryVariant: false }, // Prose Headings
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group Hover
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover :where(.group-hover\\:prose-headings\\:text-center) :is(h1, h2, h3, h4)'
)
})
it('group hover and prose headings combination flipped', () => {
let selector = '.text-center'
let candidate = 'prose-headings:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group Hover
{ format: ':where(&) :is(h1, h2, h3, h4)', isArbitraryVariant: false }, // Prose Headings
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
':where(.group:hover .prose-headings\\:group-hover\\:text-center) :is(h1, h2, h3, h4)'
)
})
it('should be possible to handle a complex utility', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'peer-disabled:peer-first-child:group-hover:group-focus:focus:hover:space-x-4'
let formats = [
{ format: '&:hover', isArbitraryVariant: false }, // Hover
{ format: '&:focus', isArbitraryVariant: false }, // Focus
{ format: ':merge(.group):focus &', isArbitraryVariant: false }, // Group focus
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group hover
{ format: ':merge(.peer):first-child ~ &', isArbitraryVariant: false }, // Peer first-child
{ format: ':merge(.peer):disabled ~ &', isArbitraryVariant: false }, // Peer disabled
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:disabled:first-child ~ .group:hover:focus .peer-disabled\\:peer-first-child\\:group-hover\\:group-focus\\:focus\\:hover\\:space-x-4:hover:focus > :not([hidden]) ~ :not([hidden])'
)
})
it('should match base utilities that are prefixed', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = 'tw-text-center'
let formats = []
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual('.tw-text-center')
})
it('should prefix classes from variants', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = 'foo:tw-text-center'
let formats = [{ format: '.foo &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual(
'.tw-foo .foo\\:tw-text-center'
)
})
it('should not prefix classes from arbitrary variants', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = '[.foo_&]:tw-text-center'
let formats = [{ format: '.foo &', isArbitraryVariant: true }]
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual(
'.foo .\\[\\.foo_\\&\\]\\:tw-text-center'
)
})
it('Merged selectors with mixed combinators uses the first one', () => {
// This isn't explicitly specced behavior but it is how it works today
let selector = '.text-center'
let candidate = 'text-center'
let formats = [
{ format: ':merge(.group):focus > &', isArbitraryVariant: true },
{ format: ':merge(.group):hover &', isArbitraryVariant: true },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover:focus > .text-center'
)
})
describe('real examples', () => {
it('example a', () => {
let selector = '.placeholder-red-500::placeholder'
let candidate = 'hover:placeholder-red-500'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:placeholder-red-500:hover::placeholder'
)
})
it('example b', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'group-hover:hover:space-x-4'
let formats = [
{ format: '&:hover', isArbitraryVariant: false },
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .group-hover\\:hover\\:space-x-4:hover > :not([hidden]) ~ :not([hidden])'
)
})
it('should work for group-hover and class dark mode combinations', () => {
let selector = '.text-center'
let candidate = 'dark:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: '.dark &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.dark .group:hover .dark\\:group-hover\\:text-center'
)
})
it('should work for group-hover and class dark mode combinations (reversed)', () => {
let selector = '.text-center'
let candidate = 'group-hover:dark:text-center'
let formats = [
{ format: '.dark &' },
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .dark .group-hover\\:dark\\:text-center'
)
})
describe('prose-headings', () => {
it('should be possible to use hover:prose-headings:text-center', () => {
let selector = '.text-center'
let candidate = 'hover:prose-headings:text-center'
let formats = [{ format: ':where(&) :is(h1, h2, h3, h4)' }, { format: '&:hover' }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
':where(.hover\\:prose-headings\\:text-center) :is(h1, h2, h3, h4):hover'
)
})
it('should be possible to use prose-headings:hover:text-center', () => {
let selector = '.text-center'
let candidate = 'prose-headings:hover:text-center'
let formats = [{ format: '&:hover' }, { format: ':where(&) :is(h1, h2, h3, h4)' }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
':where(.prose-headings\\:hover\\:text-center:hover) :is(h1, h2, h3, h4)'
)
})
})
})
describe('pseudo elements', () => {
it.each`
before | after
${'&::before'} | ${'&::before'}
${'&::before:hover'} | ${'&:hover::before'}
${'&:before:hover'} | ${'&:hover:before'}
${'&::file-selector-button:hover'} | ${'&::file-selector-button:hover'}
${'&:hover::file-selector-button'} | ${'&:hover::file-selector-button'}
${'.parent:hover &'} | ${'.parent:hover &'}
${'.parent::before &'} | ${'.parent &::before'}
${'.parent::before &:hover'} | ${'.parent &:hover::before'}
${':where(&::before) :is(h1, h2, h3, h4)'} | ${':where(&) :is(h1, h2, h3, h4)::before'}
${':where(&::file-selector-button) :is(h1, h2, h3, h4)'} | ${':where(&::file-selector-button) :is(h1, h2, h3, h4)'}
`('should translate "$before" into "$after"', ({ before, after }) => {
let result = finalizeSelector('.a', [{ format: before, isArbitraryVariant: false }], {
candidate: 'a',
})
expect(result).toEqual(after.replace('&', '.a'))
})
})