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; } } } `) })