core/api/tailwindcss-master/__tests__/applyAtRule.test.js

1403 lines
28 KiB
JavaScript

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