import { expect, test } from 'vitest'
import { __unstable__loadDesignSystem } from '.'
import { buildDesignSystem } from './design-system'
import plugin from './plugin'
import { Theme } from './theme'
const css = String.raw
function loadDesignSystem() {
let theme = new Theme()
theme.add('--spacing-0_5', '0.125rem')
theme.add('--spacing-1', '0.25rem')
theme.add('--spacing-3', '0.75rem')
theme.add('--spacing-4', '1rem')
theme.add('--width-4', '1rem')
theme.add('--colors-red-500', 'red')
theme.add('--colors-blue-500', 'blue')
theme.add('--breakpoint-sm', '640px')
theme.add('--font-size-xs', '0.75rem')
theme.add('--font-size-xs--line-height', '1rem')
theme.add('--perspective-dramatic', '100px')
theme.add('--perspective-normal', '500px')
theme.add('--opacity-background', '0.3')
return buildDesignSystem(theme)
}
test('getClassList', () => {
let design = loadDesignSystem()
let classList = design.getClassList()
let classNames = classList.flatMap(([name, meta]) => [
name,
...meta.modifiers.map((m) => `${name}/${m}`),
])
expect(classNames).toMatchSnapshot()
})
test('Theme values with underscores are converted back to decimal points', () => {
let design = loadDesignSystem()
let classes = design.getClassList()
expect(classes).toContainEqual(['inset-0.5', { modifiers: [] }])
})
test('getVariants', () => {
let design = loadDesignSystem()
let variants = design.getVariants()
expect(variants).toMatchSnapshot()
})
test('getVariants compound', () => {
let design = loadDesignSystem()
let variants = design.getVariants()
let group = variants.find((v) => v.name === 'group')!
let list = [
group.selectors({ value: 'hover' }),
group.selectors({ value: 'hover', modifier: 'sidebar' }),
group.selectors({ value: 'group-hover' }),
group.selectors({ value: 'sm' }),
group.selectors({ value: 'md' }),
]
expect(list).toEqual([
['@media (hover: hover) { &:is(:where(.group):hover *) }'],
['@media (hover: hover) { &:is(:where(.group\\/sidebar):hover *) }'],
['@media (hover: hover) { &:is(:where(.group):is(:where(.group):hover *) *) }'],
[],
[],
])
})
test('variant selectors are in the correct order', async () => {
let input = css`
@variant overactive {
&:hover {
@media (hover: hover) {
&:focus {
&:active {
@slot;
}
}
}
}
}
`
let design = await __unstable__loadDesignSystem(input)
let variants = design.getVariants()
let overactive = variants.find((v) => v.name === 'overactive')!
expect(overactive).toBeTruthy()
expect(overactive.selectors({})).toMatchInlineSnapshot(`
[
"@media (hover: hover) { &:hover { &:focus { &:active } } }",
]
`)
})
test('The variant `has-force` does not crash', () => {
let design = loadDesignSystem()
let variants = design.getVariants()
let has = variants.find((v) => v.name === 'has')!
expect(has.selectors({ value: 'force' })).toMatchInlineSnapshot(`[]`)
})
test('Can produce CSS per candidate using `candidatesToCss`', () => {
let design = loadDesignSystem()
design.invalidCandidates = new Set(['bg-[#fff]'])
expect(design.candidatesToCss(['underline', 'i-dont-exist', 'bg-[#fff]', 'bg-[#000]']))
.toMatchInlineSnapshot(`
[
".underline {
text-decoration-line: underline;
}
",
null,
null,
".bg-\\[\\#000\\] {
background-color: #000;
}
",
]
`)
})
test('Utilities do not show wrapping selector in intellisense', async () => {
let input = css`
@import 'tailwindcss/utilities';
@config './config.js';
`
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
base,
content: '@tailwind utilities;',
}),
loadModule: async () => ({
base: '',
module: {
important: '#app',
},
}),
})
expect(design.candidatesToCss(['underline', 'hover:line-through'])).toMatchInlineSnapshot(`
[
".underline {
text-decoration-line: underline;
}
",
".hover\\:line-through {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through;
}
}
}
",
]
`)
})
test('Utilities, when marked as important, show as important in intellisense', async () => {
let input = css`
@import 'tailwindcss/utilities' important;
`
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
base,
content: '@tailwind utilities;',
}),
})
expect(design.candidatesToCss(['underline', 'hover:line-through'])).toMatchInlineSnapshot(`
[
".underline {
text-decoration-line: underline !important;
}
",
".hover\\:line-through {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through !important;
}
}
}
",
]
`)
})
test('Static utilities from plugins are listed in hovers and completions', async () => {
let input = css`
@import 'tailwindcss/utilities';
@plugin "./plugin.js"l;
`
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
base,
content: '@tailwind utilities;',
}),
loadModule: async () => ({
base: '',
module: plugin(({ addUtilities }) => {
addUtilities({
'.custom-utility': {
color: 'red',
},
})
}),
}),
})
expect(design.candidatesToCss(['custom-utility'])).toMatchInlineSnapshot(`
[
".custom-utility {
color: red;
}
",
]
`)
expect(design.getClassList().map((entry) => entry[0])).toContain('custom-utility')
})
test('Functional utilities from plugins are listed in hovers and completions', async () => {
let input = css`
@import 'tailwindcss/utilities';
@plugin "./plugin.js"l;
`
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
base,
content: '@tailwind utilities;',
}),
loadModule: async () => ({
base: '',
module: plugin(({ matchUtilities }) => {
matchUtilities(
{
'custom-1': (value) => ({
color: value,
}),
},
{
values: {
red: '#ff0000',
green: '#ff0000',
},
},
)
matchUtilities(
{
'custom-2': (value, { modifier }) => ({
color: `${value} / ${modifier ?? '0%'}`,
}),
},
{
values: {
red: '#ff0000',
green: '#ff0000',
},
modifiers: {
'50': '50%',
'75': '75%',
},
},
)
matchUtilities(
{
'custom-3': (value, { modifier }) => ({
color: `${value} / ${modifier ?? '0%'}`,
}),
},
{
values: {
red: '#ff0000',
green: '#ff0000',
},
modifiers: 'any',
},
)
}),
}),
})
expect(design.candidatesToCss(['custom-1-red', 'custom-1-green', 'custom-1-unknown']))
.toMatchInlineSnapshot(`
[
".custom-1-red {
color: #ff0000;
}
",
".custom-1-green {
color: #ff0000;
}
",
null,
]
`)
expect(design.candidatesToCss(['custom-2-red', 'custom-2-green', 'custom-2-unknown']))
.toMatchInlineSnapshot(`
[
".custom-2-red {
color: #ff0000 / 0%;
}
",
".custom-2-green {
color: #ff0000 / 0%;
}
",
null,
]
`)
expect(design.candidatesToCss(['custom-2-red/50', 'custom-2-red/75', 'custom-2-red/unknown']))
.toMatchInlineSnapshot(`
[
".custom-2-red\\/50 {
color: #ff0000 / 50%;
}
",
".custom-2-red\\/75 {
color: #ff0000 / 75%;
}
",
null,
]
`)
let classMap = new Map(design.getClassList())
let classNames = Array.from(classMap.keys())
expect(classNames).toContain('custom-1-red')
expect(classMap.get('custom-1-red')?.modifiers).toEqual([])
expect(classNames).toContain('custom-1-green')
expect(classMap.get('custom-1-green')?.modifiers).toEqual([])
expect(classNames).not.toContain('custom-1-unknown')
expect(classNames).toContain('custom-2-red')
expect(classMap.get('custom-2-red')?.modifiers).toEqual(['50', '75'])
expect(classNames).toContain('custom-2-green')
expect(classMap.get('custom-2-green')?.modifiers).toEqual(['50', '75'])
expect(classNames).not.toContain('custom-2-unknown')
expect(classNames).toContain('custom-3-red')
expect(classMap.get('custom-3-red')?.modifiers).toEqual([])
expect(classNames).toContain('custom-3-green')
expect(classMap.get('custom-3-green')?.modifiers).toEqual([])
expect(classNames).not.toContain('custom-3-unknown')
})
test('Custom at-rule variants do not show up as a value under `group`', async () => {
let input = css`
@import 'tailwindcss/utilities';
@variant variant-1 (@media foo);
@variant variant-2 {
@media bar {
@slot;
}
}
@plugin "./plugin.js";
`
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
base,
content: '@tailwind utilities;',
}),
loadModule: async () => ({
base: '',
module: plugin(({ addVariant }) => {
addVariant('variant-3', '@media baz')
addVariant('variant-4', ['@media qux', '@media cat'])
}),
}),
})
let variants = design.getVariants()
let v1 = variants.find((v) => v.name === 'variant-1')!
let v2 = variants.find((v) => v.name === 'variant-2')!
let v3 = variants.find((v) => v.name === 'variant-3')!
let v4 = variants.find((v) => v.name === 'variant-4')!
let group = variants.find((v) => v.name === 'group')!
let not = variants.find((v) => v.name === 'not')!
expect(v1).not.toBeUndefined()
expect(v2).not.toBeUndefined()
expect(v3).not.toBeUndefined()
expect(v4).not.toBeUndefined()
expect(group).not.toBeUndefined()
expect(not).not.toBeUndefined()
expect(group.values).not.toContain('variant-1')
expect(group.values).not.toContain('variant-2')
expect(group.values).not.toContain('variant-3')
expect(group.values).not.toContain('variant-4')
expect(not.values).toContain('variant-1')
expect(not.values).toContain('variant-2')
expect(not.values).toContain('variant-3')
expect(not.values).toContain('variant-4')
})