import postcss from 'postcss' import resolveConfig from '../src/util/resolveConfig' import defaultConfig from '../stubs/defaultConfig.stub.js' import tailwind from '../src/index' function run(input, config = {}) { return postcss([tailwind(config)]).process(input, { from: undefined }) } test('it copies class declarations into itself', () => { const output = '.a { color: red; } .b { color: red; }' expect.assertions(2) return run('.a { color: red; } .b { @apply a; }').then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('apply values can contain tabs', () => { const input = ` .a { @apply p-4\tm-4; } ` const expected = ` .a { margin: 1rem; padding: 1rem; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('apply values can contain newlines', () => { const input = ` .a { @apply p-4 m-4 flex flex-col; } ` const expected = ` .a { display: flex; flex-direction: column; margin: 1rem; padding: 1rem; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('selectors with invalid characters do not need to be manually escaped', () => { const input = ` .a\\:1\\/2 { color: red; } .b { @apply a:1/2; } ` const expected = ` .a\\:1\\/2 { color: red; } .b { color: red; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('it removes important from applied classes by default', () => { const input = ` .a { color: red !important; } .a:hover { color: blue !important; } .b { @apply a; } ` const expected = ` .a { color: red !important; } .a:hover { color: blue !important; } .b { color: red; } .b:hover { color: blue; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('applied rules can be made !important', () => { const input = ` .a { color: red; } .b { @apply a !important; } ` const expected = ` .a { color: red; } .b { color: red !important; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('cssnext custom property sets are no longer supported', () => { const input = ` .a { color: red; } .b { @apply a --custom-property-set; } ` expect.assertions(1) return run(input).catch((e) => { expect(e).toMatchObject({ name: 'CssSyntaxError' }) }) }) test('it fails if the class does not exist', () => { expect.assertions(1) return run('.b { @apply a; }').catch((e) => { expect(e).toMatchObject({ name: 'CssSyntaxError' }) }) }) test('applying classes that are defined in a media query is supported', () => { const input = ` @media (min-width: 300px) { .a { color: blue; } } .b { @apply a; } ` const output = ` @media (min-width: 300px) { .a { color: blue; } } @media (min-width: 300px) { .b { color: blue; } } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('applying classes that are used in a media query is supported', () => { const input = ` .a { color: red; } @media (min-width: 300px) { .a { color: blue; } } .b { @apply a; } ` const output = ` .a { color: red; } @media (min-width: 300px) { .a { color: blue; } } .b { color: red; } @media (min-width: 300px) { .b { color: blue; } } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it matches classes that include pseudo-selectors', () => { const input = ` .a:hover { color: red; } .b { @apply a; } ` const output = ` .a:hover { color: red; } .b:hover { color: red; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it matches classes that have multiple rules', () => { const input = ` .a { color: red; } .b { @apply a; } .a { color: blue; } ` const output = ` .a { color: red; } .b { color: red; color: blue; } .a { color: blue; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('applying a class that appears multiple times in one selector', () => { const input = ` .a + .a > .a { color: red; } .b { @apply a; } ` const output = ` .a + .a > .a { color: red; } .b + .a > .a { color: red; } .a + .b > .a { color: red; } .a + .a > .b { color: red; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('you can apply utility classes that do not actually exist as long as they would exist if utilities were being generated', () => { const input = ` .foo { @apply mt-4; } ` const expected = ` .foo { margin-top: 1rem; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('shadow lookup will be constructed when we have missing @tailwind atrules', () => { const input = ` @tailwind base; .foo { @apply mt-4; } ` expect.assertions(1) return run(input).then((result) => { expect(result.css).toContain(`.foo { margin-top: 1rem;\n}`) }) }) test('you can apply a class that is defined in multiple rules', () => { const input = ` .foo { color: red; } .bar { @apply foo; } .foo { opacity: .5; } ` const expected = ` .foo { color: red; } .bar { color: red; opacity: .5; } .foo { opacity: .5; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply a class that is defined in a media query', () => { const input = ` .foo { @apply sm:text-center; } ` const expected = ` @media (min-width: 640px) { .foo { text-align: center } } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply pseudo-class variant utilities', () => { const input = ` .foo { @apply hover:opacity-50; } ` const expected = ` .foo:hover { opacity: 0.5 } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply responsive pseudo-class variant utilities', () => { const input = ` .foo { @apply sm:hover:opacity-50; } ` const expected = ` @media (min-width: 640px) { .foo:hover { opacity: 0.5 } } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply the container component', () => { const input = ` .foo { @apply container; } ` const expected = ` .foo { width: 100%; } @media (min-width: 640px) { .foo { max-width: 640px; } } @media (min-width: 768px) { .foo { max-width: 768px; } } @media (min-width: 1024px) { .foo { max-width: 1024px; } } @media (min-width: 1280px) { .foo { max-width: 1280px; } } @media (min-width: 1536px) { .foo { max-width: 1536px; } } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('classes are applied according to CSS source order, not apply order', () => { const input = ` .foo { color: red; } .bar { color: blue; } .baz { @apply bar foo; } ` const expected = ` .foo { color: red; } .bar { color: blue; } .baz { color: red; color: blue; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply utilities with multi-class selectors like group-hover variants', () => { const input = ` .foo { @apply group-hover:bar; } .bar { color: blue; } .group:hover .group-hover\\:bar { color: blue; } ` const expected = ` .group:hover .foo { color: blue; } .bar { color: blue; } .group:hover .group-hover\\:bar { color: blue; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply classes recursively', () => { const input = ` .baz { color: blue; } .bar { @apply baz px-4; } .foo { @apply bar; } ` const expected = ` .baz { color: blue; } .bar { padding-left: 1rem; padding-right: 1rem; color: blue; } .foo { padding-left: 1rem; padding-right: 1rem; color: blue; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply complex classes recursively', () => { const input = ` .button { @apply rounded-xl px-6 py-2 hover:text-white focus:border-opacity-100; } .button-yellow { @apply button bg-yellow-600 text-gray-200; } ` const expected = ` .button:focus { --tw-border-opacity: 1; } .button { border-radius: 0.75rem; padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 1.5rem; padding-right: 1.5rem; } .button:hover { --tw-text-opacity: 1; color: rgba(255, 255, 255, var(--tw-text-opacity)); } .button-yellow { --tw-bg-opacity: 1; background-color: rgba(217, 119, 6, var(--tw-bg-opacity)); --tw-text-opacity: 1; color: rgba(229, 231, 235, var(--tw-text-opacity)); } .button-yellow:focus { --tw-border-opacity: 1; } .button-yellow { border-radius: 0.75rem; padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 1.5rem; padding-right: 1.5rem; } .button-yellow:hover { --tw-text-opacity: 1; color: rgba(255, 255, 255, var(--tw-text-opacity)); } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply classes recursively out of order', () => { const input = ` .foo { @apply bar; } .bar { @apply baz; } .baz { color: blue; } ` const expected = ` .foo { color: blue; } .bar { color: blue; } .baz { color: blue; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('applied classes are always inserted before subsequent declarations in the same rule, even if it means moving those subsequent declarations to a new rule', () => { const input = ` .foo { background: blue; @apply opacity-50 hover:opacity-100 text-right sm:align-middle; color: red; } ` const expected = ` .foo { background: blue; opacity: 0.5; } .foo:hover { opacity: 1; } .foo { text-align: right; } @media (min-width: 640px) { .foo { vertical-align: middle; } } .foo { color: red; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('adjacent rules are collapsed after being applied', () => { const input = ` .foo { @apply hover:bg-white hover:opacity-50 absolute text-right sm:align-middle sm:text-center; } ` const expected = ` .foo:hover { --tw-bg-opacity: 1; background-color: rgba(255, 255, 255, var(--tw-bg-opacity)); opacity: 0.5; } .foo { position: absolute; text-align: right; } @media (min-width: 640px) { .foo { text-align: center; vertical-align: middle; } } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('applying a class applies all instances of that class, even complex selectors', () => { const input = ` h1 > p:hover .banana:first-child * { @apply bar; } .bar { color: blue; } @media (print) { @supports (display: grid) { .baz .bar:hover { text-align: right; float: left; } } } ` const expected = ` h1 > p:hover .banana:first-child * { color: blue; } @media (print) { @supports (display: grid) { .baz h1 > p:hover .banana:first-child *:hover { text-align: right; float: left; } } } .bar { color: blue; } @media (print) { @supports (display: grid) { .baz .bar:hover { text-align: right; float: left; } } } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply classes to rules within at-rules', () => { const input = ` @supports (display: grid) { .baz .bar { @apply text-right float-left hover:opacity-50 md:float-right; } } ` const expected = ` @supports (display: grid) { .baz .bar { float: left; } .baz .bar:hover { opacity: 0.5; } .baz .bar { text-align: right; } @media (min-width: 768px) { .baz .bar { float: right; } } } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) describe('using apply with the prefix option', () => { test('applying a class including the prefix', () => { const input = ` .foo { @apply tw-mt-4; } ` const expected = ` .foo { margin-top: 1rem; } ` const config = resolveConfig([ { ...defaultConfig, prefix: 'tw-', }, ]) expect.assertions(2) return run(input, config).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('applying a class including the prefix when using a prefix function', () => { const input = ` .foo { @apply tw-func-mt-4; } ` const expected = ` .foo { margin-top: 1rem; } ` const config = resolveConfig([ { ...defaultConfig, prefix: () => { return 'tw-func-' }, }, ]) expect.assertions(2) return run(input, config).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('applying a class without the prefix fails', () => { const input = ` .foo { @apply mt-4; } ` const config = resolveConfig([ { ...defaultConfig, prefix: 'tw-', }, ]) expect.assertions(1) return run(input, config).catch((e) => { expect(e).toMatchObject({ name: 'CssSyntaxError' }) }) }) test('custom classes with no prefix can be applied', () => { const input = ` .foo { @apply mt-4; } .mt-4 { color: red; } ` const expected = ` .foo { color: red; } .mt-4 { color: red; } ` const config = resolveConfig([ { ...defaultConfig, prefix: 'tw-', }, ]) expect.assertions(2) return run(input, config).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('built-in prefixed utilities can be extended and applied', () => { const input = ` .foo { @apply tw-mt-4; } .tw-mt-4 { color: red; } ` const expected = ` .foo { margin-top: 1rem; color: red; } .tw-mt-4 { color: red; } ` const config = resolveConfig([ { ...defaultConfig, prefix: 'tw-', }, ]) expect.assertions(2) return run(input, config).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('a helpful error message is provided if it appears the user forgot to include their prefix', () => { const input = ` .foo { @apply mt-4; } ` const config = resolveConfig([ { ...defaultConfig, prefix: 'tw-', }, ]) expect.assertions(1) return run(input, config).catch((e) => { expect(e).toMatchObject({ name: 'CssSyntaxError', reason: 'The `mt-4` class does not exist, but `tw-mt-4` does. Did you forget the prefix?', }) }) }) test('a "Did You Mean" suggestion is recommended if a similar class can be identified.', () => { const input = ` .foo { @apply anti-aliased; } ` const config = resolveConfig([ { ...defaultConfig, }, ]) expect.assertions(1) return run(input, config).catch((e) => { expect(e).toMatchObject({ name: 'CssSyntaxError', reason: "The `anti-aliased` class does not exist, but `antialiased` does. If you're sure that `anti-aliased` exists, make sure that any `@import` statements are being properly processed before Tailwind CSS sees your CSS, as `@apply` can only be used for classes in the same CSS tree.", }) }) }) test('a "Did You Mean" suggestion is omitted if a similar class cannot be identified.', () => { const input = ` .foo { @apply anteater; } ` const config = resolveConfig([{ ...defaultConfig }]) expect.assertions(1) return run(input, config).catch((e) => { expect(e).toMatchObject({ name: 'CssSyntaxError', reason: "The `anteater` class does not exist. If you're sure that `anteater` exists, make sure that any `@import` statements are being properly processed before Tailwind CSS sees your CSS, as `@apply` can only be used for classes in the same CSS tree.", }) }) }) test('you can apply classes with important and a prefix enabled', () => { const input = ` .foo { @apply tw-mt-4; } ` const expected = ` .foo { margin-top: 1rem; } ` const config = resolveConfig([ { ...defaultConfig, prefix: 'tw-', important: true, }, ]) expect.assertions(2) return run(input, config).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply classes with an important selector and a prefix enabled', () => { const input = ` .foo { @apply tw-mt-4; } ` const expected = ` .foo { margin-top: 1rem; } ` const config = resolveConfig([ { ...defaultConfig, prefix: 'tw-', important: '#app', }, ]) expect.assertions(2) return run(input, config).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) }) test('you can apply utility classes when a selector is used for the important option', () => { const input = ` .foo { @apply mt-8 mb-8; } ` const expected = ` .foo { margin-top: 2rem; margin-bottom: 2rem; } ` const config = resolveConfig([ { ...defaultConfig, important: '#app', }, ]) expect.assertions(2) return run(input, config).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply classes to a rule with multiple selectors', () => { const input = ` @supports (display: grid) { .foo, h1 > .bar * { @apply float-left opacity-50 hover:opacity-100 md:float-right; } } ` const expected = ` @supports (display: grid) { .foo, h1 > .bar * { float: left; opacity: 0.5; } .foo:hover, h1 > .bar *:hover { opacity: 1; } @media (min-width: 768px) { .foo, h1 > .bar * { float: right; } } } ` return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply classes to a rule with multiple selectors with important and a prefix enabled', () => { const input = ` @supports (display: grid) { .foo, h1 > .bar * { @apply tw-float-left tw-opacity-50 hover:tw-opacity-100 md:tw-float-right; } } ` const expected = ` @supports (display: grid) { .foo, h1 > .bar * { float: left; opacity: 0.5; } .foo:hover, h1 > .bar *:hover { opacity: 1; } @media (min-width: 768px) { .foo, h1 > .bar * { float: right; } } } ` const config = resolveConfig([ { ...defaultConfig, prefix: 'tw-', important: true, }, ]) return run(input, config).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply classes to multiple selectors at the same time, removing important', () => { const input = ` .multiple p, .multiple ul, .multiple ol { @apply mt-5; } .multiple h2, .multiple h3, .multiple h4 { @apply mt-8; } ` const expected = ` .multiple p, .multiple ul, .multiple ol { margin-top: 1.25rem; } .multiple h2, .multiple h3, .multiple h4 { margin-top: 2rem; } ` const config = resolveConfig([{ ...defaultConfig, important: true }]) return run(input, config).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply classes in a nested rule', () => { const input = ` .selector { &:hover { @apply text-white; } } ` const expected = ` .selector { &:hover { --tw-text-opacity: 1; color: rgba(255, 255, 255, var(--tw-text-opacity)); } } ` return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply classes in a nested @atrule', () => { const input = ` .selector { @media (min-width: 200px) { @apply overflow-hidden; } } ` const expected = ` .selector { @media (min-width: 200px) { overflow: hidden; } } ` return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can apply classes in a custom nested @atrule', () => { const input = ` .selector { @screen md { @apply w-2/6; } } ` const expected = ` .selector { @media (min-width: 768px) { width: 33.333333%; } } ` return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('you can deeply apply classes in a custom nested @atrule', () => { const input = ` .selector { .subselector { @screen md { @apply w-2/6; } } } ` const expected = ` .selector { .subselector { @media (min-width: 768px) { width: 33.333333% } } } ` return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('declarations within a rule that uses @apply can be !important', () => { const input = ` .foo { @apply text-center; float: left; display: block !important; } ` const expected = ` .foo { text-align: center; float: left; display: block !important; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('declarations within a rule that uses @apply with !important remain not !important', () => { const input = ` .foo { @apply text-center !important; float: left; display: block !important; } ` const expected = ` .foo { text-align: center !important; float: left; display: block !important; } ` expect.assertions(2) return run(input).then((result) => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) test('lookup tree is correctly cached based on used tailwind atrules', async () => { const input1 = ` @tailwind utilities; .foo { @apply mt-4; } ` const input2 = ` @tailwind components; .foo { @apply mt-4; } ` let config = { corePlugins: [], plugins: [ function ({ addUtilities, addComponents }) { addUtilities({ '.mt-4': { marginTop: '1rem' } }, []) addComponents({ '.container': { maxWidth: '500px' } }, []) }, ], } let output1 = await run(input1, config) let output2 = await run(input2, config) expect(output1.css).toMatchCss(` .mt-4 { margin-top: 1rem; } .foo { margin-top: 1rem; } `) expect(output2.css).toMatchCss(` .container { max-width: 500px; } .foo { margin-top: 1rem; } `) })