tailwindcss/tests/apply.test.js
Robin Malfait b9af5a916b
Improve collapsing of duplicate declarations (#6856)
* improve collapsing of duplicate properties

In theory, we don't have to do anything because the browser is smart
enough to figure everything out. However, leaving in duplicate
properties is not that ideal for file size.

Our previous method was pretty simple: if you see a declaration you
already saw in this rule, delete the previous one and keep the current
one.

This works pretty well, but this gets rid of **all** the declarations
with the same property. This is not great for overrides for older
browsers.

In a perfect world, we can handle this based on your target browser but
this is a lot of unnecessary complexity and will slow things down
performance wise.

Alternative, we improved the solution by being a bit smarter:
1. Delete duplicate declarations that have the same property and value
   (this will get rid of **exact** duplications).
2. Delete declarations with the same property and the same **unit**.

This means that we will reduce this:
```css
.example {
  height: 50%;
  height: 100px;
  height: 20vh;
  height: 30%;
  height: 50px;
  height: 30vh;
  transform: var(--value);
  transform: var(--value);
}
```

To:
```diff-css
  .example {
-   height: 50%;    /* Another height exists later with a `%` unit */
-   height: 100px;  /* Another height exists later with a `px` unit */
-   height: 20vh;   /* Another height exists later with a `vh` unit */
    height: 30%;
    height: 50px;
    height: 30vh;
-   transform: var(--value); /* Value is too complex, but is **exactly** the same as the one below */
    transform: var(--value);
  }
```

This will reduce the values that we are 100% sure that can be safely
removed. This will still result in some overrides but the browser can
handle those for you.

Fixes: #6844

* update changelog
2022-01-03 15:41:49 +01:00

778 lines
16 KiB
JavaScript

import fs from 'fs'
import path from 'path'
import { run, html, css } from './util/run'
test('@apply', () => {
let config = {
darkMode: 'class',
content: [path.resolve(__dirname, './apply.test.html')],
}
let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.basic-example {
@apply px-4 py-2 bg-blue-500 rounded-md;
}
.class-order {
@apply pt-4 pr-1 px-3 py-7 p-8;
}
.with-additional-properties {
font-weight: 500;
@apply text-right;
}
.variants {
@apply xl:focus:font-black hover:font-bold lg:font-light focus:font-medium font-semibold;
}
.only-variants {
@apply xl:focus:font-black hover:font-bold lg:font-light focus:font-medium;
}
.apply-group-variant {
@apply group-hover:text-center lg:group-hover:text-left;
}
.apply-dark-variant {
@apply dark:text-center dark:hover:text-right lg:dark:text-left;
}
.apply-custom-utility {
@apply custom-util hover:custom-util lg:custom-util xl:focus:custom-util;
}
.multiple,
.selectors {
@apply px-4 py-2 bg-blue-500 rounded-md;
}
.multiple-variants,
.selectors-variants {
@apply hover:text-center active:text-right lg:focus:text-left;
}
.multiple-group,
.selectors-group {
@apply group-hover:text-center lg:group-hover:text-left;
}
/* TODO: This works but the generated CSS is unnecessarily verbose. */
.complex-utilities {
@apply ordinal tabular-nums focus:diagonal-fractions shadow-lg hover:shadow-xl;
}
.basic-nesting-parent {
.basic-nesting-child {
@apply font-bold hover:font-normal;
}
}
.use-base-only-a {
@apply font-bold;
}
.use-base-only-b {
@apply use-base-only-a font-normal;
}
.use-dependant-only-a {
@apply font-bold;
}
.use-dependant-only-b {
@apply use-dependant-only-a font-normal;
}
.btn {
@apply font-bold py-2 px-4 rounded;
}
.btn-blue {
@apply btn bg-blue-500 hover:bg-blue-700 text-white;
}
.recursive-apply-a {
@apply font-black sm:font-thin;
}
.recursive-apply-b {
@apply recursive-apply-a font-semibold md:font-extralight;
}
.recursive-apply-c {
@apply recursive-apply-b font-bold lg:font-light;
}
.use-with-other-properties-base {
color: green;
@apply font-bold;
}
.use-with-other-properties-component {
@apply use-with-other-properties-base;
}
.add-sibling-properties {
padding: 2rem;
@apply px-4 hover:px-2 lg:px-10 xl:focus:px-1;
padding-top: 3px;
@apply use-with-other-properties-base;
}
h1 {
@apply text-2xl lg:text-2xl sm:text-3xl;
}
h2 {
@apply text-2xl;
@apply lg:text-2xl;
@apply sm:text-2xl;
}
.important-modifier {
@apply px-4 !rounded-md;
}
.important-modifier-variant {
@apply px-4 hover:!rounded-md;
}
}
@layer utilities {
.custom-util {
custom: stuff;
}
.foo {
@apply animate-spin;
}
.bar {
@apply animate-pulse !important;
}
}
`
return run(input, config).then((result) => {
let expectedPath = path.resolve(__dirname, './apply.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})
test('@apply error with unknown utility', async () => {
let config = {
darkMode: 'class',
content: [path.resolve(__dirname, './apply.test.html')],
}
let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.foo {
@apply a-utility-that-does-not-exist;
}
}
`
await expect(run(input, config)).rejects.toThrowError('class does not exist')
})
test('@apply error with nested @screen', async () => {
let config = {
darkMode: 'class',
content: [path.resolve(__dirname, './apply.test.html')],
}
let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.foo {
@screen md {
@apply text-black;
}
}
}
`
await expect(run(input, config)).rejects.toThrowError(
'@apply is not supported within nested at-rules like @screen'
)
})
test('@apply error with nested @anyatrulehere', async () => {
let config = {
darkMode: 'class',
content: [path.resolve(__dirname, './apply.test.html')],
}
let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.foo {
@genie {
@apply text-black;
}
}
}
`
await expect(run(input, config)).rejects.toThrowError(
'@apply is not supported within nested at-rules like @genie'
)
})
test('@apply error when using .group utility', async () => {
let config = {
darkMode: 'class',
content: [{ raw: '<div class="foo"></div>' }],
}
let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.foo {
@apply group;
}
}
`
await expect(run(input, config)).rejects.toThrowError(
`@apply should not be used with the 'group' utility`
)
})
test('@apply error when using a prefixed .group utility', async () => {
let config = {
prefix: 'tw-',
darkMode: 'class',
content: [{ raw: html`<div class="foo"></div>` }],
}
let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.foo {
@apply tw-group;
}
}
`
await expect(run(input, config)).rejects.toThrowError(
`@apply should not be used with the 'tw-group' utility`
)
})
test('@apply classes from outside a @layer', async () => {
let config = {
content: [{ raw: html`<div class="font-bold foo bar baz"></div>` }],
}
let input = css`
@tailwind components;
@tailwind utilities;
.foo {
@apply font-bold;
}
.bar {
@apply foo text-red-500 hover:text-green-500;
}
.baz {
@apply bar underline;
}
.keep-me-even-though-I-am-not-used-in-content {
color: green;
}
`
await run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.font-bold {
font-weight: 700;
}
.foo {
font-weight: 700;
}
.bar {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
font-weight: 700;
}
.bar:hover {
--tw-text-opacity: 1;
color: rgb(34 197 94 / var(--tw-text-opacity));
}
.baz {
text-decoration-line: underline;
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
font-weight: 700;
}
.baz:hover {
--tw-text-opacity: 1;
color: rgb(34 197 94 / var(--tw-text-opacity));
}
.keep-me-even-though-I-am-not-used-in-content {
color: green;
}
`)
})
})
test('@applying classes from outside a @layer respects the source order', async () => {
let config = {
content: [{ raw: html`<div class="container font-bold foo bar baz"></div>` }],
}
let input = css`
.baz {
@apply bar underline;
}
@tailwind components;
.keep-me-even-though-I-am-not-used-in-content {
color: green;
}
@tailwind utilities;
.foo {
@apply font-bold;
}
.bar {
@apply no-underline;
}
`
await run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.baz {
text-decoration-line: underline;
text-decoration-line: none;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.keep-me-even-though-I-am-not-used-in-content {
color: green;
}
.font-bold {
font-weight: 700;
}
.foo {
font-weight: 700;
}
.bar {
text-decoration-line: none;
}
`)
})
})
it('should remove duplicate properties when using apply with similar properties', () => {
let config = {
content: [{ raw: 'foo' }],
}
let input = css`
@tailwind utilities;
.foo {
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2;
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.foo {
position: absolute;
top: 50%;
left: 50%;
--tw-translate-x: -50%;
--tw-translate-y: -50%;
transform: var(--tw-transform);
}
`)
})
})
it('should apply all the definitions of a class', () => {
let config = {
content: [{ raw: html`<div class="foo"></div>` }],
plugins: [],
}
let input = css`
@tailwind components;
@tailwind utilities;
@layer utilities {
.aspect-w-1 {
position: relative;
}
.aspect-w-1 {
--tw-aspect-w: 1;
}
}
@layer components {
.foo {
@apply aspect-w-1;
}
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.foo {
position: relative;
--tw-aspect-w: 1;
}
`)
})
})
it('should throw when trying to apply a direct circular dependency', () => {
let config = {
content: [{ raw: html`<div class="foo"></div>` }],
plugins: [],
}
let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.foo:not(.text-red-500) {
@apply text-red-500;
}
}
`
return run(input, config).catch((err) => {
expect(err.reason).toBe(
'You cannot `@apply` the `text-red-500` utility here because it creates a circular dependency.'
)
})
})
it('should throw when trying to apply an indirect circular dependency', () => {
let config = {
content: [{ raw: html`<div class="a"></div>` }],
plugins: [],
}
let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.a {
@apply b;
}
.b {
@apply c;
}
.c {
@apply a;
}
}
`
return run(input, config).catch((err) => {
expect(err.reason).toBe(
'You cannot `@apply` the `a` utility here because it creates a circular dependency.'
)
})
})
it('should not throw when the selector is different (but contains the base partially)', () => {
let config = {
content: [{ raw: html`<div class="bg-gray-500"></div>` }],
plugins: [],
}
let input = css`
@tailwind components;
@tailwind utilities;
.focus\:bg-gray-500 {
@apply bg-gray-500;
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-gray-500 {
--tw-bg-opacity: 1;
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
}
.focus\:bg-gray-500 {
--tw-bg-opacity: 1;
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
}
`)
})
})
it('should throw when trying to apply an indirect circular dependency with a modifier (1)', () => {
let config = {
content: [{ raw: html`<div class="a"></div>` }],
plugins: [],
}
let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.a {
@apply b;
}
.b {
@apply c;
}
.c {
@apply hover:a;
}
}
`
return run(input, config).catch((err) => {
expect(err.reason).toBe(
'You cannot `@apply` the `hover:a` utility here because it creates a circular dependency.'
)
})
})
it('should throw when trying to apply an indirect circular dependency with a modifier (2)', () => {
let config = {
content: [{ raw: html`<div class="a"></div>` }],
plugins: [],
}
let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.a {
@apply b;
}
.b {
@apply hover:c;
}
.c {
@apply a;
}
}
`
return run(input, config).catch((err) => {
expect(err.reason).toBe(
'You cannot `@apply` the `a` utility here because it creates a circular dependency.'
)
})
})
it('rules with vendor prefixes are still separate when optimizing defaults rules', () => {
let config = {
experimental: { optimizeUniversalDefaults: true },
content: [{ raw: html`<div class="border"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
input[type='range']::-moz-range-thumb {
@apply border;
}
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
[type='range']::-moz-range-thumb {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
.border {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
input[type='range']::-moz-range-thumb {
border-width: 1px;
}
.border {
border-width: 1px;
}
`)
})
})
it('should be possible to apply user css', () => {
let config = {
content: [{ raw: html`<div></div>` }],
plugins: [],
}
let input = css`
@tailwind components;
@tailwind utilities;
.foo {
color: red;
}
.bar {
@apply foo;
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.foo {
color: red;
}
.bar {
color: red;
}
`)
})
})
it('should not be possible to apply user css with variants', () => {
let config = {
content: [{ raw: html`<div></div>` }],
plugins: [],
}
let input = css`
@tailwind components;
@tailwind utilities;
.foo {
color: red;
}
.bar {
@apply hover:foo;
}
`
return run(input, config).catch((err) => {
expect(err.reason).toBe(
'The `hover:foo` class does not exist. If `hover:foo` is a custom class, make sure it is defined within a `@layer` directive.'
)
})
})
it('should not apply unrelated siblings when applying something from within atrules', () => {
let config = {
content: [{ raw: html`<div class="foo bar something-unrelated"></div>` }],
plugins: [],
}
let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.foo {
font-weight: bold;
@apply bar;
}
.bar {
color: green;
}
@supports (a: b) {
.bar {
color: blue;
}
.something-unrelated {
color: red;
}
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.foo {
font-weight: bold;
color: green;
}
@supports (a: b) {
.foo {
color: blue;
}
}
.bar {
color: green;
}
@supports (a: b) {
.bar {
color: blue;
}
.something-unrelated {
color: red;
}
}
`)
})
})