tailwindcss/tests/processPlugins.test.js
Robin Malfait bfc61625d9
[WIP] Unify JIT and AOT code paths (#4188)
* WIP

* WIP

* Finish combining JIT and AOT plugins

Still lots of clean up that can be done in some of the more complex ones, but at least it's one file per plugin now.

* Remove unused import

* Fix AOT generation bugs

* Move corePlugins/index.js to corePlugins.js

* Convert JIT files to ESM

* Move tests

* Reorder core plugins to match JIT order

* Update AOT apply tests

* Unify utils

* Combine plugin lists to one single source of truth

* Finish resolving merge conflicts, fix tests

Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2021-04-30 10:58:40 -04:00

2486 lines
51 KiB
JavaScript

import _ from 'lodash'
import _postcss from 'postcss'
import tailwind from '../src/index'
import processPlugins from '../src/util/processPlugins'
import createPlugin from '../src/util/createPlugin'
function css(nodes) {
return _postcss.root({ nodes }).toString()
}
function makeConfig(overrides) {
return _.defaultsDeep(overrides, {
prefix: '',
important: false,
separator: ':',
})
}
test('plugins can create utilities with object syntax', () => {
const { components, utilities } = processPlugins(
[
function ({ addUtilities }) {
addUtilities({
'.object-fill': {
'object-fit': 'fill',
},
'.object-contain': {
'object-fit': 'contain',
},
'.object-cover': {
'object-fit': 'cover',
},
})
},
],
makeConfig()
)
expect(components.length).toBe(0)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.object-fill {
object-fit: fill
}
.object-contain {
object-fit: contain
}
.object-cover {
object-fit: cover
}
}
}
`)
})
test('plugins can create utilities with arrays of objects', () => {
const { components, utilities } = processPlugins(
[
function ({ addUtilities }) {
addUtilities([
{
'.object-fill': {
'object-fit': 'fill',
},
},
{
'.object-contain': {
'object-fit': 'contain',
},
},
{
'.object-cover': {
'object-fit': 'cover',
},
},
])
},
],
makeConfig()
)
expect(components.length).toBe(0)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.object-fill {
object-fit: fill
}
.object-contain {
object-fit: contain
}
.object-cover {
object-fit: cover
}
}
}
`)
})
test('plugins can create utilities with raw PostCSS nodes', () => {
const { components, utilities } = processPlugins(
[
function ({ addUtilities, postcss }) {
addUtilities([
postcss.rule({ selector: '.object-fill' }).append([
postcss.decl({
prop: 'object-fit',
value: 'fill',
}),
]),
postcss.rule({ selector: '.object-contain' }).append([
postcss.decl({
prop: 'object-fit',
value: 'contain',
}),
]),
postcss.rule({ selector: '.object-cover' }).append([
postcss.decl({
prop: 'object-fit',
value: 'cover',
}),
]),
])
},
],
makeConfig()
)
expect(components.length).toBe(0)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.object-fill {
object-fit: fill
}
.object-contain {
object-fit: contain
}
.object-cover {
object-fit: cover
}
}
}
`)
})
test('plugins can create utilities with mixed object styles and PostCSS nodes', () => {
const { components, utilities } = processPlugins(
[
function ({ addUtilities, postcss }) {
addUtilities([
{
'.object-fill': {
objectFit: 'fill',
},
},
postcss.rule({ selector: '.object-contain' }).append([
postcss.decl({
prop: 'object-fit',
value: 'contain',
}),
]),
postcss.rule({ selector: '.object-cover' }).append([
postcss.decl({
prop: 'object-fit',
value: 'cover',
}),
]),
])
},
],
makeConfig()
)
expect(components.length).toBe(0)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.object-fill {
object-fit: fill
}
.object-contain {
object-fit: contain
}
.object-cover {
object-fit: cover
}
}
}
`)
})
test('plugins can create utilities with variants', () => {
const { components, utilities } = processPlugins(
[
function ({ addUtilities }) {
addUtilities(
{
'.object-fill': {
'object-fit': 'fill',
},
'.object-contain': {
'object-fit': 'contain',
},
'.object-cover': {
'object-fit': 'cover',
},
},
['responsive', 'hover', 'group-hover', 'focus']
)
},
],
makeConfig()
)
expect(components.length).toBe(0)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants responsive, hover, group-hover, focus {
.object-fill {
object-fit: fill
}
.object-contain {
object-fit: contain
}
.object-cover {
object-fit: cover
}
}
}
`)
})
test('plugins can create components with object syntax', () => {
const { components, utilities } = processPlugins(
[
function ({ addComponents }) {
addComponents({
'.btn-blue': {
backgroundColor: 'blue',
color: 'white',
padding: '.5rem 1rem',
borderRadius: '.25rem',
},
'.btn-blue:hover': {
backgroundColor: 'darkblue',
},
})
},
],
makeConfig()
)
expect(utilities.length).toBe(0)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.btn-blue {
background-color: blue;
color: white;
padding: .5rem 1rem;
border-radius: .25rem
}
.btn-blue:hover {
background-color: darkblue
}
}
}
`)
})
test('plugins can add base styles with object syntax', () => {
const { base } = processPlugins(
[
function ({ addBase }) {
addBase({
img: {
maxWidth: '100%',
},
button: {
fontFamily: 'inherit',
},
})
},
],
makeConfig()
)
expect(css(base)).toMatchCss(`
@layer base {
img {
max-width: 100%
}
button {
font-family: inherit
}
}
`)
})
test('plugins can add base styles with raw PostCSS nodes', () => {
const { base } = processPlugins(
[
function ({ addBase, postcss }) {
addBase([
postcss.rule({ selector: 'img' }).append([
postcss.decl({
prop: 'max-width',
value: '100%',
}),
]),
postcss.rule({ selector: 'button' }).append([
postcss.decl({
prop: 'font-family',
value: 'inherit',
}),
]),
])
},
],
makeConfig()
)
expect(css(base)).toMatchCss(`
@layer base {
img {
max-width: 100%
}
button {
font-family: inherit
}
}
`)
})
test('plugins can create components with raw PostCSS nodes', () => {
const { components, utilities } = processPlugins(
[
function ({ addComponents, postcss }) {
addComponents([
postcss.rule({ selector: '.btn-blue' }).append([
postcss.decl({
prop: 'background-color',
value: 'blue',
}),
postcss.decl({
prop: 'color',
value: 'white',
}),
postcss.decl({
prop: 'padding',
value: '.5rem 1rem',
}),
postcss.decl({
prop: 'border-radius',
value: '.25rem',
}),
]),
postcss.rule({ selector: '.btn-blue:hover' }).append([
postcss.decl({
prop: 'background-color',
value: 'darkblue',
}),
]),
])
},
],
makeConfig()
)
expect(utilities.length).toBe(0)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.btn-blue {
background-color: blue;
color: white;
padding: .5rem 1rem;
border-radius: .25rem
}
.btn-blue:hover {
background-color: darkblue
}
}
}
`)
})
test('plugins can create components with mixed object styles and raw PostCSS nodes', () => {
const { components, utilities } = processPlugins(
[
function ({ addComponents, postcss }) {
addComponents([
postcss.rule({ selector: '.btn-blue' }).append([
postcss.decl({
prop: 'background-color',
value: 'blue',
}),
postcss.decl({
prop: 'color',
value: 'white',
}),
postcss.decl({
prop: 'padding',
value: '.5rem 1rem',
}),
postcss.decl({
prop: 'border-radius',
value: '.25rem',
}),
]),
{
'.btn-blue:hover': {
backgroundColor: 'darkblue',
},
},
])
},
],
makeConfig()
)
expect(utilities.length).toBe(0)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.btn-blue {
background-color: blue;
color: white;
padding: .5rem 1rem;
border-radius: .25rem
}
.btn-blue:hover {
background-color: darkblue
}
}
}
`)
})
test('plugins can create components with media queries with object syntax', () => {
const { components, utilities } = processPlugins(
[
function ({ addComponents }) {
addComponents({
'.container': {
width: '100%',
},
'@media (min-width: 100px)': {
'.container': {
maxWidth: '100px',
},
},
'@media (min-width: 200px)': {
'.container': {
maxWidth: '200px',
},
},
'@media (min-width: 300px)': {
'.container': {
maxWidth: '300px',
},
},
})
},
],
makeConfig()
)
expect(utilities.length).toBe(0)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.container {
width: 100%
}
@media (min-width: 100px) {
.container {
max-width: 100px
}
}
@media (min-width: 200px) {
.container {
max-width: 200px
}
}
@media (min-width: 300px) {
.container {
max-width: 300px
}
}
}
}
`)
})
test('media queries can be defined multiple times using objects-in-array syntax', () => {
const { components, utilities } = processPlugins(
[
function ({ addComponents }) {
addComponents([
{
'.container': {
width: '100%',
},
'@media (min-width: 100px)': {
'.container': {
maxWidth: '100px',
},
},
},
{
'.btn': {
padding: '1rem .5rem',
display: 'block',
},
'@media (min-width: 100px)': {
'.btn': {
display: 'inline-block',
},
},
},
])
},
],
makeConfig()
)
expect(utilities.length).toBe(0)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.container {
width: 100%
}
@media (min-width: 100px) {
.container {
max-width: 100px
}
}
.btn {
padding: 1rem .5rem;
display: block
}
@media (min-width: 100px) {
.btn {
display: inline-block
}
}
}
}
`)
})
test('plugins can create nested rules', () => {
const { components, utilities } = processPlugins(
[
function ({ addComponents }) {
addComponents({
'.btn-blue': {
backgroundColor: 'blue',
color: 'white',
padding: '.5rem 1rem',
borderRadius: '.25rem',
'&:hover': {
backgroundColor: 'darkblue',
},
'@media (min-width: 500px)': {
'&:hover': {
backgroundColor: 'orange',
},
},
'> a': {
color: 'red',
},
'h1 &': {
color: 'purple',
},
},
})
},
],
makeConfig()
)
expect(utilities.length).toBe(0)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.btn-blue {
background-color: blue;
color: white;
padding: .5rem 1rem;
border-radius: .25rem;
}
.btn-blue:hover {
background-color: darkblue;
}
@media (min-width: 500px) {
.btn-blue:hover {
background-color: orange;
}
}
.btn-blue > a {
color: red;
}
h1 .btn-blue {
color: purple;
}
}
}
`)
})
test('plugins can create rules with escaped selectors', () => {
const { components, utilities } = processPlugins(
[
function ({ e, addUtilities }) {
addUtilities({
[`.${e('top-1/4')}`]: {
top: '25%',
},
})
},
],
makeConfig()
)
expect(components.length).toBe(0)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.top-1\\/4 {
top: 25%
}
}
}
`)
})
test('plugins can access the current config', () => {
const { components, utilities } = processPlugins(
[
function ({ addComponents, config }) {
const containerClasses = [
{
'.container': {
width: '100%',
},
},
]
_.forEach(config('screens'), (breakpoint) => {
containerClasses.push({
[`@media (min-width: ${breakpoint})`]: {
'.container': { maxWidth: breakpoint },
},
})
})
addComponents(containerClasses)
},
],
makeConfig({
screens: {
sm: '576px',
md: '768px',
lg: '992px',
xl: '1200px',
},
})
)
expect(utilities.length).toBe(0)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.container {
width: 100%
}
@media (min-width: 576px) {
.container {
max-width: 576px
}
}
@media (min-width: 768px) {
.container {
max-width: 768px
}
}
@media (min-width: 992px) {
.container {
max-width: 992px
}
}
@media (min-width: 1200px) {
.container {
max-width: 1200px
}
}
}
}
`)
})
test('plugins can access the variants config directly', () => {
const { components, utilities } = processPlugins(
[
function ({ addUtilities, variants }) {
addUtilities(
{
'.object-fill': {
'object-fit': 'fill',
},
'.object-contain': {
'object-fit': 'contain',
},
'.object-cover': {
'object-fit': 'cover',
},
},
variants('objectFit')
)
},
],
makeConfig({
variants: {
objectFit: ['responsive', 'focus', 'hover'],
},
})
)
expect(components.length).toBe(0)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants responsive, focus, hover {
.object-fill {
object-fit: fill
}
.object-contain {
object-fit: contain
}
.object-cover {
object-fit: cover
}
}
}
`)
})
test('plugins apply all global variants when variants are configured globally', () => {
const { components, utilities } = processPlugins(
[
function ({ addUtilities, variants }) {
addUtilities(
{
'.object-fill': {
'object-fit': 'fill',
},
},
variants('objectFit')
)
addUtilities(
{
'.rotate-90deg': {
transform: 'rotate(90deg)',
},
},
variants('rotate')
)
},
],
makeConfig({
variants: ['responsive', 'focus', 'hover'],
})
)
expect(components.length).toBe(0)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants responsive, focus, hover {
.object-fill {
object-fit: fill
}
}
}
@layer utilities {
@variants responsive, focus, hover {
.rotate-90deg {
transform: rotate(90deg)
}
}
}
`)
})
test('plugins can check if corePlugins are enabled', () => {
const { components, utilities } = processPlugins(
[
function ({ addUtilities, corePlugins }) {
addUtilities({
'.test': {
'text-color': corePlugins('textColor') ? 'true' : 'false',
opacity: corePlugins('opacity') ? 'true' : 'false',
},
})
},
],
makeConfig({
corePlugins: { textColor: false },
})
)
expect(components.length).toBe(0)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.test {
text-color: false;
opacity: true
}
}
}
`)
})
test('plugins can check if corePlugins are enabled when using array white-listing', () => {
const { components, utilities } = processPlugins(
[
function ({ addUtilities, corePlugins }) {
addUtilities({
'.test': {
'text-color': corePlugins('textColor') ? 'true' : 'false',
opacity: corePlugins('opacity') ? 'true' : 'false',
},
})
},
],
makeConfig({
corePlugins: ['textColor'],
})
)
expect(components.length).toBe(0)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.test {
text-color: true;
opacity: false
}
}
}
`)
})
test('plugins can provide fallbacks to keys missing from the config', () => {
const { components, utilities } = processPlugins(
[
function ({ addComponents, config }) {
addComponents({
'.btn': {
borderRadius: config('borderRadius.default', '.25rem'),
},
})
},
],
makeConfig({
borderRadius: {
1: '1px',
2: '2px',
4: '4px',
8: '8px',
},
})
)
expect(utilities.length).toBe(0)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.btn {
border-radius: .25rem
}
}
}
`)
})
test('variants are optional when adding utilities', () => {
const { utilities } = processPlugins(
[
function ({ addUtilities }) {
addUtilities({
'.border-collapse': {
'border-collapse': 'collapse',
},
})
},
],
makeConfig()
)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.border-collapse {
border-collapse: collapse
}
}
}
`)
})
test('plugins can add multiple sets of utilities and components', () => {
const { components, utilities } = processPlugins(
[
function ({ addUtilities, addComponents }) {
addComponents({
'.card': {
padding: '1rem',
borderRadius: '.25rem',
},
})
addUtilities({
'.skew-12deg': {
transform: 'skewY(-12deg)',
},
})
addComponents({
'.btn': {
padding: '1rem .5rem',
display: 'inline-block',
},
})
addUtilities({
'.border-collapse': {
borderCollapse: 'collapse',
},
})
},
],
makeConfig()
)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.skew-12deg {
transform: skewY(-12deg)
}
}
}
@layer utilities {
@variants {
.border-collapse {
border-collapse: collapse
}
}
}
`)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.card {
padding: 1rem;
border-radius: .25rem
}
}
}
@layer components {
@variants {
.btn {
padding: 1rem .5rem;
display: inline-block
}
}
}
`)
})
test('plugins respect prefix and important options by default when adding utilities', () => {
return _postcss([
tailwind({
prefix: 'tw-',
important: true,
corePlugins: [],
plugins: [
function ({ addUtilities }) {
addUtilities({
'.rotate-90': {
transform: 'rotate(90deg)',
},
})
},
],
}),
])
.process(
`
@tailwind utilities;
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.tw-rotate-90 {
transform: rotate(90deg) !important
}
`
expect(result.css).toMatchCss(expected)
})
})
test('when important is a selector it is used to scope utilities instead of adding !important', () => {
return _postcss([
tailwind({
prefix: 'tw-',
important: '#app',
corePlugins: [],
plugins: [
function ({ addUtilities }) {
addUtilities({
'.rotate-90': {
transform: 'rotate(90deg)',
},
})
},
],
}),
])
.process(
`
@tailwind utilities;
`,
{ from: undefined }
)
.then((result) => {
const expected = `
#app .tw-rotate-90 {
transform: rotate(90deg)
}
`
expect(result.css).toMatchCss(expected)
})
})
test('when important is a selector it scopes all selectors in a rule, even though defining utilities like this is stupid', () => {
return _postcss([
tailwind({
important: '#app',
corePlugins: [],
plugins: [
function ({ addUtilities }) {
addUtilities({
'.rotate-90, .rotate-1\\/4': {
transform: 'rotate(90deg)',
},
})
},
],
}),
])
.process(
`
@tailwind utilities;
`,
{ from: undefined }
)
.then((result) => {
const expected = `
#app .rotate-90, #app .rotate-1\\/4 {
transform: rotate(90deg)
}
`
expect(result.css).toMatchCss(expected)
})
})
test('important utilities are not made double important when important option is used', () => {
return _postcss([
tailwind({
important: true,
corePlugins: [],
plugins: [
function ({ addUtilities }) {
addUtilities({
'.rotate-90': {
transform: 'rotate(90deg) !important',
},
})
},
],
}),
])
.process(
`
@tailwind utilities;
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.rotate-90 {
transform: rotate(90deg) !important
}
`
expect(result.css).toMatchCss(expected)
})
})
test("component declarations respect the 'prefix' option by default", () => {
const { components } = processPlugins(
[
function ({ addComponents }) {
addComponents({
'.btn-blue': {
backgroundColor: 'blue',
},
})
},
],
makeConfig({
prefix: 'tw-',
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.tw-btn-blue {
background-color: blue
}
}
}
`)
})
test('all selectors in a rule are prefixed', () => {
const { utilities, components } = processPlugins(
[
function ({ addUtilities, addComponents }) {
addUtilities({
'.rotate-90, .rotate-1\\/4': {
transform: 'rotate(90deg)',
},
})
addComponents({
'.btn-blue, .btn-red': {
padding: '10px',
},
})
},
],
makeConfig({
prefix: 'tw-',
})
)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.tw-rotate-90, .tw-rotate-1\\/4 {
transform: rotate(90deg)
}
}
}
`)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.tw-btn-blue, .tw-btn-red {
padding: 10px
}
}
}
`)
})
test("component declarations can optionally ignore 'prefix' option", () => {
const { components } = processPlugins(
[
function ({ addComponents }) {
addComponents(
{
'.btn-blue': {
backgroundColor: 'blue',
},
},
{ respectPrefix: false }
)
},
],
makeConfig({
prefix: 'tw-',
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.btn-blue {
background-color: blue
}
}
}
`)
})
test('plugins can create components with variants', () => {
const { components } = processPlugins(
[
function ({ addComponents }) {
addComponents(
{
'.btn-blue': {
backgroundColor: 'blue',
},
},
{ variants: ['responsive'] }
)
},
],
makeConfig()
)
expect(css(components)).toMatchCss(`
@layer components {
@variants responsive {
.btn-blue {
background-color: blue
}
}
}
`)
})
test('plugins can use the array shorthand to add variants to components', () => {
const { components } = processPlugins(
[
function ({ addComponents }) {
addComponents(
{
'.btn-blue': {
backgroundColor: 'blue',
},
},
['responsive', 'hover']
)
},
],
makeConfig()
)
expect(css(components)).toMatchCss(`
@layer components {
@variants responsive, hover {
.btn-blue {
background-color: blue
}
}
}
`)
})
test('components that add variants manually do not add an extra variants wrapper', () => {
const { components } = processPlugins(
[
function ({ addComponents }) {
addComponents({
'@variants responsive': {
'.btn-blue': {
backgroundColor: 'blue',
},
},
})
},
],
makeConfig()
)
expect(css(components)).toMatchCss(`
@layer components {
@variants responsive {
.btn-blue {
background-color: blue
}
}
}
`)
})
test("component declarations are not affected by the 'important' option", () => {
const { components } = processPlugins(
[
function ({ addComponents }) {
addComponents({
'.btn-blue': {
backgroundColor: 'blue',
},
})
},
],
makeConfig({
important: true,
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.btn-blue {
background-color: blue
}
}
}
`)
})
test("plugins can apply the user's chosen prefix to components manually", () => {
const { components } = processPlugins(
[
function ({ addComponents, prefix }) {
addComponents(
{
[prefix('.btn-blue')]: {
backgroundColor: 'blue',
},
},
{ respectPrefix: false }
)
},
],
makeConfig({
prefix: 'tw-',
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.tw-btn-blue {
background-color: blue
}
}
}
`)
})
test('prefix can optionally be ignored for utilities', () => {
return _postcss([
tailwind({
prefix: 'tw-',
corePlugins: [],
plugins: [
function ({ addUtilities }) {
addUtilities(
{
'.rotate-90': {
transform: 'rotate(90deg)',
},
},
{
respectPrefix: false,
}
)
},
],
}),
])
.process(
`
@tailwind utilities;
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.rotate-90 {
transform: rotate(90deg)
}
`
expect(result.css).toMatchCss(expected)
})
})
test('important can optionally be ignored for utilities', () => {
return _postcss([
tailwind({
important: true,
corePlugins: [],
plugins: [
function ({ addUtilities }) {
addUtilities(
{
'.rotate-90': {
transform: 'rotate(90deg)',
},
},
{
respectImportant: false,
}
)
},
],
}),
])
.process(
`
@tailwind utilities;
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.rotate-90 {
transform: rotate(90deg)
}
`
expect(result.css).toMatchCss(expected)
})
})
test('variants can still be specified when ignoring prefix and important options', () => {
const { utilities } = processPlugins(
[
function ({ addUtilities }) {
addUtilities(
{
'.rotate-90': {
transform: 'rotate(90deg)',
},
},
{
variants: ['responsive', 'hover', 'focus'],
respectImportant: false,
respectPrefix: false,
}
)
},
],
makeConfig({
prefix: 'tw-',
important: true,
})
)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants responsive, hover, focus {
.rotate-90 {
transform: rotate(90deg)
}
}
}
`)
})
test('prefix will prefix all classes in a selector', () => {
const { components } = processPlugins(
[
function ({ addComponents, prefix }) {
addComponents(
{
[prefix('.btn-blue .w-1\\/4 > h1.text-xl + a .bar')]: {
backgroundColor: 'blue',
},
},
{ respectPrefix: false }
)
},
],
makeConfig({
prefix: 'tw-',
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.tw-btn-blue .tw-w-1\\/4 > h1.tw-text-xl + a .tw-bar {
background-color: blue
}
}
}
`)
})
test('plugins can be provided as an object with a handler function', () => {
const { components, utilities } = processPlugins(
[
{
handler({ addUtilities }) {
addUtilities({
'.object-fill': {
'object-fit': 'fill',
},
'.object-contain': {
'object-fit': 'contain',
},
'.object-cover': {
'object-fit': 'cover',
},
})
},
},
],
makeConfig()
)
expect(components.length).toBe(0)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.object-fill {
object-fit: fill
}
.object-contain {
object-fit: contain
}
.object-cover {
object-fit: cover
}
}
}
`)
})
test('plugins can provide a config but no handler', () => {
const { components, utilities } = processPlugins(
[
{
config: {
prefix: 'tw-',
},
},
{
handler({ addUtilities }) {
addUtilities({
'.object-fill': {
'object-fit': 'fill',
},
'.object-contain': {
'object-fit': 'contain',
},
'.object-cover': {
'object-fit': 'cover',
},
})
},
},
],
makeConfig()
)
expect(components.length).toBe(0)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.object-fill {
object-fit: fill
}
.object-contain {
object-fit: contain
}
.object-cover {
object-fit: cover
}
}
}
`)
})
test('plugins can be created using the `createPlugin` function', () => {
const plugin = createPlugin(
function ({ addUtilities, theme, variants }) {
const utilities = _.fromPairs(
_.toPairs(theme('testPlugin')).map(([k, v]) => [`.test-${k}`, { testProperty: v }])
)
addUtilities(utilities, variants('testPlugin'))
},
{
theme: {
testPlugin: {
sm: '1rem',
md: '2rem',
lg: '3rem',
},
},
variants: {
testPlugin: ['responsive', 'hover'],
},
}
)
return _postcss([
tailwind({
corePlugins: [],
theme: {
screens: {
sm: '400px',
},
},
plugins: [plugin],
}),
])
.process(
`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.test-sm {
test-property: 1rem
}
.test-md {
test-property: 2rem
}
.test-lg {
test-property: 3rem
}
.hover\\:test-sm:hover {
test-property: 1rem
}
.hover\\:test-md:hover {
test-property: 2rem
}
.hover\\:test-lg:hover {
test-property: 3rem
}
@media (min-width: 400px) {
.sm\\:test-sm {
test-property: 1rem
}
.sm\\:test-md {
test-property: 2rem
}
.sm\\:test-lg {
test-property: 3rem
}
.sm\\:hover\\:test-sm:hover {
test-property: 1rem
}
.sm\\:hover\\:test-md:hover {
test-property: 2rem
}
.sm\\:hover\\:test-lg:hover {
test-property: 3rem
}
}
`
expect(result.css).toMatchCss(expected)
})
})
test('plugins with extra options can be created using the `createPlugin.withOptions` function', () => {
const plugin = createPlugin.withOptions(
function ({ className }) {
return function ({ addUtilities, theme, variants }) {
const utilities = _.fromPairs(
_.toPairs(theme('testPlugin')).map(([k, v]) => [
`.${className}-${k}`,
{ testProperty: v },
])
)
addUtilities(utilities, variants('testPlugin'))
}
},
function () {
return {
theme: {
testPlugin: {
sm: '1rem',
md: '2rem',
lg: '3rem',
},
},
variants: {
testPlugin: ['responsive', 'hover'],
},
}
}
)
return _postcss([
tailwind({
corePlugins: [],
theme: {
screens: {
sm: '400px',
},
},
plugins: [plugin({ className: 'banana' })],
}),
])
.process(
`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.banana-sm {
test-property: 1rem
}
.banana-md {
test-property: 2rem
}
.banana-lg {
test-property: 3rem
}
.hover\\:banana-sm:hover {
test-property: 1rem
}
.hover\\:banana-md:hover {
test-property: 2rem
}
.hover\\:banana-lg:hover {
test-property: 3rem
}
@media (min-width: 400px) {
.sm\\:banana-sm {
test-property: 1rem
}
.sm\\:banana-md {
test-property: 2rem
}
.sm\\:banana-lg {
test-property: 3rem
}
.sm\\:hover\\:banana-sm:hover {
test-property: 1rem
}
.sm\\:hover\\:banana-md:hover {
test-property: 2rem
}
.sm\\:hover\\:banana-lg:hover {
test-property: 3rem
}
}
`
expect(result.css).toMatchCss(expected)
})
})
test('plugins should cache correctly', () => {
const plugin = createPlugin.withOptions(
({ className = 'banana' } = {}) => ({ addComponents, variants }) => {
addComponents({ [`.${className}`]: { position: 'absolute' } }, variants('testPlugin'))
},
() => ({ variants: { testPlugin: ['responsive'] } })
)
function run(options = {}) {
return _postcss([
tailwind({
corePlugins: [],
theme: { screens: { sm: '400px' } },
plugins: [plugin(options)],
}),
]).process(`@tailwind base; @tailwind components; @tailwind utilities;`, {
from: undefined,
})
}
return Promise.all([run(), run({ className: 'apple' })]).then(([result1, result2]) => {
const expected1 = `
.banana {
position: absolute;
}
@media (min-width: 400px) {
.sm\\:banana {
position: absolute;
}
}
`
const expected2 = `
.apple {
position: absolute;
}
@media (min-width: 400px) {
.sm\\:apple {
position: absolute;
}
}
`
expect(result1.css).toMatchCss(expected1)
expect(result2.css).toMatchCss(expected2)
})
})
test('plugins created using `createPlugin.withOptions` do not need to be invoked if the user wants to use the default options', () => {
const plugin = createPlugin.withOptions(
function ({ className } = { className: 'banana' }) {
return function ({ addUtilities, theme, variants }) {
const utilities = _.fromPairs(
_.toPairs(theme('testPlugin')).map(([k, v]) => [
`.${className}-${k}`,
{ testProperty: v },
])
)
addUtilities(utilities, variants('testPlugin'))
}
},
function () {
return {
theme: {
testPlugin: {
sm: '1rem',
md: '2rem',
lg: '3rem',
},
},
variants: {
testPlugin: ['responsive', 'hover'],
},
}
}
)
return _postcss([
tailwind({
corePlugins: [],
theme: {
screens: {
sm: '400px',
},
},
plugins: [plugin],
}),
])
.process(
`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.banana-sm {
test-property: 1rem
}
.banana-md {
test-property: 2rem
}
.banana-lg {
test-property: 3rem
}
.hover\\:banana-sm:hover {
test-property: 1rem
}
.hover\\:banana-md:hover {
test-property: 2rem
}
.hover\\:banana-lg:hover {
test-property: 3rem
}
@media (min-width: 400px) {
.sm\\:banana-sm {
test-property: 1rem
}
.sm\\:banana-md {
test-property: 2rem
}
.sm\\:banana-lg {
test-property: 3rem
}
.sm\\:hover\\:banana-sm:hover {
test-property: 1rem
}
.sm\\:hover\\:banana-md:hover {
test-property: 2rem
}
.sm\\:hover\\:banana-lg:hover {
test-property: 3rem
}
}
`
expect(result.css).toMatchCss(expected)
})
})
test('the configFunction parameter is optional when using the `createPlugin.withOptions` function', () => {
const plugin = createPlugin.withOptions(function ({ className }) {
return function ({ addUtilities, theme, variants }) {
const utilities = _.fromPairs(
_.toPairs(theme('testPlugin')).map(([k, v]) => [`.${className}-${k}`, { testProperty: v }])
)
addUtilities(utilities, variants('testPlugin'))
}
})
return _postcss([
tailwind({
corePlugins: [],
theme: {
screens: {
sm: '400px',
},
testPlugin: {
sm: '1px',
md: '2px',
lg: '3px',
},
},
variants: {
testPlugin: ['responsive', 'focus'],
},
plugins: [plugin({ className: 'banana' })],
}),
])
.process(
`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.banana-sm {
test-property: 1px
}
.banana-md {
test-property: 2px
}
.banana-lg {
test-property: 3px
}
.focus\\:banana-sm:focus {
test-property: 1px
}
.focus\\:banana-md:focus {
test-property: 2px
}
.focus\\:banana-lg:focus {
test-property: 3px
}
@media (min-width: 400px) {
.sm\\:banana-sm {
test-property: 1px
}
.sm\\:banana-md {
test-property: 2px
}
.sm\\:banana-lg {
test-property: 3px
}
.sm\\:focus\\:banana-sm:focus {
test-property: 1px
}
.sm\\:focus\\:banana-md:focus {
test-property: 2px
}
.sm\\:focus\\:banana-lg:focus {
test-property: 3px
}
}
`
expect(result.css).toMatchCss(expected)
})
})
test('plugins can extend variants', () => {
const plugin = createPlugin(function () {}, {
variants: {
opacity: ({ before }) => before(['active'], 'focus'),
},
})
return _postcss([
tailwind({
theme: {
opacity: { 0: '0', 100: '1' },
},
corePlugins: ['opacity'],
variants: { opacity: ({ without }) => without(['responsive']) },
plugins: [plugin],
}),
])
.process(
`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.opacity-0 {
opacity: 0
}
.opacity-100 {
opacity: 1
}
.group:hover .group-hover\\:opacity-0 {
opacity: 0
}
.group:hover .group-hover\\:opacity-100 {
opacity: 1
}
.focus-within\\:opacity-0:focus-within {
opacity: 0
}
.focus-within\\:opacity-100:focus-within {
opacity: 1
}
.hover\\:opacity-0:hover {
opacity: 0
}
.hover\\:opacity-100:hover {
opacity: 1
}
.active\\:opacity-0:active {
opacity: 0
}
.active\\:opacity-100:active {
opacity: 1
}
.focus\\:opacity-0:focus {
opacity: 0
}
.focus\\:opacity-100:focus {
opacity: 1
}
`
expect(result.css).toMatchCss(expected)
})
})
test('keyframes are not escaped', () => {
const { components, utilities } = processPlugins(
[
function ({ addUtilities, addComponents }) {
addComponents({
'@keyframes foo': {
'25.001%': {
color: 'black',
},
},
})
addUtilities({
'@keyframes bar': {
'75.001%': {
color: 'white',
},
},
})
},
],
makeConfig()
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
@keyframes foo {
25.001% {
color: black
}
}
}
}
`)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
@keyframes bar {
75.001% {
color: white
}
}
}
}
`)
})
test('font sizes are retrieved without default line-heights or letter-spacing using theme function', () => {
const { components } = processPlugins(
[
function ({ addComponents, theme }) {
addComponents({
'.foo': {
fontSize: theme('fontSize.sm'),
},
})
},
],
makeConfig({
theme: {
fontSize: {
sm: ['14px', '20px'],
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.foo {
font-size: 14px;
}
}
}
`)
})
test('outlines are retrieved without outline-offset using theme function', () => {
const { components } = processPlugins(
[
function ({ addComponents, theme }) {
addComponents({
'.foo': {
outline: theme('outline.black'),
},
})
},
],
makeConfig({
theme: {
outline: {
black: ['2px dotted black', '4px'],
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.foo {
outline: 2px dotted black;
}
}
}
`)
})
test('box-shadow values are joined when retrieved using the theme function', () => {
const { components } = processPlugins(
[
function ({ addComponents, theme }) {
addComponents({
'.foo': {
boxShadow: theme('boxShadow.lol'),
},
})
},
],
makeConfig({
theme: {
boxShadow: {
lol: ['width', 'height'],
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.foo {
box-shadow: width, height;
}
}
}
`)
})
test('transition-property values are joined when retrieved using the theme function', () => {
const { components } = processPlugins(
[
function ({ addComponents, theme }) {
addComponents({
'.foo': {
transitionProperty: theme('transitionProperty.lol'),
},
})
},
],
makeConfig({
theme: {
transitionProperty: {
lol: ['width', 'height'],
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.foo {
transition-property: width, height;
}
}
}
`)
})
test('transition-duration values are joined when retrieved using the theme function', () => {
const { components } = processPlugins(
[
function ({ addComponents, theme }) {
addComponents({
'.foo': {
transitionDuration: theme('transitionDuration.lol'),
},
})
},
],
makeConfig({
theme: {
transitionDuration: {
lol: ['width', 'height'],
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.foo {
transition-duration: width, height;
}
}
}
`)
})
test('transition-delay values are joined when retrieved using the theme function', () => {
const { components } = processPlugins(
[
function ({ addComponents, theme }) {
addComponents({
'.foo': {
transitionDelay: theme('transitionDelay.lol'),
},
})
},
],
makeConfig({
theme: {
transitionDelay: {
lol: ['width', 'height'],
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.foo {
transition-delay: width, height;
}
}
}
`)
})
test('transition-timing-function values are joined when retrieved using the theme function', () => {
const { components } = processPlugins(
[
function ({ addComponents, theme }) {
addComponents({
'.foo': {
transitionTimingFunction: theme('transitionTimingFunction.lol'),
},
})
},
],
makeConfig({
theme: {
transitionTimingFunction: {
lol: ['width', 'height'],
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.foo {
transition-timing-function: width, height;
}
}
}
`)
})
test('background-image values are joined when retrieved using the theme function', () => {
const { components } = processPlugins(
[
function ({ addComponents, theme }) {
addComponents({
'.foo': {
backgroundImage: theme('backgroundImage.lol'),
},
})
},
],
makeConfig({
theme: {
backgroundImage: {
lol: ['width', 'height'],
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.foo {
background-image: width, height;
}
}
}
`)
})
test('background-size values are joined when retrieved using the theme function', () => {
const { components } = processPlugins(
[
function ({ addComponents, theme }) {
addComponents({
'.foo': {
backgroundSize: theme('backgroundSize.lol'),
},
})
},
],
makeConfig({
theme: {
backgroundSize: {
lol: ['width', 'height'],
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.foo {
background-size: width, height;
}
}
}
`)
})
test('background-color values are joined when retrieved using the theme function', () => {
const { components } = processPlugins(
[
function ({ addComponents, theme }) {
addComponents({
'.foo': {
backgroundColor: theme('backgroundColor.lol'),
},
})
},
],
makeConfig({
theme: {
backgroundColor: {
lol: ['width', 'height'],
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.foo {
background-color: width, height;
}
}
}
`)
})
test('cursor values are joined when retrieved using the theme function', () => {
const { components } = processPlugins(
[
function ({ addComponents, theme }) {
addComponents({
'.foo': {
cursor: theme('cursor.lol'),
},
})
},
],
makeConfig({
theme: {
cursor: {
lol: ['width', 'height'],
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.foo {
cursor: width, height;
}
}
}
`)
})
test('animation values are joined when retrieved using the theme function', () => {
const { components } = processPlugins(
[
function ({ addComponents, theme }) {
addComponents({
'.foo': {
animation: theme('animation.lol'),
},
})
},
],
makeConfig({
theme: {
animation: {
lol: ['width', 'height'],
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.foo {
animation: width, height;
}
}
}
`)
})