Run test suite against both engines (#10373)

* Run test suite against both engines

* make eslint happy

* only run `stable` tests on Node 12

* use normal expectation instead of snapshot file

When we run the tests only against `stable` (for node 12), then the
snapshots exists for the `Oxide` build. They are marked as `obsolete`
and will cause the `npm run test` script to fail. Sadly.

Inlined them for now, but ideally we make those tests more blackbox-y so
that we test that we get source maps and that we can map the sourcemap
back to the input files (without looking at the actual annotations).

* properly indent inline css

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
Adam Wathan 2023-01-20 12:45:04 -05:00 committed by GitHub
parent 7d1491449e
commit 42136e94ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 24294 additions and 23988 deletions

View File

@ -1,378 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`apply generates source maps 1`] = `
Array [
"2:4 -> 2:4",
"3:6-27 -> 3:6-27",
"4:6-33 -> 4:6-18",
"4:6-33 -> 5:6-17",
"4:6-33 -> 6:6-24",
"4:6-33 -> 7:6-61",
"5:4 -> 8:4",
"7:4 -> 10:4",
"8:6-39 -> 11:6-39",
"9:6-31 -> 12:6-18",
"9:6-31 -> 13:6-17",
"9:6-31 -> 14:6-24",
"9:6-31 -> 15:6-61",
"10:4 -> 16:4",
"13:6 -> 18:4",
"13:6-29 -> 19:6-18",
"13:6-29 -> 20:6-17",
"13:6-29 -> 21:6-24",
"13:6 -> 22:6",
"13:29 -> 23:0",
]
`;
exports[`components have source maps 1`] = `
Array [
"2:4 -> 1:0",
"2:4 -> 2:4",
"2:24 -> 3:0",
"2:4 -> 4:0",
"2:4 -> 5:4",
"2:4 -> 6:8",
"2:24 -> 7:4",
"2:24 -> 8:0",
"2:4 -> 9:0",
"2:4 -> 10:4",
"2:4 -> 11:8",
"2:24 -> 12:4",
"2:24 -> 13:0",
"2:4 -> 14:0",
"2:4 -> 15:4",
"2:4 -> 16:8",
"2:24 -> 17:4",
"2:24 -> 18:0",
"2:4 -> 19:0",
"2:4 -> 20:4",
"2:4 -> 21:8",
"2:24 -> 22:4",
"2:24 -> 23:0",
"2:4 -> 24:0",
"2:4 -> 25:4",
"2:4 -> 26:8",
"2:24 -> 27:4",
"2:24 -> 28:0",
]
`;
exports[`preflight + base have source maps 1`] = `
Array [
"2:4 -> 1:0",
"2:18-4 -> 3:1-2",
"2:18 -> 6:1",
"2:4 -> 8:0",
"2:4-18 -> 11:2-32",
"2:4-18 -> 12:2-25",
"2:4-18 -> 13:2-29",
"2:4-18 -> 14:2-31",
"2:18 -> 15:0",
"2:4 -> 17:0",
"2:4-18 -> 19:2-18",
"2:18 -> 20:0",
"2:4 -> 22:0",
"2:18 -> 28:1",
"2:4 -> 30:0",
"2:4-18 -> 31:2-26",
"2:4-18 -> 32:2-40",
"2:4-18 -> 33:2-26",
"2:4-18 -> 34:2-21",
"2:4-18 -> 35:2-230",
"2:4-18 -> 36:2-39",
"2:18 -> 37:0",
"2:4 -> 39:0",
"2:18 -> 42:1",
"2:4 -> 44:0",
"2:4-18 -> 45:2-19",
"2:4-18 -> 46:2-30",
"2:18 -> 47:0",
"2:4 -> 49:0",
"2:18 -> 53:1",
"2:4 -> 55:0",
"2:4-18 -> 56:2-19",
"2:4-18 -> 57:2-24",
"2:4-18 -> 58:2-31",
"2:18 -> 59:0",
"2:4 -> 61:0",
"2:18 -> 63:1",
"2:4 -> 65:0",
"2:4-18 -> 66:2-35",
"2:18 -> 67:0",
"2:4 -> 69:0",
"2:18 -> 71:1",
"2:4 -> 73:0",
"2:4-18 -> 79:2-20",
"2:4-18 -> 80:2-22",
"2:18 -> 81:0",
"2:4 -> 83:0",
"2:18 -> 85:1",
"2:4 -> 87:0",
"2:4-18 -> 88:2-16",
"2:4-18 -> 89:2-26",
"2:18 -> 90:0",
"2:4 -> 92:0",
"2:18 -> 94:1",
"2:4 -> 96:0",
"2:4-18 -> 98:2-21",
"2:18 -> 99:0",
"2:4 -> 101:0",
"2:18 -> 104:1",
"2:4 -> 106:0",
"2:4-18 -> 110:2-121",
"2:4-18 -> 111:2-24",
"2:18 -> 112:0",
"2:4 -> 114:0",
"2:18 -> 116:1",
"2:4 -> 118:0",
"2:4-18 -> 119:2-16",
"2:18 -> 120:0",
"2:4 -> 122:0",
"2:18 -> 124:1",
"2:4 -> 126:0",
"2:4-18 -> 128:2-16",
"2:4-18 -> 129:2-16",
"2:4-18 -> 130:2-20",
"2:4-18 -> 131:2-26",
"2:18 -> 132:0",
"2:4 -> 134:0",
"2:4-18 -> 135:2-17",
"2:18 -> 136:0",
"2:4 -> 138:0",
"2:4-18 -> 139:2-13",
"2:18 -> 140:0",
"2:4 -> 142:0",
"2:18 -> 146:1",
"2:4 -> 148:0",
"2:4-18 -> 149:2-24",
"2:4-18 -> 150:2-31",
"2:4-18 -> 151:2-35",
"2:18 -> 152:0",
"2:4 -> 154:0",
"2:18 -> 158:1",
"2:4 -> 160:0",
"2:4-18 -> 165:2-30",
"2:4-18 -> 166:2-25",
"2:4-18 -> 167:2-30",
"2:4-18 -> 168:2-30",
"2:4-18 -> 169:2-24",
"2:4-18 -> 170:2-19",
"2:4-18 -> 171:2-20",
"2:18 -> 172:0",
"2:4 -> 174:0",
"2:18 -> 176:1",
"2:4 -> 178:0",
"2:4-18 -> 180:2-22",
"2:18 -> 181:0",
"2:4 -> 183:0",
"2:18 -> 186:1",
"2:4 -> 188:0",
"2:4-18 -> 192:2-36",
"2:4-18 -> 193:2-39",
"2:4-18 -> 194:2-32",
"2:18 -> 195:0",
"2:4 -> 197:0",
"2:18 -> 199:1",
"2:4 -> 201:0",
"2:4-18 -> 202:2-15",
"2:18 -> 203:0",
"2:4 -> 205:0",
"2:18 -> 207:1",
"2:4 -> 209:0",
"2:4-18 -> 210:2-18",
"2:18 -> 211:0",
"2:4 -> 213:0",
"2:18 -> 215:1",
"2:4 -> 217:0",
"2:4-18 -> 218:2-26",
"2:18 -> 219:0",
"2:4 -> 221:0",
"2:18 -> 223:1",
"2:4 -> 225:0",
"2:4-18 -> 227:2-14",
"2:18 -> 228:0",
"2:4 -> 230:0",
"2:18 -> 233:1",
"2:4 -> 235:0",
"2:4-18 -> 236:2-39",
"2:4-18 -> 237:2-30",
"2:18 -> 238:0",
"2:4 -> 240:0",
"2:18 -> 242:1",
"2:4 -> 244:0",
"2:4-18 -> 245:2-26",
"2:18 -> 246:0",
"2:4 -> 248:0",
"2:18 -> 251:1",
"2:4 -> 253:0",
"2:4-18 -> 254:2-36",
"2:4-18 -> 255:2-23",
"2:18 -> 256:0",
"2:4 -> 258:0",
"2:18 -> 260:1",
"2:4 -> 262:0",
"2:4-18 -> 263:2-20",
"2:18 -> 264:0",
"2:4 -> 266:0",
"2:18 -> 268:1",
"2:4 -> 270:0",
"2:4-18 -> 283:2-11",
"2:18 -> 284:0",
"2:4 -> 286:0",
"2:4-18 -> 287:2-11",
"2:4-18 -> 288:2-12",
"2:18 -> 289:0",
"2:4 -> 291:0",
"2:4-18 -> 292:2-12",
"2:18 -> 293:0",
"2:4 -> 295:0",
"2:4-18 -> 298:2-18",
"2:4-18 -> 299:2-11",
"2:4-18 -> 300:2-12",
"2:18 -> 301:0",
"2:4 -> 303:0",
"2:18 -> 305:1",
"2:4 -> 307:0",
"2:4-18 -> 308:2-18",
"2:18 -> 309:0",
"2:4 -> 311:0",
"2:18 -> 314:1",
"2:4 -> 316:0",
"2:4-18 -> 318:2-20",
"2:4-18 -> 319:2-24",
"2:18 -> 320:0",
"2:4 -> 322:0",
"2:18 -> 324:1",
"2:4 -> 326:0",
"2:4-18 -> 328:2-17",
"2:18 -> 329:0",
"2:4 -> 331:0",
"2:18 -> 333:1",
"2:4 -> 334:0",
"2:4-18 -> 335:2-17",
"2:18 -> 336:0",
"2:4 -> 338:0",
"2:18 -> 342:1",
"2:4 -> 344:0",
"2:4-18 -> 352:2-24",
"2:4-18 -> 353:2-32",
"2:18 -> 354:0",
"2:4 -> 356:0",
"2:18 -> 358:1",
"2:4 -> 360:0",
"2:4-18 -> 362:2-17",
"2:4-18 -> 363:2-14",
"2:18 -> 364:0",
"2:4-18 -> 366:0-72",
"2:4 -> 367:0",
"2:4-18 -> 368:2-15",
"2:18 -> 369:0",
"2:4 -> 371:0",
"2:4-18 -> 372:2-26",
"2:4-18 -> 373:2-26",
"2:4-18 -> 374:2-21",
"2:4-18 -> 375:2-21",
"2:4-18 -> 376:2-16",
"2:4-18 -> 377:2-16",
"2:4-18 -> 378:2-16",
"2:4-18 -> 379:2-17",
"2:4-18 -> 380:2-17",
"2:4-18 -> 381:2-15",
"2:4-18 -> 382:2-15",
"2:4-18 -> 383:2-20",
"2:4-18 -> 384:2-40",
"2:4-18 -> 385:2-17",
"2:4-18 -> 386:2-22",
"2:4-18 -> 387:2-24",
"2:4-18 -> 388:2-25",
"2:4-18 -> 389:2-26",
"2:4-18 -> 390:2-20",
"2:4-18 -> 391:2-29",
"2:4-18 -> 392:2-30",
"2:4-18 -> 393:2-40",
"2:4-18 -> 394:2-36",
"2:4-18 -> 395:2-29",
"2:4-18 -> 396:2-24",
"2:4-18 -> 397:2-32",
"2:4-18 -> 398:2-14",
"2:4-18 -> 399:2-20",
"2:4-18 -> 400:2-18",
"2:4-18 -> 401:2-19",
"2:4-18 -> 402:2-20",
"2:4-18 -> 403:2-16",
"2:4-18 -> 404:2-18",
"2:4-18 -> 405:2-15",
"2:4-18 -> 406:2-21",
"2:4-18 -> 407:2-23",
"2:4-18 -> 408:2-29",
"2:4-18 -> 409:2-27",
"2:4-18 -> 410:2-28",
"2:4-18 -> 411:2-29",
"2:4-18 -> 412:2-25",
"2:4-18 -> 413:2-26",
"2:4-18 -> 414:2-27",
"2:4 -> 415:2",
"2:18 -> 416:0",
"2:4 -> 418:0",
"2:4-18 -> 419:2-26",
"2:4-18 -> 420:2-26",
"2:4-18 -> 421:2-21",
"2:4-18 -> 422:2-21",
"2:4-18 -> 423:2-16",
"2:4-18 -> 424:2-16",
"2:4-18 -> 425:2-16",
"2:4-18 -> 426:2-17",
"2:4-18 -> 427:2-17",
"2:4-18 -> 428:2-15",
"2:4-18 -> 429:2-15",
"2:4-18 -> 430:2-20",
"2:4-18 -> 431:2-40",
"2:4-18 -> 432:2-17",
"2:4-18 -> 433:2-22",
"2:4-18 -> 434:2-24",
"2:4-18 -> 435:2-25",
"2:4-18 -> 436:2-26",
"2:4-18 -> 437:2-20",
"2:4-18 -> 438:2-29",
"2:4-18 -> 439:2-30",
"2:4-18 -> 440:2-40",
"2:4-18 -> 441:2-36",
"2:4-18 -> 442:2-29",
"2:4-18 -> 443:2-24",
"2:4-18 -> 444:2-32",
"2:4-18 -> 445:2-14",
"2:4-18 -> 446:2-20",
"2:4-18 -> 447:2-18",
"2:4-18 -> 448:2-19",
"2:4-18 -> 449:2-20",
"2:4-18 -> 450:2-16",
"2:4-18 -> 451:2-18",
"2:4-18 -> 452:2-15",
"2:4-18 -> 453:2-21",
"2:4-18 -> 454:2-23",
"2:4-18 -> 455:2-29",
"2:4-18 -> 456:2-27",
"2:4-18 -> 457:2-28",
"2:4-18 -> 458:2-29",
"2:4-18 -> 459:2-25",
"2:4-18 -> 460:2-26",
"2:4-18 -> 461:2-27",
"2:4 -> 462:2",
"2:18 -> 463:0",
]
`;
exports[`source maps for layer rules are not rewritten to point to @tailwind directives 1`] = `
Array [
"2:6 -> 1:0",
"2:6 -> 2:10",
"2:25 -> 3:0",
"3:8 -> 4:8",
"4:10-31 -> 5:10-31",
"5:8 -> 6:8",
"3:8 -> 7:8",
"4:10-31 -> 8:10-31",
"5:8 -> 9:8",
]
`;

View File

@ -1,276 +1,278 @@
import { run, html, css } from './util/run'
import { crosscheck, run, html, css } from './util/run'
test('basic', () => {
let config = {
content: [
{
raw: html`
<div class="animate-spin"></div>
<div class="hover:animate-ping"></div>
<div class="group-hover:animate-bounce"></div>
`,
crosscheck(() => {
test('basic', () => {
let config = {
content: [
{
raw: html`
<div class="animate-spin"></div>
<div class="hover:animate-ping"></div>
<div class="group-hover:animate-bounce"></div>
`,
},
],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
@keyframes ping {
75%,
100% {
transform: scale(2);
opacity: 0;
}
}
.hover\:animate-ping:hover {
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
}
@keyframes bounce {
0%,
100% {
transform: translateY(-25%);
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
}
50% {
transform: none;
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
}
.group:hover .group-hover\:animate-bounce {
animation: bounce 1s infinite;
}
`)
})
})
test('custom', () => {
let config = {
content: [{ raw: html`<div class="animate-one"></div>` }],
theme: {
extend: {
keyframes: {
one: { to: { transform: 'rotate(360deg)' } },
},
animation: {
one: 'one 2s',
},
},
},
],
}
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes spin {
to {
transform: rotate(360deg);
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes one {
to {
transform: rotate(360deg);
}
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
@keyframes ping {
75%,
100% {
transform: scale(2);
opacity: 0;
.animate-one {
animation: one 2s;
}
}
.hover\:animate-ping:hover {
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
}
@keyframes bounce {
0%,
100% {
transform: translateY(-25%);
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
`)
})
})
test('custom prefixed', () => {
let config = {
prefix: 'tw-',
content: [{ raw: `<div class="tw-animate-one"></div>` }],
theme: {
extend: {
keyframes: {
one: { to: { transform: 'rotate(360deg)' } },
},
animation: {
one: 'one 2s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes tw-one {
to {
transform: rotate(360deg);
}
}
50% {
transform: none;
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
.tw-animate-one {
animation: tw-one 2s;
}
}
.group:hover .group-hover\:animate-bounce {
animation: bounce 1s infinite;
}
`)
})
})
test('custom', () => {
let config = {
content: [{ raw: html`<div class="animate-one"></div>` }],
theme: {
extend: {
keyframes: {
one: { to: { transform: 'rotate(360deg)' } },
},
animation: {
one: 'one 2s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes one {
to {
transform: rotate(360deg);
}
}
.animate-one {
animation: one 2s;
}
`)
})
})
test('custom prefixed', () => {
let config = {
prefix: 'tw-',
content: [{ raw: `<div class="tw-animate-one"></div>` }],
theme: {
extend: {
keyframes: {
one: { to: { transform: 'rotate(360deg)' } },
},
animation: {
one: 'one 2s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes tw-one {
to {
transform: rotate(360deg);
}
}
.tw-animate-one {
animation: tw-one 2s;
}
`)
})
})
test('multiple', () => {
let config = {
content: [{ raw: html`<div class="animate-multiple"></div>` }],
theme: {
extend: {
animation: {
multiple: 'bounce 2s linear, pulse 3s ease-in',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes bounce {
0%,
100% {
transform: translateY(-25%);
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
}
50% {
transform: none;
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
}
@keyframes pulse {
50% {
opacity: 0.5;
}
}
.animate-multiple {
animation: bounce 2s linear, pulse 3s ease-in;
}
`)
})
})
test('multiple custom', () => {
let config = {
content: [{ raw: html`<div class="animate-multiple"></div>` }],
theme: {
extend: {
keyframes: {
one: { to: { transform: 'rotate(360deg)' } },
two: { to: { transform: 'scale(1.23)' } },
},
animation: {
multiple: 'one 2s, two 3s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes one {
to {
transform: rotate(360deg);
}
}
@keyframes two {
to {
transform: scale(1.23);
}
}
.animate-multiple {
animation: one 2s, two 3s;
}
`)
})
})
test('with dots in the name', () => {
let config = {
content: [
{
raw: html`
<div class="animate-zoom-.5"></div>
<div class="animate-zoom-1.5"></div>
`,
},
],
theme: {
extend: {
keyframes: {
'zoom-.5': { to: { transform: 'scale(0.5)' } },
'zoom-1.5': { to: { transform: 'scale(1.5)' } },
},
animation: {
'zoom-.5': 'zoom-.5 2s',
'zoom-1.5': 'zoom-1.5 2s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes zoom-\.5 {
to {
transform: scale(0.5);
}
}
.animate-zoom-\.5 {
animation: zoom-\.5 2s;
}
@keyframes zoom-1\.5 {
to {
transform: scale(1.5);
}
}
.animate-zoom-1\.5 {
animation: zoom-1\.5 2s;
}
`)
})
})
test('with dots in the name and prefix', () => {
let config = {
prefix: 'tw-',
content: [
{
raw: html`
<div class="tw-animate-zoom-.5"></div>
<div class="tw-animate-zoom-1.5"></div>
`,
},
],
theme: {
extend: {
keyframes: {
'zoom-.5': { to: { transform: 'scale(0.5)' } },
'zoom-1.5': { to: { transform: 'scale(1.5)' } },
},
animation: {
'zoom-.5': 'zoom-.5 2s',
'zoom-1.5': 'zoom-1.5 2s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes tw-zoom-\.5 {
to {
transform: scale(0.5);
}
}
.tw-animate-zoom-\.5 {
animation: tw-zoom-\.5 2s;
}
@keyframes tw-zoom-1\.5 {
to {
transform: scale(1.5);
}
}
.tw-animate-zoom-1\.5 {
animation: tw-zoom-1\.5 2s;
}
`)
`)
})
})
test('multiple', () => {
let config = {
content: [{ raw: html`<div class="animate-multiple"></div>` }],
theme: {
extend: {
animation: {
multiple: 'bounce 2s linear, pulse 3s ease-in',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes bounce {
0%,
100% {
transform: translateY(-25%);
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
}
50% {
transform: none;
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
}
@keyframes pulse {
50% {
opacity: 0.5;
}
}
.animate-multiple {
animation: bounce 2s linear, pulse 3s ease-in;
}
`)
})
})
test('multiple custom', () => {
let config = {
content: [{ raw: html`<div class="animate-multiple"></div>` }],
theme: {
extend: {
keyframes: {
one: { to: { transform: 'rotate(360deg)' } },
two: { to: { transform: 'scale(1.23)' } },
},
animation: {
multiple: 'one 2s, two 3s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes one {
to {
transform: rotate(360deg);
}
}
@keyframes two {
to {
transform: scale(1.23);
}
}
.animate-multiple {
animation: one 2s, two 3s;
}
`)
})
})
test('with dots in the name', () => {
let config = {
content: [
{
raw: html`
<div class="animate-zoom-.5"></div>
<div class="animate-zoom-1.5"></div>
`,
},
],
theme: {
extend: {
keyframes: {
'zoom-.5': { to: { transform: 'scale(0.5)' } },
'zoom-1.5': { to: { transform: 'scale(1.5)' } },
},
animation: {
'zoom-.5': 'zoom-.5 2s',
'zoom-1.5': 'zoom-1.5 2s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes zoom-\.5 {
to {
transform: scale(0.5);
}
}
.animate-zoom-\.5 {
animation: zoom-\.5 2s;
}
@keyframes zoom-1\.5 {
to {
transform: scale(1.5);
}
}
.animate-zoom-1\.5 {
animation: zoom-1\.5 2s;
}
`)
})
})
test('with dots in the name and prefix', () => {
let config = {
prefix: 'tw-',
content: [
{
raw: html`
<div class="tw-animate-zoom-.5"></div>
<div class="tw-animate-zoom-1.5"></div>
`,
},
],
theme: {
extend: {
keyframes: {
'zoom-.5': { to: { transform: 'scale(0.5)' } },
'zoom-1.5': { to: { transform: 'scale(1.5)' } },
},
animation: {
'zoom-.5': 'zoom-.5 2s',
'zoom-1.5': 'zoom-1.5 2s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes tw-zoom-\.5 {
to {
transform: scale(0.5);
}
}
.tw-animate-zoom-\.5 {
animation: tw-zoom-\.5 2s;
}
@keyframes tw-zoom-1\.5 {
to {
transform: scale(1.5);
}
}
.tw-animate-zoom-1\.5 {
animation: tw-zoom-1\.5 2s;
}
`)
})
})
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,470 +1,474 @@
import { run, html, css, defaults } from './util/run'
import { crosscheck, run, html, css, defaults } from './util/run'
test('basic arbitrary properties', () => {
let config = {
content: [
{
raw: html`<div class="[paint-order:markers]"></div>`,
},
],
corePlugins: { preflight: false },
}
crosscheck(() => {
test('basic arbitrary properties', () => {
let config = {
content: [
{
raw: html`<div class="[paint-order:markers]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[paint-order\:markers\] {
paint-order: markers;
}
`)
.\[paint-order\:markers\] {
paint-order: markers;
}
`)
})
})
})
test('different arbitrary properties are picked up separately', () => {
let config = {
content: [
{
raw: html`<div class="[foo:bar] [bar:baz]"></div>`,
},
],
corePlugins: { preflight: false },
}
test('different arbitrary properties are picked up separately', () => {
let config = {
content: [
{
raw: html`<div class="[foo:bar] [bar:baz]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[bar\:baz\] {
bar: baz;
}
.\[bar\:baz\] {
bar: baz;
}
.\[foo\:bar\] {
foo: bar;
}
`)
.\[foo\:bar\] {
foo: bar;
}
`)
})
})
})
test('arbitrary properties with modifiers', () => {
let config = {
content: [
{
raw: html`<div class="dark:lg:hover:[paint-order:markers]"></div>`,
},
],
corePlugins: { preflight: false },
}
test('arbitrary properties with modifiers', () => {
let config = {
content: [
{
raw: html`<div class="dark:lg:hover:[paint-order:markers]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
@media (min-width: 1024px) {
.dark\:lg\:hover\:\[paint-order\:markers\]:hover {
paint-order: markers;
@media (prefers-color-scheme: dark) {
@media (min-width: 1024px) {
.dark\:lg\:hover\:\[paint-order\:markers\]:hover {
paint-order: markers;
}
}
}
}
`)
})
})
test('arbitrary properties are sorted after utilities', () => {
let config = {
content: [
{
raw: html`<div class="content-none [paint-order:markers] hover:pointer-events-none"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.content-none {
--tw-content: none;
content: var(--tw-content);
}
.\[paint-order\:markers\] {
paint-order: markers;
}
.hover\:pointer-events-none:hover {
pointer-events: none;
}
`)
})
})
test('using CSS variables', () => {
let config = {
content: [
{
raw: html`<div class="[--my-var:auto]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--my-var\:auto\] {
--my-var: auto;
}
`)
})
})
test('using underscores as spaces', () => {
let config = {
content: [
{
raw: html`<div class="[--my-var:2px_4px]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--my-var\:2px_4px\] {
--my-var: 2px 4px;
}
`)
})
})
test('using the important modifier', () => {
let config = {
content: [
{
raw: html`<div class="![--my-var:2px_4px]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\!\[--my-var\:2px_4px\] {
--my-var: 2px 4px !important;
}
`)
})
})
test('colons are allowed in quotes', () => {
let config = {
content: [
{
raw: html`<div class="[content:'foo:bar']"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[content\:\'foo\:bar\'\] {
content: 'foo:bar';
}
`)
})
})
test('colons are allowed in braces', () => {
let config = {
content: [
{
raw: html`<div class="[background-image:url(http://example.com/picture.jpg)]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[background-image\:url\(http\:\/\/example\.com\/picture\.jpg\)\] {
background-image: url(http://example.com/picture.jpg);
}
`)
})
})
test('invalid class', () => {
let config = {
content: [
{
raw: html`<div class="[a:b:c:d]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
`)
})
})
test('invalid arbitrary property', () => {
let config = {
content: [
{
raw: html`<div class="[autoplay:\${autoplay}]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
`)
})
})
test('invalid arbitrary property 2', () => {
let config = {
content: [
{
raw: html`[0:02]`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
`)
})
})
test('using fractional spacing values inside theme() function', () => {
let config = {
content: [
{
raw: html`<div
class="[border:_calc(5vw_-_theme(spacing[2.5]))_double_theme('colors.fuchsia.700')]"
></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[border\:_calc\(5vw_-_theme\(spacing\[2\.5\]\)\)_double_theme\(\'colors\.fuchsia\.700\'\)\] {
border: calc(5vw - 0.625rem) double #a21caf;
}
`)
})
})
test('using multiple arbitrary props having fractional spacing values', () => {
let config = {
content: [
{
raw: html`<div
class="[height:_calc(100vh_-_theme(spacing[2.5]))] [box-shadow:_0_calc(theme(spacing[0.5])_*_-1)_theme(colors.red.400)_inset]"
></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[box-shadow\:_0_calc\(theme\(spacing\[0\.5\]\)_\*_-1\)_theme\(colors\.red\.400\)_inset\] {
box-shadow: 0 calc(0.125rem * -1) #f87171 inset;
}
.\[height\:_calc\(100vh_-_theme\(spacing\[2\.5\]\)\)\] {
height: calc(100vh - 0.625rem);
}
`)
})
})
it('should be possible to read theme values in arbitrary properties (without quotes)', () => {
let config = {
content: [{ raw: html`<div class="[--a:theme(colors.blue.500)] [color:var(--a)]"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--a\:theme\(colors\.blue\.500\)\] {
--a: #3b82f6;
}
.\[color\:var\(--a\)\] {
color: var(--a);
}
`)
})
})
it('should be possible to read theme values in arbitrary properties (with quotes)', () => {
let config = {
content: [{ raw: html`<div class="[color:var(--a)] [--a:theme('colors.blue.500')]"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--a\:theme\(\'colors\.blue\.500\'\)\] {
--a: #3b82f6;
}
.\[color\:var\(--a\)\] {
color: var(--a);
}
`)
})
})
it('should not generate invalid CSS', () => {
let config = {
content: [
{
raw: html`
<div class="[https://en.wikipedia.org/wiki]"></div>
<div class="[http://example.org]"></div>
<div class="[http://example]"></div>
<div class="[ftp://example]"></div>
<div class="[stillworks:/example]"></div>
`,
// NOTE: In this case `stillworks:/example` being generated is not ideal
// but it at least doesn't produce invalid CSS when run through prettier
// So we can let it through since it is technically valid
},
],
corePlugins: { preflight: false },
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.\[stillworks\:\/example\] {
stillworks: /example;
}
`)
`)
})
})
test('arbitrary properties are sorted after utilities', () => {
let config = {
content: [
{
raw: html`<div
class="content-none [paint-order:markers] hover:pointer-events-none"
></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.content-none {
--tw-content: none;
content: var(--tw-content);
}
.\[paint-order\:markers\] {
paint-order: markers;
}
.hover\:pointer-events-none:hover {
pointer-events: none;
}
`)
})
})
test('using CSS variables', () => {
let config = {
content: [
{
raw: html`<div class="[--my-var:auto]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--my-var\:auto\] {
--my-var: auto;
}
`)
})
})
test('using underscores as spaces', () => {
let config = {
content: [
{
raw: html`<div class="[--my-var:2px_4px]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--my-var\:2px_4px\] {
--my-var: 2px 4px;
}
`)
})
})
test('using the important modifier', () => {
let config = {
content: [
{
raw: html`<div class="![--my-var:2px_4px]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\!\[--my-var\:2px_4px\] {
--my-var: 2px 4px !important;
}
`)
})
})
test('colons are allowed in quotes', () => {
let config = {
content: [
{
raw: html`<div class="[content:'foo:bar']"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[content\:\'foo\:bar\'\] {
content: 'foo:bar';
}
`)
})
})
test('colons are allowed in braces', () => {
let config = {
content: [
{
raw: html`<div class="[background-image:url(http://example.com/picture.jpg)]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[background-image\:url\(http\:\/\/example\.com\/picture\.jpg\)\] {
background-image: url(http://example.com/picture.jpg);
}
`)
})
})
test('invalid class', () => {
let config = {
content: [
{
raw: html`<div class="[a:b:c:d]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
`)
})
})
test('invalid arbitrary property', () => {
let config = {
content: [
{
raw: html`<div class="[autoplay:\${autoplay}]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
`)
})
})
test('invalid arbitrary property 2', () => {
let config = {
content: [
{
raw: html`[0:02]`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
`)
})
})
test('using fractional spacing values inside theme() function', () => {
let config = {
content: [
{
raw: html`<div
class="[border:_calc(5vw_-_theme(spacing[2.5]))_double_theme('colors.fuchsia.700')]"
></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[border\:_calc\(5vw_-_theme\(spacing\[2\.5\]\)\)_double_theme\(\'colors\.fuchsia\.700\'\)\] {
border: calc(5vw - 0.625rem) double #a21caf;
}
`)
})
})
test('using multiple arbitrary props having fractional spacing values', () => {
let config = {
content: [
{
raw: html`<div
class="[height:_calc(100vh_-_theme(spacing[2.5]))] [box-shadow:_0_calc(theme(spacing[0.5])_*_-1)_theme(colors.red.400)_inset]"
></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[box-shadow\:_0_calc\(theme\(spacing\[0\.5\]\)_\*_-1\)_theme\(colors\.red\.400\)_inset\] {
box-shadow: 0 calc(0.125rem * -1) #f87171 inset;
}
.\[height\:_calc\(100vh_-_theme\(spacing\[2\.5\]\)\)\] {
height: calc(100vh - 0.625rem);
}
`)
})
})
it('should be possible to read theme values in arbitrary properties (without quotes)', () => {
let config = {
content: [{ raw: html`<div class="[--a:theme(colors.blue.500)] [color:var(--a)]"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--a\:theme\(colors\.blue\.500\)\] {
--a: #3b82f6;
}
.\[color\:var\(--a\)\] {
color: var(--a);
}
`)
})
})
it('should be possible to read theme values in arbitrary properties (with quotes)', () => {
let config = {
content: [{ raw: html`<div class="[color:var(--a)] [--a:theme('colors.blue.500')]"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--a\:theme\(\'colors\.blue\.500\'\)\] {
--a: #3b82f6;
}
.\[color\:var\(--a\)\] {
color: var(--a);
}
`)
})
})
it('should not generate invalid CSS', () => {
let config = {
content: [
{
raw: html`
<div class="[https://en.wikipedia.org/wiki]"></div>
<div class="[http://example.org]"></div>
<div class="[http://example]"></div>
<div class="[ftp://example]"></div>
<div class="[stillworks:/example]"></div>
`,
// NOTE: In this case `stillworks:/example` being generated is not ideal
// but it at least doesn't produce invalid CSS when run through prettier
// So we can let it through since it is technically valid
},
],
corePlugins: { preflight: false },
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.\[stillworks\:\/example\] {
stillworks: /example;
}
`)
})
})
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,117 +1,119 @@
import { run, html, css } from './util/run'
import log from '../src/util/log'
import { crosscheck, run, html, css } from './util/run'
let warn
crosscheck(() => {
let warn
beforeEach(() => {
warn = jest.spyOn(log, 'warn')
})
beforeEach(() => {
warn = jest.spyOn(log, 'warn')
})
afterEach(() => warn.mockClear())
afterEach(() => warn.mockClear())
it('can block classes matched literally', () => {
let config = {
content: [
{
raw: html`<div
class="font-bold uppercase sm:hover:text-sm hover:text-sm bg-red-500/50 my-custom-class"
></div>`,
},
],
blocklist: ['font', 'uppercase', 'hover:text-sm', 'bg-red-500/50', 'my-custom-class'],
}
let input = css`
@tailwind utilities;
.my-custom-class {
color: red;
it('can block classes matched literally', () => {
let config = {
content: [
{
raw: html`<div
class="font-bold uppercase sm:hover:text-sm hover:text-sm bg-red-500/50 my-custom-class"
></div>`,
},
],
blocklist: ['font', 'uppercase', 'hover:text-sm', 'bg-red-500/50', 'my-custom-class'],
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchCss(css`
.font-bold {
font-weight: 700;
}
let input = css`
@tailwind utilities;
.my-custom-class {
color: red;
}
@media (min-width: 640px) {
.sm\:hover\:text-sm:hover {
font-size: 0.875rem;
line-height: 1.25rem;
`
return run(input, config).then((result) => {
return expect(result.css).toMatchCss(css`
.font-bold {
font-weight: 700;
}
.my-custom-class {
color: red;
}
@media (min-width: 640px) {
.sm\:hover\:text-sm:hover {
font-size: 0.875rem;
line-height: 1.25rem;
}
}
`)
})
})
it('can block classes inside @layer', () => {
let config = {
content: [
{
raw: html`<div class="font-bold my-custom-class"></div>`,
},
],
blocklist: ['my-custom-class'],
}
let input = css`
@tailwind utilities;
@layer utilities {
.my-custom-class {
color: red;
}
}
`)
`
return run(input, config).then((result) => {
return expect(result.css).toMatchCss(css`
.font-bold {
font-weight: 700;
}
`)
})
})
})
it('can block classes inside @layer', () => {
let config = {
content: [
{
raw: html`<div class="font-bold my-custom-class"></div>`,
},
],
blocklist: ['my-custom-class'],
}
let input = css`
@tailwind utilities;
@layer utilities {
.my-custom-class {
color: red;
}
it('blocklists do NOT support regexes', async () => {
let config = {
content: [{ raw: html`<div class="font-bold bg-[#f00d1e]"></div>` }],
blocklist: [/^bg-\[[^]+\]$/],
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchCss(css`
.font-bold {
font-weight: 700;
}
`)
})
})
let result = await run('@tailwind utilities', config)
it('blocklists do NOT support regexes', async () => {
let config = {
content: [{ raw: html`<div class="font-bold bg-[#f00d1e]"></div>` }],
blocklist: [/^bg-\[[^]+\]$/],
}
let result = await run('@tailwind utilities', config)
expect(result.css).toMatchCss(css`
.bg-\[\#f00d1e\] {
--tw-bg-opacity: 1;
background-color: rgb(240 13 30 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
`)
expect(warn).toHaveBeenCalledTimes(1)
expect(warn.mock.calls.map((x) => x[0])).toEqual(['blocklist-invalid'])
})
it('can block classes generated by the safelist', () => {
let config = {
content: [{ raw: html`<div class="font-bold"></div>` }],
safelist: [{ pattern: /^bg-red-(400|500)$/ }],
blocklist: ['bg-red-500'],
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.bg-red-400 {
expect(result.css).toMatchCss(css`
.bg-\[\#f00d1e\] {
--tw-bg-opacity: 1;
background-color: rgb(248 113 113 / var(--tw-bg-opacity));
background-color: rgb(240 13 30 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
`)
expect(warn).toHaveBeenCalledTimes(1)
expect(warn.mock.calls.map((x) => x[0])).toEqual(['blocklist-invalid'])
})
it('can block classes generated by the safelist', () => {
let config = {
content: [{ raw: html`<div class="font-bold"></div>` }],
safelist: [{ pattern: /^bg-red-(400|500)$/ }],
blocklist: ['bg-red-500'],
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.bg-red-400 {
--tw-bg-opacity: 1;
background-color: rgb(248 113 113 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
`)
})
})
})

View File

@ -1,85 +1,37 @@
import { run, html, css, defaults } from './util/run'
import { crosscheck, run, html, css, defaults } from './util/run'
test('collapse adjacent rules', () => {
let config = {
content: [
{
raw: html`
<div class="custom-component"></div>
<div class="sm:custom-component"></div>
<div class="md:custom-component"></div>
<div class="lg:custom-component"></div>
<div class="font-bold"></div>
<div class="sm:font-bold"></div>
<div class="sm:text-center"></div>
<div class="md:font-bold"></div>
<div class="md:text-center"></div>
<div class="lg:font-bold"></div>
<div class="lg:text-center"></div>
<div class="some-apply-thing"></div>
`,
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [
function ({ addVariant }) {
addVariant('foo-bar', '@supports (foo: bar)')
},
],
}
crosscheck(() => {
test('collapse adjacent rules', () => {
let config = {
content: [
{
raw: html`
<div class="custom-component"></div>
<div class="sm:custom-component"></div>
<div class="md:custom-component"></div>
<div class="lg:custom-component"></div>
<div class="font-bold"></div>
<div class="sm:font-bold"></div>
<div class="sm:text-center"></div>
<div class="md:font-bold"></div>
<div class="md:text-center"></div>
<div class="lg:font-bold"></div>
<div class="lg:text-center"></div>
<div class="some-apply-thing"></div>
`,
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [
function ({ addVariant }) {
addVariant('foo-bar', '@supports (foo: bar)')
},
],
}
let input = css`
@tailwind base;
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter.woff2') format('woff2'), url('/fonts/Inter.woff') format('woff');
}
@font-face {
font-family: 'Gilroy';
src: url('/fonts/Gilroy.woff2') format('woff2'), url('/fonts/Gilroy.woff') format('woff');
}
@page {
margin: 1cm;
}
@tailwind components;
@tailwind utilities;
@layer base {
@font-face {
font-family: 'Poppins';
src: url('/fonts/Poppins.woff2') format('woff2'), url('/fonts/Poppins.woff') format('woff');
}
@font-face {
font-family: 'Proxima Nova';
src: url('/fonts/ProximaNova.woff2') format('woff2'),
url('/fonts/ProximaNova.woff') format('woff');
}
}
.foo,
.bar {
color: black;
}
.foo,
.bar {
font-weight: 700;
}
.some-apply-thing {
@apply foo-bar:md:text-black foo-bar:md:font-bold foo-bar:text-black foo-bar:font-bold md:font-bold md:text-black;
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@font-face {
font-family: 'Poppins';
src: url('/fonts/Poppins.woff2') format('woff2'), url('/fonts/Poppins.woff') format('woff');
}
@font-face {
font-family: 'Proxima Nova';
src: url('/fonts/ProximaNova.woff2') format('woff2'),
url('/fonts/ProximaNova.woff') format('woff');
}
${defaults}
let input = css`
@tailwind base;
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter.woff2') format('woff2'), url('/fonts/Inter.woff') format('woff');
@ -91,29 +43,72 @@ test('collapse adjacent rules', () => {
@page {
margin: 1cm;
}
.font-bold {
font-weight: 700;
@tailwind components;
@tailwind utilities;
@layer base {
@font-face {
font-family: 'Poppins';
src: url('/fonts/Poppins.woff2') format('woff2'),
url('/fonts/Poppins.woff') format('woff');
}
@font-face {
font-family: 'Proxima Nova';
src: url('/fonts/ProximaNova.woff2') format('woff2'),
url('/fonts/ProximaNova.woff') format('woff');
}
}
.foo,
.bar {
color: black;
}
.foo,
.bar {
font-weight: 700;
}
@supports (foo: bar) {
.some-apply-thing {
font-weight: 700;
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
}
.some-apply-thing {
@apply foo-bar:md:text-black foo-bar:md:font-bold foo-bar:text-black foo-bar:font-bold md:font-bold md:text-black;
}
@media (min-width: 768px) {
.some-apply-thing {
font-weight: 700;
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@font-face {
font-family: 'Poppins';
src: url('/fonts/Poppins.woff2') format('woff2'),
url('/fonts/Poppins.woff') format('woff');
}
@font-face {
font-family: 'Proxima Nova';
src: url('/fonts/ProximaNova.woff2') format('woff2'),
url('/fonts/ProximaNova.woff') format('woff');
}
${defaults}
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter.woff2') format('woff2'), url('/fonts/Inter.woff') format('woff');
}
@font-face {
font-family: 'Gilroy';
src: url('/fonts/Gilroy.woff2') format('woff2'), url('/fonts/Gilroy.woff') format('woff');
}
@page {
margin: 1cm;
}
.font-bold {
font-weight: 700;
}
.foo,
.bar {
color: black;
font-weight: 700;
}
@supports (foo: bar) {
.some-apply-thing {
font-weight: 700;
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
}
}
}
@supports (foo: bar) {
@media (min-width: 768px) {
.some-apply-thing {
font-weight: 700;
@ -121,49 +116,58 @@ test('collapse adjacent rules', () => {
color: rgb(0 0 0 / var(--tw-text-opacity));
}
}
}
@media (min-width: 640px) {
.sm\:text-center {
text-align: center;
@supports (foo: bar) {
@media (min-width: 768px) {
.some-apply-thing {
font-weight: 700;
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
}
}
}
.sm\:font-bold {
font-weight: 700;
@media (min-width: 640px) {
.sm\:text-center {
text-align: center;
}
.sm\:font-bold {
font-weight: 700;
}
}
}
@media (min-width: 768px) {
.md\:text-center {
text-align: center;
@media (min-width: 768px) {
.md\:text-center {
text-align: center;
}
.md\:font-bold {
font-weight: 700;
}
}
.md\:font-bold {
font-weight: 700;
@media (min-width: 1024px) {
.lg\:text-center {
text-align: center;
}
.lg\:font-bold {
font-weight: 700;
}
}
}
@media (min-width: 1024px) {
.lg\:text-center {
text-align: center;
}
.lg\:font-bold {
font-weight: 700;
}
}
`)
`)
})
})
})
test('duplicate url imports does not break rule collapsing', () => {
let config = {
content: [{ raw: html`` }],
corePlugins: { preflight: false },
}
test('duplicate url imports does not break rule collapsing', () => {
let config = {
content: [{ raw: html`` }],
corePlugins: { preflight: false },
}
let input = css`
@import url('https://example.com');
@import url('https://example.com');
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
let input = css`
@import url('https://example.com');
`)
@import url('https://example.com');
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@import url('https://example.com');
`)
})
})
})

View File

@ -1,175 +1,177 @@
import { run, css, html } from './util/run'
import { crosscheck, run, html, css } from './util/run'
it('should collapse duplicate declarations with the same units (px)', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: 100px;
height: 200px;
}
crosscheck(() => {
it('should collapse duplicate declarations with the same units (px)', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
height: 200px;
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: 100px;
height: 200px;
}
}
`)
})
})
it('should collapse duplicate declarations with the same units (no unit)', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
line-height: 3;
line-height: 2;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
line-height: 2;
}
`)
})
})
it('should not collapse duplicate declarations with the different units', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: 100px;
height: 50%;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
height: 100px;
height: 50%;
}
`)
})
})
it('should collapse the duplicate declarations with the same unit, but leave the ones with different units', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: 100px;
height: 50%;
height: 20vh;
height: 200px;
height: 100%;
height: 30vh;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
height: 200px;
height: 100%;
height: 30vh;
}
`)
})
})
it('should collapse the duplicate declarations with the exact same value', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: var(--value);
color: blue;
height: var(--value);
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
color: blue;
height: var(--value);
}
`)
})
})
it('should work on a real world example', () => {
let config = {
content: [{ raw: html`<div class="h-available"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.h-available {
height: 100%;
height: 100vh;
height: -webkit-fill-available;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.h-available {
height: 100%;
height: 100vh;
height: -webkit-fill-available;
}
`)
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
height: 200px;
}
`)
})
})
it('should collapse duplicate declarations with the same units (no unit)', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
line-height: 3;
line-height: 2;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
line-height: 2;
}
`)
})
})
it('should not collapse duplicate declarations with the different units', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: 100px;
height: 50%;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
height: 100px;
height: 50%;
}
`)
})
})
it('should collapse the duplicate declarations with the same unit, but leave the ones with different units', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: 100px;
height: 50%;
height: 20vh;
height: 200px;
height: 100%;
height: 30vh;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
height: 200px;
height: 100%;
height: 30vh;
}
`)
})
})
it('should collapse the duplicate declarations with the exact same value', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: var(--value);
color: blue;
height: var(--value);
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
color: blue;
height: var(--value);
}
`)
})
})
it('should work on a real world example', () => {
let config = {
content: [{ raw: html`<div class="h-available"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.h-available {
height: 100%;
height: 100vh;
height: -webkit-fill-available;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.h-available {
height: 100%;
height: 100vh;
height: -webkit-fill-available;
}
`)
})
})
})

View File

@ -1,234 +1,236 @@
import { run, css, html } from './util/run'
import { crosscheck, run, html, css } from './util/run'
test('basic color opacity modifier', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/50"></div>` }],
}
crosscheck(() => {
test('basic color opacity modifier', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/50"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-red-500\/50 {
background-color: rgb(239 68 68 / 0.5);
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-red-500\/50 {
background-color: rgb(239 68 68 / 0.5);
}
`)
})
})
})
test('colors with slashes are matched first', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/50"></div>` }],
theme: {
extend: {
test('colors with slashes are matched first', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/50"></div>` }],
theme: {
extend: {
colors: {
'red-500/50': '#ff0000',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-red-500\/50 {
--tw-bg-opacity: 1;
background-color: rgb(255 0 0 / var(--tw-bg-opacity));
}
`)
})
})
test('arbitrary color opacity modifier', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/[var(--opacity)]"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-red-500\/\[var\(--opacity\)\] {
background-color: rgb(239 68 68 / var(--opacity));
}
`)
})
})
test('missing alpha generates nothing', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(``)
})
})
test('arbitrary color with opacity from scale', async () => {
let config = {
content: [{ raw: 'bg-[wheat]/50' }],
theme: {},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-\[wheat\]\/50 {
background-color: rgb(245 222 179 / 0.5);
}
`)
})
})
test('arbitrary color with arbitrary opacity', async () => {
let config = {
content: [{ raw: 'bg-[#bada55]/[0.2]' }],
theme: {},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-\[\#bada55\]\/\[0\.2\] {
background-color: rgb(186 218 85 / 0.2);
}
`)
})
})
test('undefined theme color with opacity from scale', async () => {
let config = {
content: [{ raw: 'bg-garbage/50' }],
theme: {},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(``)
})
})
test('values not in the opacity config are ignored', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/29"></div>` }],
theme: {
opacity: {
0: '0',
25: '0.25',
5: '0.5',
75: '0.75',
100: '1',
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(``)
})
})
test('function colors are supported', async () => {
let config = {
content: [{ raw: html`<div class="bg-blue/50"></div>` }],
theme: {
colors: {
'red-500/50': '#ff0000',
blue: ({ opacityValue }) => {
return `rgba(var(--colors-blue), ${opacityValue})`
},
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-red-500\/50 {
--tw-bg-opacity: 1;
background-color: rgb(255 0 0 / var(--tw-bg-opacity));
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-blue\/50 {
background-color: rgba(var(--colors-blue), 0.5);
}
`)
})
})
})
test('arbitrary color opacity modifier', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/[var(--opacity)]"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-red-500\/\[var\(--opacity\)\] {
background-color: rgb(239 68 68 / var(--opacity));
}
`)
})
})
test('missing alpha generates nothing', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(``)
})
})
test('arbitrary color with opacity from scale', async () => {
let config = {
content: [{ raw: 'bg-[wheat]/50' }],
theme: {},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-\[wheat\]\/50 {
background-color: rgb(245 222 179 / 0.5);
}
`)
})
})
test('arbitrary color with arbitrary opacity', async () => {
let config = {
content: [{ raw: 'bg-[#bada55]/[0.2]' }],
theme: {},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-\[\#bada55\]\/\[0\.2\] {
background-color: rgb(186 218 85 / 0.2);
}
`)
})
})
test('undefined theme color with opacity from scale', async () => {
let config = {
content: [{ raw: 'bg-garbage/50' }],
theme: {},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(``)
})
})
test('values not in the opacity config are ignored', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/29"></div>` }],
theme: {
opacity: {
0: '0',
25: '0.25',
5: '0.5',
75: '0.75',
100: '1',
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(``)
})
})
test('function colors are supported', async () => {
let config = {
content: [{ raw: html`<div class="bg-blue/50"></div>` }],
theme: {
colors: {
blue: ({ opacityValue }) => {
return `rgba(var(--colors-blue), ${opacityValue})`
test('utilities that support any type are supported', async () => {
let config = {
content: [
{
raw: html`
<div class="from-red-500/50"></div>
<div class="fill-red-500/25"></div>
<div class="placeholder-red-500/75"></div>
`,
},
],
theme: {
extend: {
fill: (theme) => theme('colors'),
},
},
},
}
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-blue\/50 {
background-color: rgba(var(--colors-blue), 0.5);
}
`)
})
})
test('utilities that support any type are supported', async () => {
let config = {
content: [
{
raw: html`
<div class="from-red-500/50"></div>
<div class="fill-red-500/25"></div>
<div class="placeholder-red-500/75"></div>
`,
},
],
theme: {
extend: {
fill: (theme) => theme('colors'),
},
},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.from-red-500\/50 {
--tw-gradient-from: rgb(239 68 68 / 0.5);
--tw-gradient-to: rgb(239 68 68 / 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.fill-red-500\/25 {
fill: rgb(239 68 68 / 0.25);
}
.placeholder-red-500\/75::placeholder {
color: rgb(239 68 68 / 0.75);
}
`)
})
})
test('opacity modifier in combination with partial custom properties', async () => {
let config = {
content: [
{
raw: html`
<div class="bg-[hsl(var(--foo),50%,50%)]"></div>
<div class="bg-[hsl(123,var(--foo),50%)]"></div>
<div class="bg-[hsl(123,50%,var(--foo))]"></div>
<div class="bg-[hsl(var(--foo),50%,50%)]/50"></div>
<div class="bg-[hsl(123,var(--foo),50%)]/50"></div>
<div class="bg-[hsl(123,50%,var(--foo))]/50"></div>
<div class="bg-[hsl(var(--foo),var(--bar),var(--baz))]/50"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-\[hsl\(123\2c 50\%\2c var\(--foo\)\)\] {
--tw-bg-opacity: 1;
background-color: hsl(123 50% var(--foo) / var(--tw-bg-opacity));
}
.bg-\[hsl\(123\2c 50\%\2c var\(--foo\)\)\]\/50 {
background-color: hsl(123 50% var(--foo) / 0.5);
}
.bg-\[hsl\(123\2c var\(--foo\)\2c 50\%\)\] {
--tw-bg-opacity: 1;
background-color: hsl(123 var(--foo) 50% / var(--tw-bg-opacity));
}
.bg-\[hsl\(123\2c var\(--foo\)\2c 50\%\)\]\/50 {
background-color: hsl(123 var(--foo) 50% / 0.5);
}
.bg-\[hsl\(var\(--foo\)\2c 50\%\2c 50\%\)\] {
--tw-bg-opacity: 1;
background-color: hsl(var(--foo) 50% 50% / var(--tw-bg-opacity));
}
.bg-\[hsl\(var\(--foo\)\2c 50\%\2c 50\%\)\]\/50 {
background-color: hsl(var(--foo) 50% 50% / 0.5);
}
.bg-\[hsl\(var\(--foo\)\2c var\(--bar\)\2c var\(--baz\)\)\]\/50 {
background-color: hsl(var(--foo) var(--bar) var(--baz) / 0.5);
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.from-red-500\/50 {
--tw-gradient-from: rgb(239 68 68 / 0.5);
--tw-gradient-to: rgb(239 68 68 / 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.fill-red-500\/25 {
fill: rgb(239 68 68 / 0.25);
}
.placeholder-red-500\/75::placeholder {
color: rgb(239 68 68 / 0.75);
}
`)
})
})
test('opacity modifier in combination with partial custom properties', async () => {
let config = {
content: [
{
raw: html`
<div class="bg-[hsl(var(--foo),50%,50%)]"></div>
<div class="bg-[hsl(123,var(--foo),50%)]"></div>
<div class="bg-[hsl(123,50%,var(--foo))]"></div>
<div class="bg-[hsl(var(--foo),50%,50%)]/50"></div>
<div class="bg-[hsl(123,var(--foo),50%)]/50"></div>
<div class="bg-[hsl(123,50%,var(--foo))]/50"></div>
<div class="bg-[hsl(var(--foo),var(--bar),var(--baz))]/50"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-\[hsl\(123\2c 50\%\2c var\(--foo\)\)\] {
--tw-bg-opacity: 1;
background-color: hsl(123 50% var(--foo) / var(--tw-bg-opacity));
}
.bg-\[hsl\(123\2c 50\%\2c var\(--foo\)\)\]\/50 {
background-color: hsl(123 50% var(--foo) / 0.5);
}
.bg-\[hsl\(123\2c var\(--foo\)\2c 50\%\)\] {
--tw-bg-opacity: 1;
background-color: hsl(123 var(--foo) 50% / var(--tw-bg-opacity));
}
.bg-\[hsl\(123\2c var\(--foo\)\2c 50\%\)\]\/50 {
background-color: hsl(123 var(--foo) 50% / 0.5);
}
.bg-\[hsl\(var\(--foo\)\2c 50\%\2c 50\%\)\] {
--tw-bg-opacity: 1;
background-color: hsl(var(--foo) 50% 50% / var(--tw-bg-opacity));
}
.bg-\[hsl\(var\(--foo\)\2c 50\%\2c 50\%\)\]\/50 {
background-color: hsl(var(--foo) 50% 50% / 0.5);
}
.bg-\[hsl\(var\(--foo\)\2c var\(--bar\)\2c var\(--baz\)\)\]\/50 {
background-color: hsl(var(--foo) var(--bar) var(--baz) / 0.5);
}
`)
})
})
})

View File

@ -1,89 +1,92 @@
import { parseColor, formatColor } from '../src/util/color'
import { crosscheck } from './util/run'
describe('parseColor', () => {
it.each`
color | output
${'black'} | ${{ mode: 'rgb', color: ['0', '0', '0'], alpha: undefined }}
${'#0088cc'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: undefined }}
${'#08c'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: undefined }}
${'#0088cc99'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: '0.6' }}
${'#08c9'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: '0.6' }}
${'rgb(0, 30, 60)'} | ${{ mode: 'rgb', color: ['0', '30', '60'], alpha: undefined }}
${'rgba(0, 30, 60, 0.5)'} | ${{ mode: 'rgb', color: ['0', '30', '60'], alpha: '0.5' }}
${'rgb(0 30 60)'} | ${{ mode: 'rgb', color: ['0', '30', '60'], alpha: undefined }}
${'rgb(0 30 60 / 0.5)'} | ${{ mode: 'rgb', color: ['0', '30', '60'], alpha: '0.5' }}
${'rgb(var(--foo), 30, 60)'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', '60'], alpha: undefined }}
${'rgb(0, var(--foo), 60)'} | ${{ mode: 'rgb', color: ['0', 'var(--foo)', '60'], alpha: undefined }}
${'rgb(0, 30, var(--foo))'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: undefined }}
${'rgb(0, 30, var(--foo), 0.5)'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: '0.5' }}
${'rgb(var(--foo), 30, var(--bar))'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', 'var(--bar)'], alpha: undefined }}
${'rgb(var(--foo), var(--bar), var(--baz))'} | ${{ mode: 'rgb', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'rgb(var(--foo) 30 60)'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', '60'], alpha: undefined }}
${'rgb(0 var(--foo) 60)'} | ${{ mode: 'rgb', color: ['0', 'var(--foo)', '60'], alpha: undefined }}
${'rgb(0 30 var(--foo))'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: undefined }}
${'rgb(0 30 var(--foo) / 0.5)'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: '0.5' }}
${'rgb(var(--foo) 30 var(--bar))'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', 'var(--bar)'], alpha: undefined }}
${'rgb(var(--foo) var(--bar) var(--baz))'} | ${{ mode: 'rgb', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'hsl(0, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0', '30%', '60%'], alpha: undefined }}
${'hsl(0deg, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0deg', '30%', '60%'], alpha: undefined }}
${'hsl(0rad, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0rad', '30%', '60%'], alpha: undefined }}
${'hsl(0grad, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0grad', '30%', '60%'], alpha: undefined }}
${'hsl(0turn, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0turn', '30%', '60%'], alpha: undefined }}
${'hsla(0, 30%, 60%, 0.5)'} | ${{ mode: 'hsl', color: ['0', '30%', '60%'], alpha: '0.5' }}
${'hsla(0deg, 30%, 60%, 0.5)'} | ${{ mode: 'hsl', color: ['0deg', '30%', '60%'], alpha: '0.5' }}
${'hsla(0rad, 30%, 60%, 0.5)'} | ${{ mode: 'hsl', color: ['0rad', '30%', '60%'], alpha: '0.5' }}
${'hsla(0grad, 30%, 60%, 0.5)'} | ${{ mode: 'hsl', color: ['0grad', '30%', '60%'], alpha: '0.5' }}
${'hsla(0turn, 30%, 60%, 0.5)'} | ${{ mode: 'hsl', color: ['0turn', '30%', '60%'], alpha: '0.5' }}
${'hsl(0 30% 60%)'} | ${{ mode: 'hsl', color: ['0', '30%', '60%'], alpha: undefined }}
${'hsl(0deg 30% 60%)'} | ${{ mode: 'hsl', color: ['0deg', '30%', '60%'], alpha: undefined }}
${'hsl(0rad 30% 60%)'} | ${{ mode: 'hsl', color: ['0rad', '30%', '60%'], alpha: undefined }}
${'hsl(0grad 30% 60%)'} | ${{ mode: 'hsl', color: ['0grad', '30%', '60%'], alpha: undefined }}
${'hsl(0turn 30% 60%)'} | ${{ mode: 'hsl', color: ['0turn', '30%', '60%'], alpha: undefined }}
${'hsl(0 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0', '30%', '60%'], alpha: '0.5' }}
${'hsl(0deg 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0deg', '30%', '60%'], alpha: '0.5' }}
${'hsl(0rad 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0rad', '30%', '60%'], alpha: '0.5' }}
${'hsl(0grad 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0grad', '30%', '60%'], alpha: '0.5' }}
${'hsl(0turn 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0turn', '30%', '60%'], alpha: '0.5' }}
${'hsl(var(--foo), 30%, 60%)'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', '60%'], alpha: undefined }}
${'hsl(0, var(--foo), 60%)'} | ${{ mode: 'hsl', color: ['0', 'var(--foo)', '60%'], alpha: undefined }}
${'hsl(0, 30%, var(--foo))'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: undefined }}
${'hsl(0, 30%, var(--foo), 0.5)'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: '0.5' }}
${'hsl(var(--foo), 30%, var(--bar))'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', 'var(--bar)'], alpha: undefined }}
${'hsl(var(--foo), var(--bar), var(--baz))'} | ${{ mode: 'hsl', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'hsl(var(--foo) 30% 60%)'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', '60%'], alpha: undefined }}
${'hsl(0 var(--foo) 60%)'} | ${{ mode: 'hsl', color: ['0', 'var(--foo)', '60%'], alpha: undefined }}
${'hsl(0 30% var(--foo))'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: undefined }}
${'hsl(0 30% var(--foo) / 0.5)'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: '0.5' }}
${'hsl(var(--foo) 30% var(--bar))'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', 'var(--bar)'], alpha: undefined }}
${'hsl(var(--foo) var(--bar) var(--baz))'} | ${{ mode: 'hsl', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'transparent'} | ${{ mode: 'rgb', color: ['0', '0', '0'], alpha: '0' }}
`('should parse "$color" to the correct value', ({ color, output }) => {
expect(parseColor(color)).toEqual(output)
crosscheck(() => {
describe('parseColor', () => {
it.each`
color | output
${'black'} | ${{ mode: 'rgb', color: ['0', '0', '0'], alpha: undefined }}
${'#0088cc'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: undefined }}
${'#08c'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: undefined }}
${'#0088cc99'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: '0.6' }}
${'#08c9'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: '0.6' }}
${'rgb(0, 30, 60)'} | ${{ mode: 'rgb', color: ['0', '30', '60'], alpha: undefined }}
${'rgba(0, 30, 60, 0.5)'} | ${{ mode: 'rgb', color: ['0', '30', '60'], alpha: '0.5' }}
${'rgb(0 30 60)'} | ${{ mode: 'rgb', color: ['0', '30', '60'], alpha: undefined }}
${'rgb(0 30 60 / 0.5)'} | ${{ mode: 'rgb', color: ['0', '30', '60'], alpha: '0.5' }}
${'rgb(var(--foo), 30, 60)'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', '60'], alpha: undefined }}
${'rgb(0, var(--foo), 60)'} | ${{ mode: 'rgb', color: ['0', 'var(--foo)', '60'], alpha: undefined }}
${'rgb(0, 30, var(--foo))'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: undefined }}
${'rgb(0, 30, var(--foo), 0.5)'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: '0.5' }}
${'rgb(var(--foo), 30, var(--bar))'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', 'var(--bar)'], alpha: undefined }}
${'rgb(var(--foo), var(--bar), var(--baz))'} | ${{ mode: 'rgb', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'rgb(var(--foo) 30 60)'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', '60'], alpha: undefined }}
${'rgb(0 var(--foo) 60)'} | ${{ mode: 'rgb', color: ['0', 'var(--foo)', '60'], alpha: undefined }}
${'rgb(0 30 var(--foo))'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: undefined }}
${'rgb(0 30 var(--foo) / 0.5)'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: '0.5' }}
${'rgb(var(--foo) 30 var(--bar))'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', 'var(--bar)'], alpha: undefined }}
${'rgb(var(--foo) var(--bar) var(--baz))'} | ${{ mode: 'rgb', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'hsl(0, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0', '30%', '60%'], alpha: undefined }}
${'hsl(0deg, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0deg', '30%', '60%'], alpha: undefined }}
${'hsl(0rad, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0rad', '30%', '60%'], alpha: undefined }}
${'hsl(0grad, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0grad', '30%', '60%'], alpha: undefined }}
${'hsl(0turn, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0turn', '30%', '60%'], alpha: undefined }}
${'hsla(0, 30%, 60%, 0.5)'} | ${{ mode: 'hsl', color: ['0', '30%', '60%'], alpha: '0.5' }}
${'hsla(0deg, 30%, 60%, 0.5)'} | ${{ mode: 'hsl', color: ['0deg', '30%', '60%'], alpha: '0.5' }}
${'hsla(0rad, 30%, 60%, 0.5)'} | ${{ mode: 'hsl', color: ['0rad', '30%', '60%'], alpha: '0.5' }}
${'hsla(0grad, 30%, 60%, 0.5)'} | ${{ mode: 'hsl', color: ['0grad', '30%', '60%'], alpha: '0.5' }}
${'hsla(0turn, 30%, 60%, 0.5)'} | ${{ mode: 'hsl', color: ['0turn', '30%', '60%'], alpha: '0.5' }}
${'hsl(0 30% 60%)'} | ${{ mode: 'hsl', color: ['0', '30%', '60%'], alpha: undefined }}
${'hsl(0deg 30% 60%)'} | ${{ mode: 'hsl', color: ['0deg', '30%', '60%'], alpha: undefined }}
${'hsl(0rad 30% 60%)'} | ${{ mode: 'hsl', color: ['0rad', '30%', '60%'], alpha: undefined }}
${'hsl(0grad 30% 60%)'} | ${{ mode: 'hsl', color: ['0grad', '30%', '60%'], alpha: undefined }}
${'hsl(0turn 30% 60%)'} | ${{ mode: 'hsl', color: ['0turn', '30%', '60%'], alpha: undefined }}
${'hsl(0 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0', '30%', '60%'], alpha: '0.5' }}
${'hsl(0deg 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0deg', '30%', '60%'], alpha: '0.5' }}
${'hsl(0rad 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0rad', '30%', '60%'], alpha: '0.5' }}
${'hsl(0grad 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0grad', '30%', '60%'], alpha: '0.5' }}
${'hsl(0turn 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0turn', '30%', '60%'], alpha: '0.5' }}
${'hsl(var(--foo), 30%, 60%)'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', '60%'], alpha: undefined }}
${'hsl(0, var(--foo), 60%)'} | ${{ mode: 'hsl', color: ['0', 'var(--foo)', '60%'], alpha: undefined }}
${'hsl(0, 30%, var(--foo))'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: undefined }}
${'hsl(0, 30%, var(--foo), 0.5)'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: '0.5' }}
${'hsl(var(--foo), 30%, var(--bar))'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', 'var(--bar)'], alpha: undefined }}
${'hsl(var(--foo), var(--bar), var(--baz))'} | ${{ mode: 'hsl', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'hsl(var(--foo) 30% 60%)'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', '60%'], alpha: undefined }}
${'hsl(0 var(--foo) 60%)'} | ${{ mode: 'hsl', color: ['0', 'var(--foo)', '60%'], alpha: undefined }}
${'hsl(0 30% var(--foo))'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: undefined }}
${'hsl(0 30% var(--foo) / 0.5)'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: '0.5' }}
${'hsl(var(--foo) 30% var(--bar))'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', 'var(--bar)'], alpha: undefined }}
${'hsl(var(--foo) var(--bar) var(--baz))'} | ${{ mode: 'hsl', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'transparent'} | ${{ mode: 'rgb', color: ['0', '0', '0'], alpha: '0' }}
`('should parse "$color" to the correct value', ({ color, output }) => {
expect(parseColor(color)).toEqual(output)
})
it.each`
color
${'var(--my-color)'}
${'currentColor'}
${'inherit'}
${'initial'}
${'revert'}
${'unset'}
`('should return `null` for unparseable color "$color"', ({ color }) => {
expect(parseColor(color)).toBe(null)
})
})
it.each`
color
${'var(--my-color)'}
${'currentColor'}
${'inherit'}
${'initial'}
${'revert'}
${'unset'}
`('should return `null` for unparseable color "$color"', ({ color }) => {
expect(parseColor(color)).toBe(null)
})
})
describe('formatColor', () => {
it.each`
color | output
${{ mode: 'rgb', color: ['0', '0', '0'], alpha: undefined }} | ${'rgb(0 0 0)'}
${{ mode: 'rgb', color: ['0', '136', '204'], alpha: undefined }} | ${'rgb(0 136 204)'}
${{ mode: 'rgb', color: ['0', '136', '204'], alpha: '0.6' }} | ${'rgb(0 136 204 / 0.6)'}
${{ mode: 'hsl', color: ['0', '0%', '0%'], alpha: undefined }} | ${'hsl(0 0% 0%)'}
${{ mode: 'hsl', color: ['0', '136%', '204%'], alpha: undefined }} | ${'hsl(0 136% 204%)'}
${{ mode: 'hsl', color: ['0', '136%', '204%'], alpha: '0.6' }} | ${'hsl(0 136% 204% / 0.6)'}
`('should format the color pieces into a proper "$output"', ({ color, output }) => {
expect(formatColor(color)).toEqual(output)
describe('formatColor', () => {
it.each`
color | output
${{ mode: 'rgb', color: ['0', '0', '0'], alpha: undefined }} | ${'rgb(0 0 0)'}
${{ mode: 'rgb', color: ['0', '136', '204'], alpha: undefined }} | ${'rgb(0 136 204)'}
${{ mode: 'rgb', color: ['0', '136', '204'], alpha: '0.6' }} | ${'rgb(0 136 204 / 0.6)'}
${{ mode: 'hsl', color: ['0', '0%', '0%'], alpha: undefined }} | ${'hsl(0 0% 0%)'}
${{ mode: 'hsl', color: ['0', '136%', '204%'], alpha: undefined }} | ${'hsl(0 136% 204%)'}
${{ mode: 'hsl', color: ['0', '136%', '204%'], alpha: '0.6' }} | ${'hsl(0 136% 204% / 0.6)'}
`('should format the color pieces into a proper "$output"', ({ color, output }) => {
expect(formatColor(color)).toEqual(output)
})
})
})

View File

@ -1,81 +1,83 @@
import { run, html, css, defaults } from './util/run'
import { crosscheck, run, html, css, defaults } from './util/run'
it('should generate the partial selector, if only a partial is used (base layer)', () => {
let config = {
content: [{ raw: html`<div></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@layer base {
:root {
font-weight: bold;
}
/* --- */
:root,
.a {
color: black;
}
crosscheck(() => {
it('should generate the partial selector, if only a partial is used (base layer)', () => {
let config = {
content: [{ raw: html`<div></div>` }],
corePlugins: { preflight: false },
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
:root {
font-weight: bold;
let input = css`
@tailwind base;
@layer base {
:root {
font-weight: bold;
}
/* --- */
:root,
.a {
color: black;
}
}
`
/* --- */
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
:root {
font-weight: bold;
}
:root,
.a {
color: black;
/* --- */
:root,
.a {
color: black;
}
${defaults}
`)
})
})
it('should generate the partial selector, if only a partial is used (utilities layer)', () => {
let config = {
content: [{ raw: html`<div class="a"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
@layer utilities {
:root {
font-weight: bold;
}
/* --- */
:root,
.a {
color: black;
}
}
`
${defaults}
`)
})
})
it('should generate the partial selector, if only a partial is used (utilities layer)', () => {
let config = {
content: [{ raw: html`<div class="a"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
@layer utilities {
:root {
font-weight: bold;
}
/* --- */
:root,
.a {
color: black;
}
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
:root {
font-weight: bold;
}
/* --- */
:root,
.a {
color: black;
}
`)
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
:root {
font-weight: bold;
}
/* --- */
:root,
.a {
color: black;
}
`)
})
})
})

View File

@ -1,25 +1,28 @@
import configurePlugins from '../src/util/configurePlugins'
import { crosscheck } from './util/run'
test('setting a plugin to false removes it', () => {
const plugins = ['fontSize', 'display', 'backgroundPosition']
crosscheck(() => {
test('setting a plugin to false removes it', () => {
const plugins = ['fontSize', 'display', 'backgroundPosition']
const configuredPlugins = configurePlugins({ display: false }, plugins)
const configuredPlugins = configurePlugins({ display: false }, plugins)
expect(configuredPlugins).toEqual(['fontSize', 'backgroundPosition'])
})
test('passing only false removes all plugins', () => {
const plugins = ['fontSize', 'display', 'backgroundPosition']
const configuredPlugins = configurePlugins(false, plugins)
expect(configuredPlugins).toEqual([])
})
test('passing an array safelists plugins', () => {
const plugins = ['fontSize', 'display', 'backgroundPosition']
const configuredPlugins = configurePlugins(['display'], plugins)
expect(configuredPlugins).toEqual(['display'])
expect(configuredPlugins).toEqual(['fontSize', 'backgroundPosition'])
})
test('passing only false removes all plugins', () => {
const plugins = ['fontSize', 'display', 'backgroundPosition']
const configuredPlugins = configurePlugins(false, plugins)
expect(configuredPlugins).toEqual([])
})
test('passing an array safelists plugins', () => {
const plugins = ['fontSize', 'display', 'backgroundPosition']
const configuredPlugins = configurePlugins(['display'], plugins)
expect(configuredPlugins).toEqual(['display'])
})
})

View File

@ -1,345 +1,347 @@
import { run, html, css } from './util/run'
import { crosscheck, run, html, css } from './util/run'
test('options are not required', () => {
let config = { content: [{ raw: html`<div class="container"></div>` }] }
crosscheck(({}) => {
test('options are not required', () => {
let config = { content: [{ raw: html`<div class="container"></div>` }] }
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 640px) {
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
max-width: 640px;
width: 100%;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
}
`)
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
`)
})
})
})
test('screens can be passed explicitly', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 400px) {
.container {
max-width: 400px;
}
}
@media (min-width: 500px) {
.container {
max-width: 500px;
}
}
`)
})
})
test('screens are ordered ascending by min-width', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['500px', '400px'],
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 400px) {
.container {
max-width: 400px;
}
}
@media (min-width: 500px) {
.container {
max-width: 500px;
}
}
`)
})
})
test('screens are deduplicated by min-width', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: {
sm: '576px',
md: '768px',
'sm-only': { min: '576px', max: '767px' },
test('screens can be passed explicitly', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
},
},
},
}
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 576px) {
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
max-width: 576px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
`)
})
})
test('the container can be centered by default', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
center: true,
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
margin-right: auto;
margin-left: auto;
}
@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;
}
}
`)
})
})
test('horizontal padding can be included by default', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
padding: '2rem',
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
padding-right: 2rem;
padding-left: 2rem;
}
@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;
}
}
`)
})
})
test('responsive horizontal padding can be included by default', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
screens: {
sm: '576px',
md: { min: '768px' },
lg: { 'min-width': '992px' },
xl: { min: '1200px', max: '1600px' },
},
container: {
padding: {
DEFAULT: '1rem',
sm: '2rem',
lg: '4rem',
xl: '5rem',
},
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
padding-right: 1rem;
padding-left: 1rem;
}
@media (min-width: 576px) {
.container {
max-width: 576px;
padding-right: 2rem;
padding-left: 2rem;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 992px) {
.container {
max-width: 992px;
padding-right: 4rem;
padding-left: 4rem;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1200px;
padding-right: 5rem;
padding-left: 5rem;
}
}
`)
})
})
test('setting all options at once', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
center: true,
padding: '2rem',
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
margin-right: auto;
margin-left: auto;
padding-right: 2rem;
padding-left: 2rem;
}
@media (min-width: 400px) {
.container {
max-width: 400px;
}
}
@media (min-width: 500px) {
.container {
max-width: 500px;
}
}
`)
})
})
test('container can use variants', () => {
let config = {
content: [{ raw: html`<div class="lg:hover:container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 1024px) {
.lg\:hover\:container:hover {
width: 100%;
}
@media (min-width: 400px) {
.lg\:hover\:container:hover {
.container {
max-width: 400px;
}
}
@media (min-width: 500px) {
.lg\:hover\:container:hover {
.container {
max-width: 500px;
}
}
}
`)
`)
})
})
test('screens are ordered ascending by min-width', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['500px', '400px'],
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 400px) {
.container {
max-width: 400px;
}
}
@media (min-width: 500px) {
.container {
max-width: 500px;
}
}
`)
})
})
test('screens are deduplicated by min-width', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: {
sm: '576px',
md: '768px',
'sm-only': { min: '576px', max: '767px' },
},
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 576px) {
.container {
max-width: 576px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
`)
})
})
test('the container can be centered by default', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
center: true,
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
margin-right: auto;
margin-left: auto;
}
@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;
}
}
`)
})
})
test('horizontal padding can be included by default', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
padding: '2rem',
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
padding-right: 2rem;
padding-left: 2rem;
}
@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;
}
}
`)
})
})
test('responsive horizontal padding can be included by default', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
screens: {
sm: '576px',
md: { min: '768px' },
lg: { 'min-width': '992px' },
xl: { min: '1200px', max: '1600px' },
},
container: {
padding: {
DEFAULT: '1rem',
sm: '2rem',
lg: '4rem',
xl: '5rem',
},
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
padding-right: 1rem;
padding-left: 1rem;
}
@media (min-width: 576px) {
.container {
max-width: 576px;
padding-right: 2rem;
padding-left: 2rem;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 992px) {
.container {
max-width: 992px;
padding-right: 4rem;
padding-left: 4rem;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1200px;
padding-right: 5rem;
padding-left: 5rem;
}
}
`)
})
})
test('setting all options at once', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
center: true,
padding: '2rem',
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
margin-right: auto;
margin-left: auto;
padding-right: 2rem;
padding-left: 2rem;
}
@media (min-width: 400px) {
.container {
max-width: 400px;
}
}
@media (min-width: 500px) {
.container {
max-width: 500px;
}
}
`)
})
})
test('container can use variants', () => {
let config = {
content: [{ raw: html`<div class="lg:hover:container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 1024px) {
.lg\:hover\:container:hover {
width: 100%;
}
@media (min-width: 400px) {
.lg\:hover\:container:hover {
max-width: 400px;
}
}
@media (min-width: 500px) {
.lg\:hover\:container:hover {
max-width: 500px;
}
}
}
`)
})
})
})

View File

@ -1,127 +1,132 @@
import { crosscheck, css } from './util/run'
const fs = require('fs')
const path = require('path')
const postcss = require('postcss')
const tailwind = require('../src/index.js')
const sharedState = require('../src/lib/sharedState.js')
const configPath = path.resolve(__dirname, './context-reuse.tailwind.config.js')
const { css } = require('./util/run.js')
function run(input, config = {}, from = null) {
let { currentTestName } = expect.getState()
crosscheck(({ stable, oxide }) => {
function run(input, config = {}, from = null) {
let { currentTestName } = expect.getState()
from = `${path.resolve(__filename)}?test=${currentTestName}&${from}`
from = `${path.resolve(__filename)}?test=${currentTestName}&${from}`
return postcss(tailwind(config)).process(input, { from })
}
beforeEach(async () => {
let config = {
content: [path.resolve(__dirname, './context-reuse.test.html')],
corePlugins: { preflight: false },
return postcss(tailwind(config)).process(input, { from })
}
await fs.promises.writeFile(configPath, `module.exports = ${JSON.stringify(config)};`)
})
beforeEach(async () => {
let config = {
content: [path.resolve(__dirname, './context-reuse.test.html')],
corePlugins: { preflight: false },
}
afterEach(async () => {
await fs.promises.unlink(configPath)
})
it('re-uses the context across multiple files with the same config', async () => {
let results = [
await run(`@tailwind utilities;`, configPath, `id=1`),
// Using @apply directives should still re-use the context
// They depend on the config but do not the other way around
await run(`body { @apply bg-blue-400; }`, configPath, `id=2`),
await run(`body { @apply text-red-400; }`, configPath, `id=3`),
await run(`body { @apply mb-4; }`, configPath, `id=4`),
]
let dependencies = results.map((result) => {
return result.messages
.filter((message) => message.type === 'dependency')
.map((message) => message.file)
await fs.promises.writeFile(configPath, `module.exports = ${JSON.stringify(config)};`)
})
// The content files don't have any utilities in them so this should be empty
expect(results[0].css).toMatchFormattedCss(css``)
afterEach(async () => {
await fs.promises.unlink(configPath)
})
// However, @apply is being used so we want to verify that they're being inlined into the CSS rules
expect(results[1].css).toMatchFormattedCss(css`
body {
--tw-bg-opacity: 1;
background-color: rgb(96 165 250 / var(--tw-bg-opacity));
}
`)
oxide.test.todo('re-uses the context across multiple files with the same config')
stable.test('re-uses the context across multiple files with the same config', async () => {
let results = [
await run(`@tailwind utilities;`, configPath, `id=1`),
expect(results[2].css).toMatchFormattedCss(css`
body {
--tw-text-opacity: 1;
color: rgb(248 113 113 / var(--tw-text-opacity));
}
`)
// Using @apply directives should still re-use the context
// They depend on the config but do not the other way around
await run(`body { @apply bg-blue-400; }`, configPath, `id=2`),
await run(`body { @apply text-red-400; }`, configPath, `id=3`),
await run(`body { @apply mb-4; }`, configPath, `id=4`),
]
expect(results[3].css).toMatchFormattedCss(css`
body {
margin-bottom: 1rem;
}
`)
let dependencies = results.map((result) => {
return result.messages
.filter((message) => message.type === 'dependency')
.map((message) => message.file)
})
// Files with @tailwind directives depends on the PostCSS tree, config, AND any content files
expect(dependencies[0]).toEqual([
path.resolve(__dirname, 'context-reuse.test.html'),
path.resolve(__dirname, 'context-reuse.tailwind.config.js'),
])
// The content files don't have any utilities in them so this should be empty
expect(results[0].css).toMatchFormattedCss(css``)
// @apply depends only on the containing PostCSS tree *and* the config file but no content files
// as they cannot affect the outcome of the @apply directives
expect(dependencies[1]).toEqual([path.resolve(__dirname, 'context-reuse.tailwind.config.js')])
// However, @apply is being used so we want to verify that they're being inlined into the CSS rules
expect(results[1].css).toMatchFormattedCss(css`
body {
--tw-bg-opacity: 1;
background-color: rgb(96 165 250 / var(--tw-bg-opacity));
}
`)
expect(dependencies[2]).toEqual([path.resolve(__dirname, 'context-reuse.tailwind.config.js')])
expect(results[2].css).toMatchFormattedCss(css`
body {
--tw-text-opacity: 1;
color: rgb(248 113 113 / var(--tw-text-opacity));
}
`)
expect(dependencies[3]).toEqual([path.resolve(__dirname, 'context-reuse.tailwind.config.js')])
expect(results[3].css).toMatchFormattedCss(css`
body {
margin-bottom: 1rem;
}
`)
// And none of this should have resulted in multiple contexts being created
expect(sharedState.contextSourcesMap.size).toBe(1)
})
// Files with @tailwind directives depends on the PostCSS tree, config, AND any content files
expect(dependencies[0]).toEqual([
path.resolve(__dirname, 'context-reuse.test.html'),
path.resolve(__dirname, 'context-reuse.tailwind.config.js'),
])
it('updates layers when any CSS containing @tailwind directives changes', async () => {
let result
// @apply depends only on the containing PostCSS tree *and* the config file but no content files
// as they cannot affect the outcome of the @apply directives
expect(dependencies[1]).toEqual([path.resolve(__dirname, 'context-reuse.tailwind.config.js')])
// Compile the initial version once
let input = css`
@tailwind utilities;
@layer utilities {
.custom-utility {
expect(dependencies[2]).toEqual([path.resolve(__dirname, 'context-reuse.tailwind.config.js')])
expect(dependencies[3]).toEqual([path.resolve(__dirname, 'context-reuse.tailwind.config.js')])
// And none of this should have resulted in multiple contexts being created
expect(sharedState.contextSourcesMap.size).toBe(1)
})
oxide.test.todo('updates layers when any CSS containing @tailwind directives changes')
stable.test('updates layers when any CSS containing @tailwind directives changes', async () => {
let result
// Compile the initial version once
let input = css`
@tailwind utilities;
@layer utilities {
.custom-utility {
color: orange;
}
}
`
result = await run(input, configPath, `id=1`)
expect(result.css).toMatchFormattedCss(css`
.only\:custom-utility:only-child {
color: orange;
}
}
`
`)
result = await run(input, configPath, `id=1`)
// Save the file with a change
input = css`
@tailwind utilities;
@layer utilities {
.custom-utility {
color: blue;
}
}
`
expect(result.css).toMatchFormattedCss(css`
.only\:custom-utility:only-child {
color: orange;
}
`)
result = await run(input, configPath, `id=1`)
// Save the file with a change
input = css`
@tailwind utilities;
@layer utilities {
.custom-utility {
expect(result.css).toMatchFormattedCss(css`
.only\:custom-utility:only-child {
color: blue;
}
}
`
result = await run(input, configPath, `id=1`)
expect(result.css).toMatchFormattedCss(css`
.only\:custom-utility:only-child {
color: blue;
}
`)
`)
})
})

View File

@ -1,5 +1,4 @@
import { run, html, css } from './util/run'
import { env } from '../src/lib/sharedState'
import { crosscheck, run, html, css } from './util/run'
function customExtractor(content) {
let matches = content.match(/class="([^"]+)"/)
@ -22,111 +21,117 @@ let expected = css`
}
`
let group = env.OXIDE ? describe.skip : describe
group('modern', () => {
test('extract.DEFAULT', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
extract: {
DEFAULT: customExtractor,
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
test('extract.{extension}', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
extract: {
html: customExtractor,
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
test('extract function', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
extract: customExtractor,
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
test('raw content with extension', () => {
let config = {
content: {
files: [
{
raw: sharedHtml,
extension: 'html',
crosscheck(({ stable, oxide }) => {
describe('modern', () => {
oxide.test.todo('extract.DEFAULT')
stable.test('extract.DEFAULT', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
extract: {
DEFAULT: customExtractor,
},
],
extract: {
html: () => ['invisible'],
},
},
corePlugins: { preflight: false },
}
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.invisible {
visibility: hidden;
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
})
})
group('legacy', () => {
test('defaultExtractor', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
options: {
defaultExtractor: customExtractor,
oxide.test.todo('extract.{extension}')
stable.test('extract.{extension}', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
extract: {
html: customExtractor,
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
})
test('extractors array', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
options: {
extractors: [
oxide.test.todo('extract function')
stable.test('extract function', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
extract: customExtractor,
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
oxide.test.todo('raw content with extension')
stable.test('raw content with extension', () => {
let config = {
content: {
files: [
{
extractor: customExtractor,
extensions: ['html'],
raw: sharedHtml,
extension: 'html',
},
],
extract: {
html: () => ['invisible'],
},
},
},
}
corePlugins: { preflight: false },
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.invisible {
visibility: hidden;
}
`)
})
})
})
describe('legacy', () => {
oxide.test.todo('defaultExtractor')
stable.test('defaultExtractor', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
options: {
defaultExtractor: customExtractor,
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
oxide.test.todo('extractors array')
stable.test('extractors array', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
options: {
extractors: [
{
extractor: customExtractor,
extensions: ['html'],
},
],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -1,55 +1,57 @@
import { run, html, css } from './util/run'
import { crosscheck, run, html, css } from './util/run'
test('custom separator', () => {
let config = {
darkMode: 'class',
content: [
{
raw: html`
<div class="md_hover_text-right"></div>
<div class="motion-safe_hover_text-center"></div>
<div class="dark_focus_text-left"></div>
<div class="group-hover_focus-within_text-left"></div>
<div class="rtl_active_text-center"></div>
`,
},
],
separator: '_',
}
crosscheck(() => {
test('custom separator', () => {
let config = {
darkMode: 'class',
content: [
{
raw: html`
<div class="md_hover_text-right"></div>
<div class="motion-safe_hover_text-center"></div>
<div class="dark_focus_text-left"></div>
<div class="group-hover_focus-within_text-left"></div>
<div class="rtl_active_text-center"></div>
`,
},
],
separator: '_',
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.group:hover .group-hover_focus-within_text-left:focus-within {
text-align: left;
}
[dir='rtl'] .rtl_active_text-center:active {
text-align: center;
}
@media (prefers-reduced-motion: no-preference) {
.motion-safe_hover_text-center:hover {
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.group:hover .group-hover_focus-within_text-left:focus-within {
text-align: left;
}
[dir='rtl'] .rtl_active_text-center:active {
text-align: center;
}
}
.dark .dark_focus_text-left:focus {
text-align: left;
}
@media (min-width: 768px) {
.md_hover_text-right:hover {
text-align: right;
@media (prefers-reduced-motion: no-preference) {
.motion-safe_hover_text-center:hover {
text-align: center;
}
}
}
`)
.dark .dark_focus_text-left:focus {
text-align: left;
}
@media (min-width: 768px) {
.md_hover_text-right:hover {
text-align: right;
}
}
`)
})
})
test('dash is not supported', () => {
let config = {
darkMode: 'class',
content: [{ raw: 'lg-hover-font-bold' }],
separator: '-',
}
return expect(run('@tailwind utilities', config)).rejects.toThrowError(
"The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead."
)
})
})
test('dash is not supported', () => {
let config = {
darkMode: 'class',
content: [{ raw: 'lg-hover-font-bold' }],
separator: '-',
}
return expect(run('@tailwind utilities', config)).rejects.toThrowError(
"The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead."
)
})

View File

@ -1,70 +1,72 @@
import { run, html, css } from './util/run'
import { env } from '../src/lib/sharedState'
let t = env.OXIDE ? test.skip : test
import { crosscheck, run, html, css } from './util/run'
function customTransformer(content) {
return content.replace(/uppercase/g, 'lowercase')
}
t('transform function', () => {
let config = {
content: {
files: [{ raw: html`<div class="uppercase"></div>` }],
transform: customTransformer,
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.lowercase {
text-transform: lowercase;
}
`)
})
})
t('transform.DEFAULT', () => {
let config = {
content: {
files: [{ raw: html`<div class="uppercase"></div>` }],
transform: {
DEFAULT: customTransformer,
crosscheck(({ stable, oxide }) => {
oxide.test.todo('transform function')
stable.test('transform function', () => {
let config = {
content: {
files: [{ raw: html`<div class="uppercase"></div>` }],
transform: customTransformer,
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.lowercase {
text-transform: lowercase;
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.lowercase {
text-transform: lowercase;
}
`)
})
})
})
t('transform.{extension}', () => {
let config = {
content: {
files: [
{ raw: 'blah blah blah', extension: 'html' },
{ raw: 'blah blah blah', extension: 'php' },
],
transform: {
html: () => 'uppercase',
php: () => 'lowercase',
oxide.test.todo('transform.DEFAULT')
stable.test('transform.DEFAULT', () => {
let config = {
content: {
files: [{ raw: html`<div class="uppercase"></div>` }],
transform: {
DEFAULT: customTransformer,
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.uppercase {
text-transform: uppercase;
}
.lowercase {
text-transform: lowercase;
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.lowercase {
text-transform: lowercase;
}
`)
})
})
oxide.test.todo('transform.{extension}')
stable.test('transform.{extension}', () => {
let config = {
content: {
files: [
{ raw: 'blah blah blah', extension: 'html' },
{ raw: 'blah blah blah', extension: 'php' },
],
transform: {
html: () => 'uppercase',
php: () => 'lowercase',
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.uppercase {
text-transform: uppercase;
}
.lowercase {
text-transform: lowercase;
}
`)
})
})
})

View File

@ -3,97 +3,34 @@ import path from 'path'
import { cjsConfigFile, defaultConfigFile } from '../src/constants'
import inTempDirectory from '../jest/runInTempDirectory'
import { run, html, css, javascript } from './util/run'
import { crosscheck, run, html, css, javascript } from './util/run'
test('it uses the values from the custom config file', () => {
let config = require(path.resolve(`${__dirname}/fixtures/custom-config.js`))
crosscheck(() => {
test('it uses the values from the custom config file', () => {
let config = require(path.resolve(`${__dirname}/fixtures/custom-config.js`))
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
}
`)
`)
})
})
})
test('custom config can be passed as an object', () => {
let config = {
content: [{ raw: html`<div class="mobile:font-bold"></div>` }],
theme: {
screens: {
mobile: '400px',
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
test('custom config path can be passed using `config` property in an object', () => {
let config = {
config: path.resolve(`${__dirname}/fixtures/custom-config.js`),
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
test('custom config can be passed under the `config` property', () => {
let config = {
config: {
test('custom config can be passed as an object', () => {
let config = {
content: [{ raw: html`<div class="mobile:font-bold"></div>` }],
theme: {
screens: {
mobile: '400px',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
test('tailwind.config.cjs is picked up by default', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(cjsConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return run('@tailwind utilities').then((result) => {
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
@ -103,23 +40,13 @@ test('tailwind.config.cjs is picked up by default', () => {
`)
})
})
})
test('tailwind.config.js is picked up by default', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(defaultConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
},
},
}`
)
test('custom config path can be passed using `config` property in an object', () => {
let config = {
config: path.resolve(`${__dirname}/fixtures/custom-config.js`),
}
return run('@tailwind utilities').then((result) => {
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
@ -129,258 +56,333 @@ test('tailwind.config.js is picked up by default', () => {
`)
})
})
})
test('tailwind.config.cjs is picked up by default when passing an empty object', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(cjsConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
test('custom config can be passed under the `config` property', () => {
let config = {
config: {
content: [{ raw: html`<div class="mobile:font-bold"></div>` }],
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return run('@tailwind utilities', {}).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
})
test('tailwind.config.js is picked up by default when passing an empty object', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(defaultConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return run('@tailwind utilities', {}).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
})
test('the default config can be overridden using the presets key', () => {
let config = {
content: [{ raw: html`<div class="min-h-primary min-h-secondary min-h-0"></div>` }],
presets: [
{
theme: {
extend: { minHeight: { secondary: '24px' } },
},
},
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
}
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-0 {
min-height: 0px;
}
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
})
test('presets can be functions', () => {
let config = {
content: [{ raw: html`<div class="min-h-primary min-h-secondary min-h-0"></div>` }],
presets: [
() => ({
test('tailwind.config.cjs is picked up by default', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(cjsConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
extend: { minHeight: { secondary: '24px' } },
screens: {
mobile: '400px',
},
},
}),
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
}
}`
)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-0 {
min-height: 0px;
}
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
return run('@tailwind utilities').then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
})
})
test('the default config can be removed by using an empty presets key in a preset', () => {
let config = {
content: [{ raw: html`<div class="min-h-primary min-h-secondary min-h-0"></div>` }],
presets: [
{
presets: [],
test('tailwind.config.js is picked up by default', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(defaultConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
extend: { minHeight: { secondary: '24px' } },
screens: {
mobile: '400px',
},
},
}`
)
return run('@tailwind utilities').then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
})
test('tailwind.config.cjs is picked up by default when passing an empty object', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(cjsConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return run('@tailwind utilities', {}).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
})
test('tailwind.config.js is picked up by default when passing an empty object', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(defaultConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return run('@tailwind utilities', {}).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
})
test('the default config can be overridden using the presets key', () => {
let config = {
content: [{ raw: html`<div class="min-h-primary min-h-secondary min-h-0"></div>` }],
presets: [
{
theme: {
extend: { minHeight: { secondary: '24px' } },
},
},
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
}
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-0 {
min-height: 0px;
}
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
})
})
})
test('presets can have their own presets', () => {
let config = {
content: [{ raw: html`<div class="bg-red bg-transparent bg-black bg-white"></div>` }],
presets: [
{
presets: [],
theme: {
colors: { red: '#dd0000' },
},
test('presets can be functions', () => {
let config = {
content: [{ raw: html`<div class="min-h-primary min-h-secondary min-h-0"></div>` }],
presets: [
() => ({
theme: {
extend: { minHeight: { secondary: '24px' } },
},
}),
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
{
presets: [
{
presets: [],
theme: {
colors: {
transparent: 'transparent',
red: '#ff0000',
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-0 {
min-height: 0px;
}
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
})
})
test('the default config can be removed by using an empty presets key in a preset', () => {
let config = {
content: [{ raw: html`<div class="min-h-primary min-h-secondary min-h-0"></div>` }],
presets: [
{
presets: [],
theme: {
extend: { minHeight: { secondary: '24px' } },
},
},
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
})
})
test('presets can have their own presets', () => {
let config = {
content: [{ raw: html`<div class="bg-red bg-transparent bg-black bg-white"></div>` }],
presets: [
{
presets: [],
theme: {
colors: { red: '#dd0000' },
},
},
{
presets: [
{
presets: [],
theme: {
colors: {
transparent: 'transparent',
red: '#ff0000',
},
},
},
},
],
theme: {
extend: {
colors: {
black: 'black',
red: '#ee0000',
},
backgroundColor: (theme) => theme('colors'),
},
},
corePlugins: ['backgroundColor'],
},
],
theme: {
extend: { colors: { white: 'white' } },
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-black {
background-color: black;
}
.bg-red {
background-color: #ee0000;
}
.bg-transparent {
background-color: transparent;
}
.bg-white {
background-color: white;
}
`)
})
})
test('function presets can be mixed with object presets', () => {
let config = {
content: [{ raw: html`<div class="bg-red bg-transparent bg-black bg-white"></div>` }],
presets: [
() => ({
presets: [],
theme: {
colors: { red: '#dd0000' },
},
}),
{
presets: [
() => ({
presets: [],
theme: {
],
theme: {
extend: {
colors: {
transparent: 'transparent',
red: '#ff0000',
black: 'black',
red: '#ee0000',
},
backgroundColor: (theme) => theme('colors'),
},
}),
],
theme: {
extend: {
colors: {
black: 'black',
red: '#ee0000',
},
backgroundColor: (theme) => theme('colors'),
},
corePlugins: ['backgroundColor'],
},
corePlugins: ['backgroundColor'],
],
theme: {
extend: { colors: { white: 'white' } },
},
],
theme: {
extend: { colors: { white: 'white' } },
},
}
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-black {
background-color: black;
}
.bg-red {
background-color: #ee0000;
}
.bg-transparent {
background-color: transparent;
}
.bg-white {
background-color: white;
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-black {
background-color: black;
}
.bg-red {
background-color: #ee0000;
}
.bg-transparent {
background-color: transparent;
}
.bg-white {
background-color: white;
}
`)
})
})
test('function presets can be mixed with object presets', () => {
let config = {
content: [{ raw: html`<div class="bg-red bg-transparent bg-black bg-white"></div>` }],
presets: [
() => ({
presets: [],
theme: {
colors: { red: '#dd0000' },
},
}),
{
presets: [
() => ({
presets: [],
theme: {
colors: {
transparent: 'transparent',
red: '#ff0000',
},
},
}),
],
theme: {
extend: {
colors: {
black: 'black',
red: '#ee0000',
},
backgroundColor: (theme) => theme('colors'),
},
},
corePlugins: ['backgroundColor'],
},
],
theme: {
extend: { colors: { white: 'white' } },
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-black {
background-color: black;
}
.bg-red {
background-color: #ee0000;
}
.bg-transparent {
background-color: transparent;
}
.bg-white {
background-color: white;
}
`)
})
})
})

View File

@ -1,123 +1,125 @@
import { run, html, css, defaults } from './util/run'
import { crosscheck, run, html, css, defaults } from './util/run'
it('should be possible to use the darkMode "class" mode', () => {
let config = {
darkMode: 'class',
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
crosscheck(() => {
it('should be possible to use the darkMode "class" mode', () => {
let config = {
darkMode: 'class',
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.dark .dark\:font-bold {
font-weight: 700;
}
`)
})
})
it('should be possible to change the class name', () => {
let config = {
darkMode: ['class', '.test-dark'],
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.test-dark .dark\:font-bold {
font-weight: 700;
}
`)
})
})
it('should be possible to use the darkMode "media" mode', () => {
let config = {
darkMode: 'media',
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
.dark\:font-bold {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.dark .dark\:font-bold {
font-weight: 700;
}
}
`)
`)
})
})
})
it('should default to the `media` mode when no mode is provided', () => {
let config = {
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
it('should be possible to change the class name', () => {
let config = {
darkMode: ['class', '.test-dark'],
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
.dark\:font-bold {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.test-dark .dark\:font-bold {
font-weight: 700;
}
}
`)
`)
})
})
})
it('should default to the `media` mode when mode is set to `false`', () => {
let config = {
darkMode: false,
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
it('should be possible to use the darkMode "media" mode', () => {
let config = {
darkMode: 'media',
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
.dark\:font-bold {
font-weight: 700;
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
.dark\:font-bold {
font-weight: 700;
}
}
}
`)
`)
})
})
it('should default to the `media` mode when no mode is provided', () => {
let config = {
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
.dark\:font-bold {
font-weight: 700;
}
}
`)
})
})
it('should default to the `media` mode when mode is set to `false`', () => {
let config = {
darkMode: false,
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
.dark\:font-bold {
font-weight: 700;
}
}
`)
})
})
})

View File

@ -1,6 +1,8 @@
import config from '../src/public/default-config'
import configStub from '../stubs/defaultConfig.stub.js'
test.todo('remove mutation from these tests so we can run against both engines')
test('the default config matches the stub', () => {
expect(config).toEqual(configStub)
})

View File

@ -1,6 +1,8 @@
import theme from '../src/public/default-theme'
import configStub from '../stubs/defaultConfig.stub.js'
test.todo('remove mutation from these tests so we can run against both engines')
test('the default theme matches the stub', () => {
expect(theme).toEqual(configStub.theme)
})

View File

@ -1,128 +1,130 @@
import { run, html, css } from './util/run'
import { crosscheck, run, html, css } from './util/run'
it('should warn when we detect nested css', () => {
let config = {
content: [{ raw: html`<div class="nested"></div>` }],
}
let input = css`
@tailwind utilities;
.nested {
.example {
}
crosscheck(() => {
it('should warn when we detect nested css', () => {
let config = {
content: [{ raw: html`<div class="nested"></div>` }],
}
`
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(1)
expect(result.messages).toMatchObject([
{
type: 'warning',
text: [
'Nested CSS was detected, but CSS nesting has not been configured correctly.',
'Please enable a CSS nesting plugin *before* Tailwind in your configuration.',
'See how here: https://tailwindcss.com/docs/using-with-preprocessors#nesting',
].join('\n'),
},
])
})
})
it('should not warn when we detect nested css inside css @layer rules', () => {
let config = {
content: [{ raw: html`<div class="underline"></div>` }],
}
let input = css`
@layer tw-base, tw-components, tw-utilities;
@layer tw-utilities {
let input = css`
@tailwind utilities;
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@layer tw-base, tw-components, tw-utilities;
@layer tw-utilities {
.underline {
text-decoration-line: underline;
.nested {
.example {
}
}
`)
expect(result.messages).toHaveLength(0)
`
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(1)
expect(result.messages).toMatchObject([
{
type: 'warning',
text: [
'Nested CSS was detected, but CSS nesting has not been configured correctly.',
'Please enable a CSS nesting plugin *before* Tailwind in your configuration.',
'See how here: https://tailwindcss.com/docs/using-with-preprocessors#nesting',
].join('\n'),
},
])
})
})
})
it('should warn when we detect namespaced @tailwind at rules', () => {
let config = {
content: [{ raw: html`<div class="text-center"></div>` }],
}
let input = css`
.namespace {
@tailwind utilities;
it('should not warn when we detect nested css inside css @layer rules', () => {
let config = {
content: [{ raw: html`<div class="underline"></div>` }],
}
`
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(1)
expect(result.messages).toMatchObject([
{
type: 'warning',
text: [
'Nested @tailwind rules were detected, but are not supported.',
"Consider using a prefix to scope Tailwind's classes: https://tailwindcss.com/docs/configuration#prefix",
'Alternatively, use the important selector strategy: https://tailwindcss.com/docs/configuration#selector-strategy',
].join('\n'),
},
])
})
})
it('should not warn when nesting a single rule inside a media query', () => {
let config = {
content: [{ raw: html`<div class="nested"></div>` }],
}
let input = css`
@tailwind utilities;
@media (min-width: 768px) {
.nested {
let input = css`
@layer tw-base, tw-components, tw-utilities;
@layer tw-utilities {
@tailwind utilities;
}
}
`
`
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(0)
expect(result.messages).toEqual([])
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@layer tw-base, tw-components, tw-utilities;
@layer tw-utilities {
.underline {
text-decoration-line: underline;
}
}
`)
expect(result.messages).toHaveLength(0)
})
})
})
it('should only warn for the first detected nesting ', () => {
let config = {
content: [{ raw: html`<div class="nested other"></div>` }],
}
it('should warn when we detect namespaced @tailwind at rules', () => {
let config = {
content: [{ raw: html`<div class="text-center"></div>` }],
}
let input = css`
@tailwind utilities;
let input = css`
.namespace {
@tailwind utilities;
}
`
.nested {
.example {
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(1)
expect(result.messages).toMatchObject([
{
type: 'warning',
text: [
'Nested @tailwind rules were detected, but are not supported.',
"Consider using a prefix to scope Tailwind's classes: https://tailwindcss.com/docs/configuration#prefix",
'Alternatively, use the important selector strategy: https://tailwindcss.com/docs/configuration#selector-strategy',
].join('\n'),
},
])
})
})
it('should not warn when nesting a single rule inside a media query', () => {
let config = {
content: [{ raw: html`<div class="nested"></div>` }],
}
let input = css`
@tailwind utilities;
@media (min-width: 768px) {
.nested {
}
}
`
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(0)
expect(result.messages).toEqual([])
})
})
it('should only warn for the first detected nesting ', () => {
let config = {
content: [{ raw: html`<div class="nested other"></div>` }],
}
let input = css`
@tailwind utilities;
.nested {
.example {
}
.other {
}
}
.other {
.example {
}
}
}
`
.other {
.example {
}
}
`
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(1)
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(1)
})
})
})

View File

@ -1,5 +1,8 @@
import escapeClassName from '../src/util/escapeClassName'
import { crosscheck } from './util/run'
test('invalid characters are escaped', () => {
expect(escapeClassName('w:_$-1/2')).toEqual('w\\:_\\$-1\\/2')
crosscheck(() => {
test('invalid characters are escaped', () => {
expect(escapeClassName('w:_$-1/2')).toEqual('w\\:_\\$-1\\/2')
})
})

File diff suppressed because it is too large Load Diff

View File

@ -1,206 +1,208 @@
import { run, html, css } from './util/run'
import { crosscheck, run, html, css } from './util/run'
test('experimental universal selector improvements (box-shadow)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize shadow"></div>` }],
corePlugins: { preflight: false },
}
crosscheck(() => {
test('experimental universal selector improvements (box-shadow)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize shadow"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
.resize {
resize: both;
}
.resize {
resize: both;
}
.shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
})
test('experimental universal selector improvements (pseudo hover)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize hover:shadow"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.hover\:shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
.resize {
resize: both;
}
.hover\:shadow:hover {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
})
test('experimental universal selector improvements (multiple classes: group)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize group-hover:shadow"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.group-hover\:shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
.resize {
resize: both;
}
.group:hover .group-hover\:shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
})
test('experimental universal selector improvements (child selectors: divide-y)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize divide-y"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.resize {
resize: both;
}
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
`)
})
})
test('experimental universal selector improvements (hover:divide-y)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize hover:divide-y"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.resize {
resize: both;
}
.hover\:divide-y:hover > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
`)
})
})
test('experimental universal selector improvements (#app important)', () => {
let config = {
experimental: 'all',
important: '#app',
content: [{ raw: html`<div class="resize divide-y shadow"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
#app .resize {
resize: both;
}
#app .divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
#app .shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
.shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
})
test('experimental universal selector improvements (pseudo hover)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize hover:shadow"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.hover\:shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
.resize {
resize: both;
}
.hover\:shadow:hover {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
})
test('experimental universal selector improvements (multiple classes: group)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize group-hover:shadow"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.group-hover\:shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
.resize {
resize: both;
}
.group:hover .group-hover\:shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
})
test('experimental universal selector improvements (child selectors: divide-y)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize divide-y"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.resize {
resize: both;
}
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
`)
})
})
test('experimental universal selector improvements (hover:divide-y)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize hover:divide-y"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.resize {
resize: both;
}
.hover\:divide-y:hover > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
`)
})
})
test('experimental universal selector improvements (#app important)', () => {
let config = {
experimental: 'all',
important: '#app',
content: [{ raw: html`<div class="resize divide-y shadow"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
#app .resize {
resize: both;
}
#app .divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
#app .shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
})
})

View File

@ -1,30 +1,34 @@
import { run, html, css } from './util/run'
import { crosscheck, run, html, css } from './util/run'
test('PHP arrays', async () => {
let config = {
content: [
{ raw: html`<h1 class="<?php echo wrap(['class' => "max-w-[16rem]"]); ?>">Hello world</h1>` },
],
}
crosscheck(() => {
test('PHP arrays', async () => {
let config = {
content: [
{
raw: html`<h1 class="<?php echo wrap(['class' => "max-w-[16rem]"]); ?>">Hello world</h1>`,
},
],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.max-w-\[16rem\] {
max-width: 16rem;
}
`)
})
})
test('arbitrary values with quotes', async () => {
let config = { content: [{ raw: html`<div class="content-['hello]']"></div>` }] }
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.content-\[\'hello\]\'\] {
--tw-content: 'hello]';
content: var(--tw-content);
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.max-w-\[16rem\] {
max-width: 16rem;
}
`)
})
})
test('arbitrary values with quotes', async () => {
let config = { content: [{ raw: html`<div class="content-['hello]']"></div>` }] }
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.content-\[\'hello\]\'\] {
--tw-content: 'hello]';
content: var(--tw-content);
}
`)
})
})
})

View File

@ -1,81 +1,84 @@
import { crosscheck } from './util/run'
import flattenColorPalette from '../src/util/flattenColorPalette'
test('it flattens nested color objects', () => {
expect(
flattenColorPalette({
crosscheck(() => {
test('it flattens nested color objects', () => {
expect(
flattenColorPalette({
purple: 'purple',
white: {
25: 'rgba(255,255,255,.25)',
50: 'rgba(255,255,255,.5)',
75: 'rgba(255,255,255,.75)',
DEFAULT: '#fff',
},
red: {
1: 'rgb(33,0,0)',
2: 'rgb(67,0,0)',
3: 'rgb(100,0,0)',
},
green: {
1: 'rgb(0,33,0)',
2: 'rgb(0,67,0)',
3: 'rgb(0,100,0)',
},
blue: {
1: 'rgb(0,0,33)',
2: 'rgb(0,0,67)',
3: 'rgb(0,0,100)',
},
})
).toEqual({
purple: 'purple',
white: {
25: 'rgba(255,255,255,.25)',
50: 'rgba(255,255,255,.5)',
75: 'rgba(255,255,255,.75)',
DEFAULT: '#fff',
},
red: {
1: 'rgb(33,0,0)',
2: 'rgb(67,0,0)',
3: 'rgb(100,0,0)',
},
green: {
1: 'rgb(0,33,0)',
2: 'rgb(0,67,0)',
3: 'rgb(0,100,0)',
},
blue: {
1: 'rgb(0,0,33)',
2: 'rgb(0,0,67)',
3: 'rgb(0,0,100)',
},
'white-25': 'rgba(255,255,255,.25)',
'white-50': 'rgba(255,255,255,.5)',
'white-75': 'rgba(255,255,255,.75)',
white: '#fff',
'red-1': 'rgb(33,0,0)',
'red-2': 'rgb(67,0,0)',
'red-3': 'rgb(100,0,0)',
'green-1': 'rgb(0,33,0)',
'green-2': 'rgb(0,67,0)',
'green-3': 'rgb(0,100,0)',
'blue-1': 'rgb(0,0,33)',
'blue-2': 'rgb(0,0,67)',
'blue-3': 'rgb(0,0,100)',
})
).toEqual({
purple: 'purple',
'white-25': 'rgba(255,255,255,.25)',
'white-50': 'rgba(255,255,255,.5)',
'white-75': 'rgba(255,255,255,.75)',
white: '#fff',
'red-1': 'rgb(33,0,0)',
'red-2': 'rgb(67,0,0)',
'red-3': 'rgb(100,0,0)',
'green-1': 'rgb(0,33,0)',
'green-2': 'rgb(0,67,0)',
'green-3': 'rgb(0,100,0)',
'blue-1': 'rgb(0,0,33)',
'blue-2': 'rgb(0,0,67)',
'blue-3': 'rgb(0,0,100)',
})
})
test('it flattens deeply nested color objects', () => {
expect(
flattenColorPalette({
primary: 'purple',
secondary: {
DEFAULT: 'blue',
hover: 'cyan',
focus: 'red',
},
button: {
primary: {
DEFAULT: 'magenta',
hover: 'green',
focus: {
DEFAULT: 'yellow',
variant: 'orange',
test('it flattens deeply nested color objects', () => {
expect(
flattenColorPalette({
primary: 'purple',
secondary: {
DEFAULT: 'blue',
hover: 'cyan',
focus: 'red',
},
button: {
primary: {
DEFAULT: 'magenta',
hover: 'green',
focus: {
DEFAULT: 'yellow',
variant: 'orange',
},
},
},
},
})
).toEqual({
primary: 'purple',
secondary: 'blue',
'secondary-hover': 'cyan',
'secondary-focus': 'red',
'button-primary': 'magenta',
'button-primary-hover': 'green',
'button-primary-focus': 'yellow',
'button-primary-focus-variant': 'orange',
})
).toEqual({
primary: 'purple',
secondary: 'blue',
'secondary-hover': 'cyan',
'secondary-focus': 'red',
'button-primary': 'magenta',
'button-primary-hover': 'green',
'button-primary-focus': 'yellow',
'button-primary-focus-variant': 'orange',
})
test('it handles empty objects', () => {
expect(flattenColorPalette({})).toEqual({})
})
})
test('it handles empty objects', () => {
expect(flattenColorPalette({})).toEqual({})
})

View File

@ -1,356 +1,359 @@
import { finalizeSelector } from '../src/util/formatVariantSelector'
import { crosscheck } from './util/run'
it('should be possible to add a simple variant to a simple selector', () => {
let selector = '.text-center'
let candidate = 'hover:text-center'
crosscheck(() => {
it('should be possible to add a simple variant to a simple selector', () => {
let selector = '.text-center'
let candidate = 'hover:text-center'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual('.hover\\:text-center:hover')
})
expect(finalizeSelector(selector, formats, { candidate })).toEqual('.hover\\:text-center:hover')
})
it('should be possible to add a multiple simple variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'focus:hover:text-center'
it('should be possible to add a multiple simple variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'focus:hover:text-center'
let formats = [
{ format: '&:hover', isArbitraryVariant: false },
{ format: '&:focus', isArbitraryVariant: false },
]
let formats = [
{ format: '&:hover', isArbitraryVariant: false },
{ format: '&:focus', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.focus\\:hover\\:text-center:hover:focus'
)
})
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.focus\\:hover\\:text-center:hover:focus'
)
})
it('should be possible to add a simple variant to a selector containing escaped parts', () => {
let selector = '.bg-\\[rgba\\(0\\,0\\,0\\)\\]'
let candidate = 'hover:bg-[rgba(0,0,0)]'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]:hover'
)
})
it('should be possible to add a simple variant to a selector containing escaped parts (escape is slightly different)', () => {
let selector = '.bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]'
let candidate = 'hover:bg-[rgba(0,0,0)]'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]:hover'
)
})
it('should be possible to add a simple variant to a more complex selector', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'hover:space-x-4'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:space-x-4:hover > :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple simple variants to a more complex selector', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'disabled:focus:hover:space-x-4'
let formats = [
{ format: '&:hover', isArbitraryVariant: false },
{ format: '&:focus', isArbitraryVariant: false },
{ format: '&:disabled', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.disabled\\:focus\\:hover\\:space-x-4:hover:focus:disabled > :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add a single merge variant to a simple selector', () => {
let selector = '.text-center'
let candidate = 'group-hover:text-center'
let formats = [{ format: ':merge(.group):hover &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .group-hover\\:text-center'
)
})
it('should be possible to add multiple merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'group-focus:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:focus:hover .group-focus\\:group-hover\\:text-center'
)
})
it('should be possible to add a single merge variant to a more complex selector', () => {
let selector = '.space-x-4 ~ :not([hidden]) ~ :not([hidden])'
let candidate = 'group-hover:space-x-4'
let formats = [{ format: ':merge(.group):hover &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .group-hover\\:space-x-4 ~ :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple merge variants to a more complex selector', () => {
let selector = '.space-x-4 ~ :not([hidden]) ~ :not([hidden])'
let candidate = 'group-focus:group-hover:space-x-4'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:focus:hover .group-focus\\:group-hover\\:space-x-4 ~ :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple unique merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'peer-focus:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.peer):focus ~ &' },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:focus ~ .group:hover .peer-focus\\:group-hover\\:text-center'
)
})
it('should be possible to add multiple unique merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'group-hover:peer-focus:text-center'
let formats = [
{ format: ':merge(.peer):focus ~ &', isArbitraryVariant: false },
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .peer:focus ~ .group-hover\\:peer-focus\\:text-center'
)
})
it('should be possible to use multiple :merge() calls with different "arguments"', () => {
let selector = '.foo'
let candidate = 'peer-focus:group-focus:peer-hover:group-hover:foo'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.peer):hover ~ &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
{ format: ':merge(.peer):focus ~ &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:focus:hover ~ .group:focus:hover .peer-focus\\:group-focus\\:peer-hover\\:group-hover\\:foo'
)
})
it('group hover and prose headings combination', () => {
let selector = '.text-center'
let candidate = 'group-hover:prose-headings:text-center'
let formats = [
{ format: ':where(&) :is(h1, h2, h3, h4)', isArbitraryVariant: false }, // Prose Headings
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group Hover
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover :where(.group-hover\\:prose-headings\\:text-center) :is(h1, h2, h3, h4)'
)
})
it('group hover and prose headings combination flipped', () => {
let selector = '.text-center'
let candidate = 'prose-headings:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group Hover
{ format: ':where(&) :is(h1, h2, h3, h4)', isArbitraryVariant: false }, // Prose Headings
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
':where(.group:hover .prose-headings\\:group-hover\\:text-center) :is(h1, h2, h3, h4)'
)
})
it('should be possible to handle a complex utility', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'peer-disabled:peer-first-child:group-hover:group-focus:focus:hover:space-x-4'
let formats = [
{ format: '&:hover', isArbitraryVariant: false }, // Hover
{ format: '&:focus', isArbitraryVariant: false }, // Focus
{ format: ':merge(.group):focus &', isArbitraryVariant: false }, // Group focus
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group hover
{ format: ':merge(.peer):first-child ~ &', isArbitraryVariant: false }, // Peer first-child
{ format: ':merge(.peer):disabled ~ &', isArbitraryVariant: false }, // Peer disabled
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:disabled:first-child ~ .group:hover:focus .peer-disabled\\:peer-first-child\\:group-hover\\:group-focus\\:focus\\:hover\\:space-x-4:hover:focus > :not([hidden]) ~ :not([hidden])'
)
})
it('should match base utilities that are prefixed', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = 'tw-text-center'
let formats = []
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual('.tw-text-center')
})
it('should prefix classes from variants', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = 'foo:tw-text-center'
let formats = [{ format: '.foo &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual(
'.tw-foo .foo\\:tw-text-center'
)
})
it('should not prefix classes from arbitrary variants', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = '[.foo_&]:tw-text-center'
let formats = [{ format: '.foo &', isArbitraryVariant: true }]
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual(
'.foo .\\[\\.foo_\\&\\]\\:tw-text-center'
)
})
it('Merged selectors with mixed combinators uses the first one', () => {
// This isn't explicitly specced behavior but it is how it works today
let selector = '.text-center'
let candidate = 'text-center'
let formats = [
{ format: ':merge(.group):focus > &', isArbitraryVariant: true },
{ format: ':merge(.group):hover &', isArbitraryVariant: true },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover:focus > .text-center'
)
})
describe('real examples', () => {
it('example a', () => {
let selector = '.placeholder-red-500::placeholder'
let candidate = 'hover:placeholder-red-500'
it('should be possible to add a simple variant to a selector containing escaped parts', () => {
let selector = '.bg-\\[rgba\\(0\\,0\\,0\\)\\]'
let candidate = 'hover:bg-[rgba(0,0,0)]'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:placeholder-red-500:hover::placeholder'
'.hover\\:bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]:hover'
)
})
it('example b', () => {
it('should be possible to add a simple variant to a selector containing escaped parts (escape is slightly different)', () => {
let selector = '.bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]'
let candidate = 'hover:bg-[rgba(0,0,0)]'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]:hover'
)
})
it('should be possible to add a simple variant to a more complex selector', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'group-hover:hover:space-x-4'
let candidate = 'hover:space-x-4'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:space-x-4:hover > :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple simple variants to a more complex selector', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'disabled:focus:hover:space-x-4'
let formats = [
{ format: '&:hover', isArbitraryVariant: false },
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: '&:focus', isArbitraryVariant: false },
{ format: '&:disabled', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .group-hover\\:hover\\:space-x-4:hover > :not([hidden]) ~ :not([hidden])'
'.disabled\\:focus\\:hover\\:space-x-4:hover:focus:disabled > :not([hidden]) ~ :not([hidden])'
)
})
it('should work for group-hover and class dark mode combinations', () => {
it('should be possible to add a single merge variant to a simple selector', () => {
let selector = '.text-center'
let candidate = 'dark:group-hover:text-center'
let candidate = 'group-hover:text-center'
let formats = [{ format: ':merge(.group):hover &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .group-hover\\:text-center'
)
})
it('should be possible to add multiple merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'group-focus:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: '.dark &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.dark .group:hover .dark\\:group-hover\\:text-center'
'.group:focus:hover .group-focus\\:group-hover\\:text-center'
)
})
it('should work for group-hover and class dark mode combinations (reversed)', () => {
let selector = '.text-center'
let candidate = 'group-hover:dark:text-center'
it('should be possible to add a single merge variant to a more complex selector', () => {
let selector = '.space-x-4 ~ :not([hidden]) ~ :not([hidden])'
let candidate = 'group-hover:space-x-4'
let formats = [{ format: ':merge(.group):hover &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .group-hover\\:space-x-4 ~ :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple merge variants to a more complex selector', () => {
let selector = '.space-x-4 ~ :not([hidden]) ~ :not([hidden])'
let candidate = 'group-focus:group-hover:space-x-4'
let formats = [
{ format: '.dark &' },
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:focus:hover .group-focus\\:group-hover\\:space-x-4 ~ :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple unique merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'peer-focus:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.peer):focus ~ &' },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:focus ~ .group:hover .peer-focus\\:group-hover\\:text-center'
)
})
it('should be possible to add multiple unique merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'group-hover:peer-focus:text-center'
let formats = [
{ format: ':merge(.peer):focus ~ &', isArbitraryVariant: false },
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .dark .group-hover\\:dark\\:text-center'
'.group:hover .peer:focus ~ .group-hover\\:peer-focus\\:text-center'
)
})
describe('prose-headings', () => {
it('should be possible to use hover:prose-headings:text-center', () => {
let selector = '.text-center'
let candidate = 'hover:prose-headings:text-center'
it('should be possible to use multiple :merge() calls with different "arguments"', () => {
let selector = '.foo'
let candidate = 'peer-focus:group-focus:peer-hover:group-hover:foo'
let formats = [{ format: ':where(&) :is(h1, h2, h3, h4)' }, { format: '&:hover' }]
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.peer):hover ~ &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
{ format: ':merge(.peer):focus ~ &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:focus:hover ~ .group:focus:hover .peer-focus\\:group-focus\\:peer-hover\\:group-hover\\:foo'
)
})
it('group hover and prose headings combination', () => {
let selector = '.text-center'
let candidate = 'group-hover:prose-headings:text-center'
let formats = [
{ format: ':where(&) :is(h1, h2, h3, h4)', isArbitraryVariant: false }, // Prose Headings
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group Hover
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover :where(.group-hover\\:prose-headings\\:text-center) :is(h1, h2, h3, h4)'
)
})
it('group hover and prose headings combination flipped', () => {
let selector = '.text-center'
let candidate = 'prose-headings:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group Hover
{ format: ':where(&) :is(h1, h2, h3, h4)', isArbitraryVariant: false }, // Prose Headings
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
':where(.group:hover .prose-headings\\:group-hover\\:text-center) :is(h1, h2, h3, h4)'
)
})
it('should be possible to handle a complex utility', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'peer-disabled:peer-first-child:group-hover:group-focus:focus:hover:space-x-4'
let formats = [
{ format: '&:hover', isArbitraryVariant: false }, // Hover
{ format: '&:focus', isArbitraryVariant: false }, // Focus
{ format: ':merge(.group):focus &', isArbitraryVariant: false }, // Group focus
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group hover
{ format: ':merge(.peer):first-child ~ &', isArbitraryVariant: false }, // Peer first-child
{ format: ':merge(.peer):disabled ~ &', isArbitraryVariant: false }, // Peer disabled
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:disabled:first-child ~ .group:hover:focus .peer-disabled\\:peer-first-child\\:group-hover\\:group-focus\\:focus\\:hover\\:space-x-4:hover:focus > :not([hidden]) ~ :not([hidden])'
)
})
it('should match base utilities that are prefixed', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = 'tw-text-center'
let formats = []
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual('.tw-text-center')
})
it('should prefix classes from variants', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = 'foo:tw-text-center'
let formats = [{ format: '.foo &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual(
'.tw-foo .foo\\:tw-text-center'
)
})
it('should not prefix classes from arbitrary variants', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = '[.foo_&]:tw-text-center'
let formats = [{ format: '.foo &', isArbitraryVariant: true }]
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual(
'.foo .\\[\\.foo_\\&\\]\\:tw-text-center'
)
})
it('Merged selectors with mixed combinators uses the first one', () => {
// This isn't explicitly specced behavior but it is how it works today
let selector = '.text-center'
let candidate = 'text-center'
let formats = [
{ format: ':merge(.group):focus > &', isArbitraryVariant: true },
{ format: ':merge(.group):hover &', isArbitraryVariant: true },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover:focus > .text-center'
)
})
describe('real examples', () => {
it('example a', () => {
let selector = '.placeholder-red-500::placeholder'
let candidate = 'hover:placeholder-red-500'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
':where(.hover\\:prose-headings\\:text-center) :is(h1, h2, h3, h4):hover'
'.hover\\:placeholder-red-500:hover::placeholder'
)
})
it('should be possible to use prose-headings:hover:text-center', () => {
let selector = '.text-center'
let candidate = 'prose-headings:hover:text-center'
it('example b', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'group-hover:hover:space-x-4'
let formats = [{ format: '&:hover' }, { format: ':where(&) :is(h1, h2, h3, h4)' }]
let formats = [
{ format: '&:hover', isArbitraryVariant: false },
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
':where(.prose-headings\\:hover\\:text-center:hover) :is(h1, h2, h3, h4)'
'.group:hover .group-hover\\:hover\\:space-x-4:hover > :not([hidden]) ~ :not([hidden])'
)
})
})
})
describe('pseudo elements', () => {
it.each`
before | after
${'&::before'} | ${'&::before'}
${'&::before:hover'} | ${'&:hover::before'}
${'&:before:hover'} | ${'&:hover:before'}
${'&::file-selector-button:hover'} | ${'&::file-selector-button:hover'}
${'&:hover::file-selector-button'} | ${'&:hover::file-selector-button'}
${'.parent:hover &'} | ${'.parent:hover &'}
${'.parent::before &'} | ${'.parent &::before'}
${'.parent::before &:hover'} | ${'.parent &:hover::before'}
${':where(&::before) :is(h1, h2, h3, h4)'} | ${':where(&) :is(h1, h2, h3, h4)::before'}
${':where(&::file-selector-button) :is(h1, h2, h3, h4)'} | ${':where(&::file-selector-button) :is(h1, h2, h3, h4)'}
`('should translate "$before" into "$after"', ({ before, after }) => {
let result = finalizeSelector('.a', [{ format: before, isArbitraryVariant: false }], {
candidate: 'a',
it('should work for group-hover and class dark mode combinations', () => {
let selector = '.text-center'
let candidate = 'dark:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: '.dark &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.dark .group:hover .dark\\:group-hover\\:text-center'
)
})
expect(result).toEqual(after.replace('&', '.a'))
it('should work for group-hover and class dark mode combinations (reversed)', () => {
let selector = '.text-center'
let candidate = 'group-hover:dark:text-center'
let formats = [
{ format: '.dark &' },
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .dark .group-hover\\:dark\\:text-center'
)
})
describe('prose-headings', () => {
it('should be possible to use hover:prose-headings:text-center', () => {
let selector = '.text-center'
let candidate = 'hover:prose-headings:text-center'
let formats = [{ format: ':where(&) :is(h1, h2, h3, h4)' }, { format: '&:hover' }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
':where(.hover\\:prose-headings\\:text-center) :is(h1, h2, h3, h4):hover'
)
})
it('should be possible to use prose-headings:hover:text-center', () => {
let selector = '.text-center'
let candidate = 'prose-headings:hover:text-center'
let formats = [{ format: '&:hover' }, { format: ':where(&) :is(h1, h2, h3, h4)' }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
':where(.prose-headings\\:hover\\:text-center:hover) :is(h1, h2, h3, h4)'
)
})
})
})
describe('pseudo elements', () => {
it.each`
before | after
${'&::before'} | ${'&::before'}
${'&::before:hover'} | ${'&:hover::before'}
${'&:before:hover'} | ${'&:hover:before'}
${'&::file-selector-button:hover'} | ${'&::file-selector-button:hover'}
${'&:hover::file-selector-button'} | ${'&:hover::file-selector-button'}
${'.parent:hover &'} | ${'.parent:hover &'}
${'.parent::before &'} | ${'.parent &::before'}
${'.parent::before &:hover'} | ${'.parent &:hover::before'}
${':where(&::before) :is(h1, h2, h3, h4)'} | ${':where(&) :is(h1, h2, h3, h4)::before'}
${':where(&::file-selector-button) :is(h1, h2, h3, h4)'} | ${':where(&::file-selector-button) :is(h1, h2, h3, h4)'}
`('should translate "$before" into "$after"', ({ before, after }) => {
let result = finalizeSelector('.a', [{ format: before, isArbitraryVariant: false }], {
candidate: 'a',
})
expect(result).toEqual(after.replace('&', '.a'))
})
})
})

View File

@ -1,47 +1,48 @@
import { css } from './util/run'
import { generateRules } from '../src/lib/generateRules'
import resolveConfig from '../src/public/resolve-config'
import { createContext } from '../src/lib/setupContextUtils'
import { crosscheck, css } from './util/run'
it('should not generate rules that are incorrect', () => {
let config = {
plugins: [
({ matchVariant }) => {
matchVariant('@', (value) => `@container (min-width: ${value})`)
},
],
}
let context = createContext(resolveConfig(config))
let rules = generateRules(
new Set([
// Invalid, missing `-`
'group[:hover]:underline',
// Invalid, `-` should not be there
'@-[200px]:underline',
// Valid
'group-[:hover]:underline',
'@[200px]:underline',
]),
context
)
// Ensure we only have 2 valid rules
expect(rules).toHaveLength(2)
// Ensure we have the correct values
expect(rules[0][1].toString()).toMatchFormattedCss(css`
.group:hover .group-\[\:hover\]\:underline {
text-decoration-line: underline;
crosscheck(() => {
it('should not generate rules that are incorrect', () => {
let config = {
plugins: [
({ matchVariant }) => {
matchVariant('@', (value) => `@container (min-width: ${value})`)
},
],
}
`)
expect(rules[1][1].toString()).toMatchFormattedCss(css`
@container (min-width: 200px) {
.\@\[200px\]\:underline {
let context = createContext(resolveConfig(config))
let rules = generateRules(
new Set([
// Invalid, missing `-`
'group[:hover]:underline',
// Invalid, `-` should not be there
'@-[200px]:underline',
// Valid
'group-[:hover]:underline',
'@[200px]:underline',
]),
context
)
// Ensure we only have 2 valid rules
expect(rules).toHaveLength(2)
// Ensure we have the correct values
expect(rules[0][1].toString()).toMatchFormattedCss(css`
.group:hover .group-\[\:hover\]\:underline {
text-decoration-line: underline;
}
}
`)
`)
expect(rules[1][1].toString()).toMatchFormattedCss(css`
@container (min-width: 200px) {
.\@\[200px\]\:underline {
text-decoration-line: underline;
}
}
`)
})
})

View File

@ -1,119 +1,122 @@
import resolveConfig from '../src/public/resolve-config'
import { createContext } from '../src/lib/setupContextUtils'
import { crosscheck } from './util/run'
it('should generate every possible class, without variants', () => {
let config = {}
crosscheck(() => {
it('should generate every possible class, without variants', () => {
let config = {}
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).toBeInstanceOf(Array)
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).toBeInstanceOf(Array)
// Verify we have a `container` for the 'components' section.
expect(classes).toContain('container')
// Verify we have a `container` for the 'components' section.
expect(classes).toContain('container')
// Verify we handle the DEFAULT case correctly
expect(classes).toContain('border')
// Verify we handle the DEFAULT case correctly
expect(classes).toContain('border')
// Verify we handle negative values correctly
expect(classes).toContain('-inset-1/4')
expect(classes).toContain('-m-0')
expect(classes).not.toContain('-uppercase')
expect(classes).not.toContain('-opacity-50')
// Verify we handle negative values correctly
expect(classes).toContain('-inset-1/4')
expect(classes).toContain('-m-0')
expect(classes).not.toContain('-uppercase')
expect(classes).not.toContain('-opacity-50')
config = { theme: { extend: { margin: { DEFAULT: '5px' } } } }
context = createContext(resolveConfig(config))
classes = context.getClassList()
config = { theme: { extend: { margin: { DEFAULT: '5px' } } } }
context = createContext(resolveConfig(config))
classes = context.getClassList()
expect(classes).not.toContain('-m-DEFAULT')
})
expect(classes).not.toContain('-m-DEFAULT')
})
it('should generate every possible class while handling negatives and prefixes', () => {
let config = { prefix: 'tw-' }
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).toBeInstanceOf(Array)
it('should generate every possible class while handling negatives and prefixes', () => {
let config = { prefix: 'tw-' }
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).toBeInstanceOf(Array)
// Verify we have a `container` for the 'components' section.
expect(classes).toContain('tw-container')
// Verify we have a `container` for the 'components' section.
expect(classes).toContain('tw-container')
// Verify we handle the DEFAULT case correctly
expect(classes).toContain('tw-border')
// Verify we handle the DEFAULT case correctly
expect(classes).toContain('tw-border')
// Verify we handle negative values correctly
expect(classes).toContain('-tw-inset-1/4')
expect(classes).toContain('-tw-m-0')
expect(classes).not.toContain('-tw-uppercase')
expect(classes).not.toContain('-tw-opacity-50')
// Verify we handle negative values correctly
expect(classes).toContain('-tw-inset-1/4')
expect(classes).toContain('-tw-m-0')
expect(classes).not.toContain('-tw-uppercase')
expect(classes).not.toContain('-tw-opacity-50')
// These utilities do work but there's no reason to generate
// them alongside the `-{prefix}-{utility}` versions
expect(classes).not.toContain('tw--inset-1/4')
expect(classes).not.toContain('tw--m-0')
// These utilities do work but there's no reason to generate
// them alongside the `-{prefix}-{utility}` versions
expect(classes).not.toContain('tw--inset-1/4')
expect(classes).not.toContain('tw--m-0')
config = {
prefix: 'tw-',
theme: { extend: { margin: { DEFAULT: '5px' } } },
}
context = createContext(resolveConfig(config))
classes = context.getClassList()
config = {
prefix: 'tw-',
theme: { extend: { margin: { DEFAULT: '5px' } } },
}
context = createContext(resolveConfig(config))
classes = context.getClassList()
expect(classes).not.toContain('-tw-m-DEFAULT')
})
expect(classes).not.toContain('-tw-m-DEFAULT')
})
it('should not generate utilities with opacity by default', () => {
let config = {}
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
it('should not generate utilities with opacity by default', () => {
let config = {}
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).not.toContain('bg-red-500/50')
})
expect(classes).not.toContain('bg-red-500/50')
})
it('should not generate utilities with opacity even if safe-listed', () => {
let config = {
safelist: [
{
pattern: /^bg-red-(400|500)(\/(40|50))?$/,
},
],
}
it('should not generate utilities with opacity even if safe-listed', () => {
let config = {
safelist: [
{
pattern: /^bg-red-(400|500)(\/(40|50))?$/,
},
],
}
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).not.toContain('bg-red-500/50')
})
expect(classes).not.toContain('bg-red-500/50')
})
it('should not generate utilities that are set to undefined or null to so that they are removed', () => {
let config = {
theme: {
extend: {
colors: {
red: null,
green: undefined,
blue: {
100: null,
200: undefined,
it('should not generate utilities that are set to undefined or null to so that they are removed', () => {
let config = {
theme: {
extend: {
colors: {
red: null,
green: undefined,
blue: {
100: null,
200: undefined,
},
},
},
},
},
safelist: [
{
pattern: /^bg-(red|green|blue)-.*$/,
},
],
}
safelist: [
{
pattern: /^bg-(red|green|blue)-.*$/,
},
],
}
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).not.toContain('bg-red-100') // Red is `null`
expect(classes).not.toContain('bg-red-100') // Red is `null`
expect(classes).not.toContain('bg-green-100') // Green is `undefined`
expect(classes).not.toContain('bg-green-100') // Green is `undefined`
expect(classes).not.toContain('bg-blue-100') // Blue.100 is `null`
expect(classes).not.toContain('bg-blue-200') // Blue.200 is `undefined`
expect(classes).not.toContain('bg-blue-100') // Blue.100 is `null`
expect(classes).not.toContain('bg-blue-200') // Blue.200 is `undefined`
expect(classes).toContain('bg-blue-50')
expect(classes).toContain('bg-blue-300')
expect(classes).toContain('bg-blue-50')
expect(classes).toContain('bg-blue-300')
})
})

View File

@ -1,6 +1,7 @@
import resolveConfig from '../src/public/resolve-config'
import { createContext } from '../src/lib/setupContextUtils'
import bigSign from '../src/util/bigSign'
import { crosscheck } from './util/run'
/**
* This is a function that the prettier-plugin-tailwindcss would use. It would
@ -24,92 +25,94 @@ function defaultSort(arrayOfTuples) {
.join(' ')
}
it('should return a list of tuples with the sort order', () => {
let input = 'font-bold underline hover:font-medium unknown'
let config = {}
let context = createContext(resolveConfig(config))
expect(context.getClassOrder(input.split(' '))).toEqual([
['font-bold', expect.any(BigInt)],
['underline', expect.any(BigInt)],
['hover:font-medium', expect.any(BigInt)],
crosscheck(() => {
it('should return a list of tuples with the sort order', () => {
let input = 'font-bold underline hover:font-medium unknown'
let config = {}
let context = createContext(resolveConfig(config))
expect(context.getClassOrder(input.split(' '))).toEqual([
['font-bold', expect.any(BigInt)],
['underline', expect.any(BigInt)],
['hover:font-medium', expect.any(BigInt)],
// Unknown values receive `null`
['unknown', null],
])
})
// Unknown values receive `null`
['unknown', null],
])
})
it.each([
// Utitlies
['px-3 p-1 py-3', 'p-1 px-3 py-3'],
it.each([
// Utitlies
['px-3 p-1 py-3', 'p-1 px-3 py-3'],
// Utitlies and components
['px-4 container', 'container px-4'],
// Utitlies and components
['px-4 container', 'container px-4'],
// Utilities with variants
['px-3 focus:hover:p-3 hover:p-1 py-3', 'px-3 py-3 hover:p-1 focus:hover:p-3'],
// Utilities with variants
['px-3 focus:hover:p-3 hover:p-1 py-3', 'px-3 py-3 hover:p-1 focus:hover:p-3'],
// Utitlies with important
['px-3 !py-4', 'px-3 !py-4'],
['!py-4 px-3', '!py-4 px-3'],
// Utitlies with important
['px-3 !py-4', 'px-3 !py-4'],
['!py-4 px-3', '!py-4 px-3'],
// Components with variants
['hover:container container', 'container hover:container'],
// Components with variants
['hover:container container', 'container hover:container'],
// Components and utilities with variants
[
'focus:hover:container hover:underline hover:container p-1',
'p-1 hover:container hover:underline focus:hover:container',
],
// Components and utilities with variants
[
'focus:hover:container hover:underline hover:container p-1',
'p-1 hover:container hover:underline focus:hover:container',
],
// Leave user css order alone, and move to the front
['b p-1 a', 'b a p-1'],
['hover:b focus:p-1 a', 'hover:b a focus:p-1'],
// Leave user css order alone, and move to the front
['b p-1 a', 'b a p-1'],
['hover:b focus:p-1 a', 'hover:b a focus:p-1'],
// Add special treatment for `group` and `peer`
['a peer container underline', 'a peer container underline'],
])('should sort "%s" based on the order we generate them in to "%s"', (input, output) => {
let config = {}
let context = createContext(resolveConfig(config))
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
})
it.each([
// Utitlies
['tw-px-3 tw-p-1 tw-py-3', 'tw-p-1 tw-px-3 tw-py-3'],
// Utitlies and components
['tw-px-4 tw-container', 'tw-container tw-px-4'],
// Utilities with variants
[
'tw-px-3 focus:hover:tw-p-3 hover:tw-p-1 tw-py-3',
'tw-px-3 tw-py-3 hover:tw-p-1 focus:hover:tw-p-3',
],
// Utitlies with important
['tw-px-3 !tw-py-4', 'tw-px-3 !tw-py-4'],
['!tw-py-4 tw-px-3', '!tw-py-4 tw-px-3'],
// Components with variants
['hover:tw-container tw-container', 'tw-container hover:tw-container'],
// Components and utilities with variants
[
'focus:hover:tw-container hover:tw-underline hover:tw-container tw-p-1',
'tw-p-1 hover:tw-container hover:tw-underline focus:hover:tw-container',
],
// Leave user css order alone, and move to the front
['b tw-p-1 a', 'b a tw-p-1'],
['hover:b focus:tw-p-1 a', 'hover:b a focus:tw-p-1'],
// Add special treatment for `group` and `peer`
['a tw-peer tw-container tw-underline', 'a tw-peer tw-container tw-underline'],
])(
'should sort "%s" with prefixex based on the order we generate them in to "%s"',
(input, output) => {
let config = { prefix: 'tw-' }
// Add special treatment for `group` and `peer`
['a peer container underline', 'a peer container underline'],
])('should sort "%s" based on the order we generate them in to "%s"', (input, output) => {
let config = {}
let context = createContext(resolveConfig(config))
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
}
)
})
it.each([
// Utitlies
['tw-px-3 tw-p-1 tw-py-3', 'tw-p-1 tw-px-3 tw-py-3'],
// Utitlies and components
['tw-px-4 tw-container', 'tw-container tw-px-4'],
// Utilities with variants
[
'tw-px-3 focus:hover:tw-p-3 hover:tw-p-1 tw-py-3',
'tw-px-3 tw-py-3 hover:tw-p-1 focus:hover:tw-p-3',
],
// Utitlies with important
['tw-px-3 !tw-py-4', 'tw-px-3 !tw-py-4'],
['!tw-py-4 tw-px-3', '!tw-py-4 tw-px-3'],
// Components with variants
['hover:tw-container tw-container', 'tw-container hover:tw-container'],
// Components and utilities with variants
[
'focus:hover:tw-container hover:tw-underline hover:tw-container tw-p-1',
'tw-p-1 hover:tw-container hover:tw-underline focus:hover:tw-container',
],
// Leave user css order alone, and move to the front
['b tw-p-1 a', 'b a tw-p-1'],
['hover:b focus:tw-p-1 a', 'hover:b a focus:tw-p-1'],
// Add special treatment for `group` and `peer`
['a tw-peer tw-container tw-underline', 'a tw-peer tw-container tw-underline'],
])(
'should sort "%s" with prefixex based on the order we generate them in to "%s"',
(input, output) => {
let config = { prefix: 'tw-' }
let context = createContext(resolveConfig(config))
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
}
)
})

View File

@ -2,185 +2,188 @@ import postcss from 'postcss'
import selectorParser from 'postcss-selector-parser'
import resolveConfig from '../src/public/resolve-config'
import { createContext } from '../src/lib/setupContextUtils'
import { crosscheck } from './util/run'
it('should return a list of variants with meta information about the variant', () => {
let config = {}
let context = createContext(resolveConfig(config))
crosscheck(() => {
it('should return a list of variants with meta information about the variant', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variants = context.getVariants()
expect(variants).toContainEqual({
name: 'hover',
isArbitrary: false,
hasDash: true,
values: [],
selectors: expect.any(Function),
expect(variants).toContainEqual({
name: 'hover',
isArbitrary: false,
hasDash: true,
values: [],
selectors: expect.any(Function),
})
expect(variants).toContainEqual({
name: 'group',
isArbitrary: true,
hasDash: true,
values: expect.any(Array),
selectors: expect.any(Function),
})
// `group-hover` now belongs to the `group` variant. The information exposed for the `group`
// variant is all you need.
expect(variants.find((v) => v.name === 'group-hover')).toBeUndefined()
})
expect(variants).toContainEqual({
name: 'group',
isArbitrary: true,
hasDash: true,
values: expect.any(Array),
selectors: expect.any(Function),
it('should provide selectors for simple variants', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'hover')
expect(variant.selectors()).toEqual(['&:hover'])
})
// `group-hover` now belongs to the `group` variant. The information exposed for the `group`
// variant is all you need.
expect(variants.find((v) => v.name === 'group-hover')).toBeUndefined()
})
it('should provide selectors for parallel variants', () => {
let config = {}
let context = createContext(resolveConfig(config))
it('should provide selectors for simple variants', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'marker')
expect(variant.selectors()).toEqual(['& *::marker', '&::marker'])
})
let variant = variants.find((v) => v.name === 'hover')
expect(variant.selectors()).toEqual(['&:hover'])
})
it('should provide selectors for complex matchVariant variants like `group`', () => {
let config = {}
let context = createContext(resolveConfig(config))
it('should provide selectors for parallel variants', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'group')
expect(variant.selectors()).toEqual(['.group &'])
expect(variant.selectors({})).toEqual(['.group &'])
expect(variant.selectors({ value: 'hover' })).toEqual(['.group:hover &'])
expect(variant.selectors({ value: '.foo_&' })).toEqual(['.foo .group &'])
expect(variant.selectors({ modifier: 'foo', value: 'hover' })).toEqual(['.group\\/foo:hover &'])
expect(variant.selectors({ modifier: 'foo', value: '.foo_&' })).toEqual(['.foo .group\\/foo &'])
})
let variant = variants.find((v) => v.name === 'marker')
expect(variant.selectors()).toEqual(['& *::marker', '&::marker'])
})
it('should provide selectors for variants with atrules', () => {
let config = {}
let context = createContext(resolveConfig(config))
it('should provide selectors for complex matchVariant variants like `group`', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'supports')
expect(variant.selectors({ value: 'display:grid' })).toEqual(['@supports (display:grid)'])
expect(variant.selectors({ value: 'aspect-ratio' })).toEqual([
'@supports (aspect-ratio: var(--tw))',
])
})
let variant = variants.find((v) => v.name === 'group')
expect(variant.selectors()).toEqual(['.group &'])
expect(variant.selectors({})).toEqual(['.group &'])
expect(variant.selectors({ value: 'hover' })).toEqual(['.group:hover &'])
expect(variant.selectors({ value: '.foo_&' })).toEqual(['.foo .group &'])
expect(variant.selectors({ modifier: 'foo', value: 'hover' })).toEqual(['.group\\/foo:hover &'])
expect(variant.selectors({ modifier: 'foo', value: '.foo_&' })).toEqual(['.foo .group\\/foo &'])
})
it('should provide selectors for variants with atrules', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'supports')
expect(variant.selectors({ value: 'display:grid' })).toEqual(['@supports (display:grid)'])
expect(variant.selectors({ value: 'aspect-ratio' })).toEqual([
'@supports (aspect-ratio: var(--tw))',
])
})
it('should provide selectors for custom plugins that do a combination of parallel variants with modifiers with arbitrary values and with atrules', () => {
let config = {
plugins: [
function ({ matchVariant }) {
matchVariant('foo', (value, { modifier }) => {
return [
`
it('should provide selectors for custom plugins that do a combination of parallel variants with modifiers with arbitrary values and with atrules', () => {
let config = {
plugins: [
function ({ matchVariant }) {
matchVariant('foo', (value, { modifier }) => {
return [
`
@supports (foo: ${modifier}) {
@media (width <= 400px) {
&:hover
}
}
`,
`.${modifier}\\/${value} &:focus`,
]
})
},
],
}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'foo')
expect(variant.selectors({ modifier: 'bar', value: 'baz' })).toEqual([
'@supports (foo: bar) { @media (width <= 400px) { &:hover } }',
'.bar\\/baz &:focus',
])
})
it('should work for plugins that still use the modifySelectors API', () => {
let config = {
plugins: [
function ({ addVariant }) {
addVariant('foo', ({ modifySelectors, container }) => {
// Manually mutating the selector
modifySelectors(({ selector }) => {
return selectorParser((selectors) => {
selectors.walkClasses((classNode) => {
classNode.value = `foo:${classNode.value}`
classNode.parent.insertBefore(classNode, selectorParser().astSync(`.foo `))
})
}).processSync(selector)
`.${modifier}\\/${value} &:focus`,
]
})
},
],
}
let context = createContext(resolveConfig(config))
// Manually wrap in supports query
let wrapper = postcss.atRule({ name: 'supports', params: 'display: grid' })
let nodes = container.nodes
container.removeAll()
wrapper.append(nodes)
container.append(wrapper)
})
},
],
}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'foo')
expect(variant.selectors({})).toEqual(['@supports (display: grid) { .foo .foo\\:& }'])
})
it('should special case the `@`', () => {
let config = {
plugins: [
({ matchVariant }) => {
matchVariant(
'@',
(value, { modifier }) => `@container ${modifier ?? ''} (min-width: ${value})`,
{
modifiers: 'any',
values: {
xs: '20rem',
sm: '24rem',
md: '28rem',
lg: '32rem',
xl: '36rem',
'2xl': '42rem',
'3xl': '48rem',
'4xl': '56rem',
'5xl': '64rem',
'6xl': '72rem',
'7xl': '80rem',
},
}
)
},
],
}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === '@')
expect(variant).toEqual({
name: '@',
isArbitrary: true,
hasDash: false,
values: expect.any(Array),
selectors: expect.any(Function),
let variant = variants.find((v) => v.name === 'foo')
expect(variant.selectors({ modifier: 'bar', value: 'baz' })).toEqual([
'@supports (foo: bar) { @media (width <= 400px) { &:hover } }',
'.bar\\/baz &:focus',
])
})
it('should work for plugins that still use the modifySelectors API', () => {
let config = {
plugins: [
function ({ addVariant }) {
addVariant('foo', ({ modifySelectors, container }) => {
// Manually mutating the selector
modifySelectors(({ selector }) => {
return selectorParser((selectors) => {
selectors.walkClasses((classNode) => {
classNode.value = `foo:${classNode.value}`
classNode.parent.insertBefore(classNode, selectorParser().astSync(`.foo `))
})
}).processSync(selector)
})
// Manually wrap in supports query
let wrapper = postcss.atRule({ name: 'supports', params: 'display: grid' })
let nodes = container.nodes
container.removeAll()
wrapper.append(nodes)
container.append(wrapper)
})
},
],
}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'foo')
expect(variant.selectors({})).toEqual(['@supports (display: grid) { .foo .foo\\:& }'])
})
it('should special case the `@`', () => {
let config = {
plugins: [
({ matchVariant }) => {
matchVariant(
'@',
(value, { modifier }) => `@container ${modifier ?? ''} (min-width: ${value})`,
{
modifiers: 'any',
values: {
xs: '20rem',
sm: '24rem',
md: '28rem',
lg: '32rem',
xl: '36rem',
'2xl': '42rem',
'3xl': '48rem',
'4xl': '56rem',
'5xl': '64rem',
'6xl': '72rem',
'7xl': '80rem',
},
}
)
},
],
}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === '@')
expect(variant).toEqual({
name: '@',
isArbitrary: true,
hasDash: false,
values: expect.any(Array),
selectors: expect.any(Function),
})
expect(variant.selectors({ value: 'xs', modifier: 'foo' })).toEqual([
'@container foo (min-width: 20rem)',
])
})
expect(variant.selectors({ value: 'xs', modifier: 'foo' })).toEqual([
'@container foo (min-width: 20rem)',
])
})

View File

@ -1,82 +1,84 @@
import { run, html, css, defaults } from './util/run'
import { crosscheck, run, html, css, defaults } from './util/run'
test('using @import instead of @tailwind', () => {
let config = {
content: [
{
raw: html`
<h1>Hello world!</h1>
<div class="container"></div>
<div class="mt-6"></div>
<div class="bg-black"></div>
<div class="md:hover:text-center"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addBase }) {
addBase({
h1: {
fontSize: '32px',
},
})
},
],
}
crosscheck(() => {
test('using @import instead of @tailwind', () => {
let config = {
content: [
{
raw: html`
<h1>Hello world!</h1>
<div class="container"></div>
<div class="mt-6"></div>
<div class="bg-black"></div>
<div class="md:hover:text-center"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addBase }) {
addBase({
h1: {
fontSize: '32px',
},
})
},
],
}
let input = css`
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
`
let input = css`
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
h1 {
font-size: 32px;
}
${defaults}
.container {
width: 100%;
}
@media (min-width: 640px) {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
h1 {
font-size: 32px;
}
${defaults}
.container {
max-width: 640px;
width: 100%;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
}
.mt-6 {
margin-top: 1.5rem;
}
.bg-black {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
@media (min-width: 768px) {
.md\:hover\:text-center:hover {
text-align: center;
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
}
`)
.mt-6 {
margin-top: 1.5rem;
}
.bg-black {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
@media (min-width: 768px) {
.md\:hover\:text-center:hover {
text-align: center;
}
}
`)
})
})
})

View File

@ -2,228 +2,230 @@ import fs from 'fs'
import path from 'path'
import * as sharedState from '../src/lib/sharedState'
import { run, css, html, defaults } from './util/run'
import { crosscheck, run, html, css, defaults } from './util/run'
test('important boolean', () => {
let config = {
important: true,
darkMode: 'class',
content: [
{
raw: html`
<div class="container"></div>
<div class="btn"></div>
<div class="animate-spin"></div>
<div class="custom-util"></div>
<div class="custom-component"></div>
<div class="custom-important-component"></div>
<div class="font-bold"></div>
<div class="md:hover:text-right"></div>
<div class="motion-safe:hover:text-center"></div>
<div class="dark:focus:text-left"></div>
<div class="group-hover:focus-within:text-left"></div>
<div class="rtl:active:text-center"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addComponents, addUtilities }) {
addComponents(
{
'.btn': {
button: 'yes',
},
},
{ respectImportant: true }
)
addComponents(
{
'@font-face': {
'font-family': 'Inter',
},
'@page': {
margin: '1cm',
},
},
{ respectImportant: true }
)
addUtilities(
{
'.custom-util': {
button: 'no',
},
},
{ respectImportant: false }
)
},
],
}
let input = css`
@tailwind base;
@tailwind components;
@layer components {
.custom-component {
@apply font-bold;
}
.custom-important-component {
@apply text-center !important;
}
}
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.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;
}
}
.btn {
button: yes !important;
}
@font-face {
font-family: Inter;
}
@page {
margin: 1cm;
}
.custom-component {
font-weight: 700;
}
.custom-important-component {
text-align: center !important;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite !important;
}
.font-bold {
font-weight: 700 !important;
}
.custom-util {
button: no;
}
.group:hover .group-hover\:focus-within\:text-left:focus-within {
text-align: left !important;
}
[dir='rtl'] .rtl\:active\:text-center:active {
text-align: center !important;
}
@media (prefers-reduced-motion: no-preference) {
.motion-safe\:hover\:text-center:hover {
text-align: center !important;
}
}
.dark .dark\:focus\:text-left:focus {
text-align: left !important;
}
@media (min-width: 768px) {
.md\:hover\:text-right:hover {
text-align: right !important;
}
}
`)
})
})
// This is in a describe block so we can use `afterEach` :)
describe('duplicate elision', () => {
let filePath = path.resolve(__dirname, './important-boolean-duplicates.test.html')
afterEach(async () => await fs.promises.unlink(filePath))
test('important rules are not duplicated when rebuilding', async () => {
crosscheck(() => {
test('important boolean', () => {
let config = {
important: true,
content: [filePath],
darkMode: 'class',
content: [
{
raw: html`
<div class="container"></div>
<div class="btn"></div>
<div class="animate-spin"></div>
<div class="custom-util"></div>
<div class="custom-component"></div>
<div class="custom-important-component"></div>
<div class="font-bold"></div>
<div class="md:hover:text-right"></div>
<div class="motion-safe:hover:text-center"></div>
<div class="dark:focus:text-left"></div>
<div class="group-hover:focus-within:text-left"></div>
<div class="rtl:active:text-center"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addComponents, addUtilities }) {
addComponents(
{
'.btn': {
button: 'yes',
},
},
{ respectImportant: true }
)
addComponents(
{
'@font-face': {
'font-family': 'Inter',
},
'@page': {
margin: '1cm',
},
},
{ respectImportant: true }
)
addUtilities(
{
'.custom-util': {
button: 'no',
},
},
{ respectImportant: false }
)
},
],
}
await fs.promises.writeFile(
config.content[0],
html`
<div class="ml-2"></div>
<div class="ml-4"></div>
`
)
let input = css`
@tailwind base;
@tailwind components;
@layer components {
.custom-component {
@apply font-bold;
}
.custom-important-component {
@apply text-center !important;
}
}
@tailwind utilities;
`
let result = await run(input, config)
let allContexts = Array.from(sharedState.contextMap.values())
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.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;
}
}
.btn {
button: yes !important;
}
@font-face {
font-family: Inter;
}
@page {
margin: 1cm;
}
.custom-component {
font-weight: 700;
}
.custom-important-component {
text-align: center !important;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite !important;
}
.font-bold {
font-weight: 700 !important;
}
.custom-util {
button: no;
}
.group:hover .group-hover\:focus-within\:text-left:focus-within {
text-align: left !important;
}
[dir='rtl'] .rtl\:active\:text-center:active {
text-align: center !important;
}
@media (prefers-reduced-motion: no-preference) {
.motion-safe\:hover\:text-center:hover {
text-align: center !important;
}
}
.dark .dark\:focus\:text-left:focus {
text-align: left !important;
}
@media (min-width: 768px) {
.md\:hover\:text-right:hover {
text-align: right !important;
}
}
`)
})
})
let context = allContexts[allContexts.length - 1]
// This is in a describe block so we can use `afterEach` :)
describe('duplicate elision', () => {
let filePath = path.resolve(__dirname, './important-boolean-duplicates.test.html')
let ruleCacheSize1 = context.ruleCache.size
afterEach(async () => await fs.promises.unlink(filePath))
expect(result.css).toMatchFormattedCss(css`
.ml-2 {
margin-left: 0.5rem !important;
test('important rules are not duplicated when rebuilding', async () => {
let config = {
important: true,
content: [filePath],
}
.ml-4 {
margin-left: 1rem !important;
}
`)
await fs.promises.writeFile(
config.content[0],
html`
<div class="ml-2"></div>
<div class="ml-6"></div>
await fs.promises.writeFile(
config.content[0],
html`
<div class="ml-2"></div>
<div class="ml-4"></div>
`
)
let input = css`
@tailwind utilities;
`
)
result = await run(input, config)
let result = await run(input, config)
let allContexts = Array.from(sharedState.contextMap.values())
let ruleCacheSize2 = context.ruleCache.size
let context = allContexts[allContexts.length - 1]
expect(result.css).toMatchFormattedCss(css`
.ml-2 {
margin-left: 0.5rem !important;
}
.ml-4 {
margin-left: 1rem !important;
}
.ml-6 {
margin-left: 1.5rem !important;
}
`)
let ruleCacheSize1 = context.ruleCache.size
// The rule cache was effectively doubling in size previously
// because the rule cache was never de-duped
// This ensures this behavior doesn't return
expect(ruleCacheSize2 - ruleCacheSize1).toBeLessThan(10)
expect(result.css).toMatchFormattedCss(css`
.ml-2 {
margin-left: 0.5rem !important;
}
.ml-4 {
margin-left: 1rem !important;
}
`)
await fs.promises.writeFile(
config.content[0],
html`
<div class="ml-2"></div>
<div class="ml-6"></div>
`
)
result = await run(input, config)
let ruleCacheSize2 = context.ruleCache.size
expect(result.css).toMatchFormattedCss(css`
.ml-2 {
margin-left: 0.5rem !important;
}
.ml-4 {
margin-left: 1rem !important;
}
.ml-6 {
margin-left: 1.5rem !important;
}
`)
// The rule cache was effectively doubling in size previously
// because the rule cache was never de-duped
// This ensures this behavior doesn't return
expect(ruleCacheSize2 - ruleCacheSize1).toBeLessThan(10)
})
})
})

View File

@ -1,77 +1,79 @@
import { run, html, css, defaults } from './util/run'
import { crosscheck, run, html, css, defaults } from './util/run'
test('important modifier with prefix', () => {
let config = {
important: false,
prefix: 'tw-',
darkMode: 'class',
content: [
{
raw: html`<!-- The string "!*" can cause problems if we don't handle it, let's include it -->
<div class="!*"></div>
<div class="!tw-container"></div>
<div class="!tw-font-bold"></div>
<div class="hover:!tw-text-center"></div>
<div class="lg:!tw-opacity-50"></div>
<div class="xl:focus:disabled:!tw-float-right"></div> `,
},
],
corePlugins: { preflight: false },
}
crosscheck(() => {
test('important modifier with prefix', () => {
let config = {
important: false,
prefix: 'tw-',
darkMode: 'class',
content: [
{
raw: html`<!-- The string "!*" can cause problems if we don't handle it, let's include it -->
<div class="!*"></div>
<div class="!tw-container"></div>
<div class="!tw-font-bold"></div>
<div class="hover:!tw-text-center"></div>
<div class="lg:!tw-opacity-50"></div>
<div class="xl:focus:disabled:!tw-float-right"></div> `,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\!tw-container {
width: 100% !important;
}
@media (min-width: 640px) {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\!tw-container {
max-width: 640px !important;
width: 100% !important;
}
}
@media (min-width: 768px) {
.\!tw-container {
max-width: 768px !important;
@media (min-width: 640px) {
.\!tw-container {
max-width: 640px !important;
}
}
}
@media (min-width: 1024px) {
.\!tw-container {
max-width: 1024px !important;
@media (min-width: 768px) {
.\!tw-container {
max-width: 768px !important;
}
}
}
@media (min-width: 1280px) {
.\!tw-container {
max-width: 1280px !important;
@media (min-width: 1024px) {
.\!tw-container {
max-width: 1024px !important;
}
}
}
@media (min-width: 1536px) {
.\!tw-container {
max-width: 1536px !important;
@media (min-width: 1280px) {
.\!tw-container {
max-width: 1280px !important;
}
}
}
.\!tw-font-bold {
font-weight: 700 !important;
}
.hover\:\!tw-text-center:hover {
text-align: center !important;
}
@media (min-width: 1024px) {
.lg\:\!tw-opacity-50 {
opacity: 0.5 !important;
@media (min-width: 1536px) {
.\!tw-container {
max-width: 1536px !important;
}
}
}
@media (min-width: 1280px) {
.xl\:focus\:disabled\:\!tw-float-right:disabled:focus {
float: right !important;
.\!tw-font-bold {
font-weight: 700 !important;
}
}
`)
.hover\:\!tw-text-center:hover {
text-align: center !important;
}
@media (min-width: 1024px) {
.lg\:\!tw-opacity-50 {
opacity: 0.5 !important;
}
}
@media (min-width: 1280px) {
.xl\:focus\:disabled\:\!tw-float-right:disabled:focus {
float: right !important;
}
}
`)
})
})
})

View File

@ -1,109 +1,111 @@
import { run, css, html } from './util/run'
import { crosscheck, run, html, css } from './util/run'
test('important modifier', () => {
let config = {
important: false,
darkMode: 'class',
content: [
{
raw: html`
<div class="!container"></div>
<div class="!font-bold"></div>
<div class="hover:!text-center"></div>
<div class="lg:!opacity-50"></div>
<div class="xl:focus:disabled:!float-right"></div>
<div class="!custom-parent-5"></div>
<div class="btn !disabled"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ theme, matchUtilities, addComponents }) {
matchUtilities(
{
'custom-parent': (value) => {
return {
'.custom-child': {
margin: value,
},
}
crosscheck(() => {
test('important modifier', () => {
let config = {
important: false,
darkMode: 'class',
content: [
{
raw: html`
<div class="!container"></div>
<div class="!font-bold"></div>
<div class="hover:!text-center"></div>
<div class="lg:!opacity-50"></div>
<div class="xl:focus:disabled:!float-right"></div>
<div class="!custom-parent-5"></div>
<div class="btn !disabled"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ theme, matchUtilities, addComponents }) {
matchUtilities(
{
'custom-parent': (value) => {
return {
'.custom-child': {
margin: value,
},
}
},
},
},
{ values: theme('spacing') }
)
addComponents({
'.btn': {
'&.disabled, &:disabled': {
color: 'gray',
{ values: theme('spacing') }
)
addComponents({
'.btn': {
'&.disabled, &:disabled': {
color: 'gray',
},
},
},
})
},
],
}
})
},
],
}
let input = css`
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.\!container {
width: 100% !important;
}
@media (min-width: 640px) {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.\!container {
max-width: 640px !important;
width: 100% !important;
}
}
@media (min-width: 768px) {
.\!container {
max-width: 768px !important;
@media (min-width: 640px) {
.\!container {
max-width: 640px !important;
}
}
}
@media (min-width: 1024px) {
.\!container {
max-width: 1024px !important;
@media (min-width: 768px) {
.\!container {
max-width: 768px !important;
}
}
}
@media (min-width: 1280px) {
.\!container {
max-width: 1280px !important;
@media (min-width: 1024px) {
.\!container {
max-width: 1024px !important;
}
}
}
@media (min-width: 1536px) {
.\!container {
max-width: 1536px !important;
@media (min-width: 1280px) {
.\!container {
max-width: 1280px !important;
}
}
}
.btn.disabled,
.btn:disabled {
color: gray;
}
.btn.\!disabled {
color: gray !important;
}
.\!font-bold {
font-weight: 700 !important;
}
.\!custom-parent-5 .custom-child {
margin: 1.25rem !important;
}
.hover\:\!text-center:hover {
text-align: center !important;
}
@media (min-width: 1024px) {
.lg\:\!opacity-50 {
opacity: 0.5 !important;
@media (min-width: 1536px) {
.\!container {
max-width: 1536px !important;
}
}
}
@media (min-width: 1280px) {
.xl\:focus\:disabled\:\!float-right:disabled:focus {
float: right !important;
.btn.disabled,
.btn:disabled {
color: gray;
}
}
`)
.btn.\!disabled {
color: gray !important;
}
.\!font-bold {
font-weight: 700 !important;
}
.\!custom-parent-5 .custom-child {
margin: 1.25rem !important;
}
.hover\:\!text-center:hover {
text-align: center !important;
}
@media (min-width: 1024px) {
.lg\:\!opacity-50 {
opacity: 0.5 !important;
}
}
@media (min-width: 1280px) {
.xl\:focus\:disabled\:\!float-right:disabled:focus {
float: right !important;
}
}
`)
})
})
})

View File

@ -1,154 +1,156 @@
import { run, html, css, defaults } from './util/run'
import { crosscheck, run, html, css, defaults } from './util/run'
test('important selector', () => {
let config = {
important: '#app',
darkMode: 'class',
content: [
{
raw: html`
<div class="container"></div>
<div class="btn"></div>
<div class="animate-spin"></div>
<div class="custom-util"></div>
<div class="custom-component"></div>
<div class="custom-important-component"></div>
<div class="font-bold"></div>
<div class="md:hover:text-right"></div>
<div class="motion-safe:hover:text-center"></div>
<div class="dark:focus:text-left"></div>
<div class="group-hover:focus-within:text-left"></div>
<div class="rtl:active:text-center"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addComponents, addUtilities }) {
addComponents(
{
'.btn': {
button: 'yes',
crosscheck(() => {
test('important selector', () => {
let config = {
important: '#app',
darkMode: 'class',
content: [
{
raw: html`
<div class="container"></div>
<div class="btn"></div>
<div class="animate-spin"></div>
<div class="custom-util"></div>
<div class="custom-component"></div>
<div class="custom-important-component"></div>
<div class="font-bold"></div>
<div class="md:hover:text-right"></div>
<div class="motion-safe:hover:text-center"></div>
<div class="dark:focus:text-left"></div>
<div class="group-hover:focus-within:text-left"></div>
<div class="rtl:active:text-center"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addComponents, addUtilities }) {
addComponents(
{
'.btn': {
button: 'yes',
},
},
},
{ respectImportant: true }
)
addComponents(
{
'@font-face': {
'font-family': 'Inter',
{ respectImportant: true }
)
addComponents(
{
'@font-face': {
'font-family': 'Inter',
},
'@page': {
margin: '1cm',
},
},
'@page': {
margin: '1cm',
{ respectImportant: true }
)
addUtilities(
{
'.custom-util': {
button: 'no',
},
},
},
{ respectImportant: true }
)
addUtilities(
{
'.custom-util': {
button: 'no',
},
},
{ respectImportant: false }
)
},
],
}
let input = css`
@tailwind base;
@tailwind components;
@layer components {
.custom-component {
@apply font-bold;
}
.custom-important-component {
@apply text-center !important;
}
{ respectImportant: false }
)
},
],
}
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.container {
width: 100%;
let input = css`
@tailwind base;
@tailwind components;
@layer components {
.custom-component {
@apply font-bold;
}
.custom-important-component {
@apply text-center !important;
}
}
@media (min-width: 640px) {
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.container {
max-width: 640px;
width: 100%;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
}
#app .btn {
button: yes;
}
@font-face {
font-family: Inter;
}
@page {
margin: 1cm;
}
.custom-component {
font-weight: 700;
}
.custom-important-component {
text-align: center !important;
}
@keyframes spin {
to {
transform: rotate(360deg);
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
}
#app .animate-spin {
animation: spin 1s linear infinite;
}
#app .font-bold {
font-weight: 700;
}
.custom-util {
button: no;
}
#app .group:hover .group-hover\:focus-within\:text-left:focus-within {
text-align: left;
}
#app [dir='rtl'] .rtl\:active\:text-center:active {
text-align: center;
}
@media (prefers-reduced-motion: no-preference) {
#app .motion-safe\:hover\:text-center:hover {
#app .btn {
button: yes;
}
@font-face {
font-family: Inter;
}
@page {
margin: 1cm;
}
.custom-component {
font-weight: 700;
}
.custom-important-component {
text-align: center !important;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
#app .animate-spin {
animation: spin 1s linear infinite;
}
#app .font-bold {
font-weight: 700;
}
.custom-util {
button: no;
}
#app .group:hover .group-hover\:focus-within\:text-left:focus-within {
text-align: left;
}
#app [dir='rtl'] .rtl\:active\:text-center:active {
text-align: center;
}
}
#app .dark .dark\:focus\:text-left:focus {
text-align: left;
}
@media (min-width: 768px) {
#app .md\:hover\:text-right:hover {
text-align: right;
@media (prefers-reduced-motion: no-preference) {
#app .motion-safe\:hover\:text-center:hover {
text-align: center;
}
}
}
`)
#app .dark .dark\:focus\:text-left:focus {
text-align: left;
}
@media (min-width: 768px) {
#app .md\:hover\:text-right:hover {
text-align: right;
}
}
`)
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -1,302 +1,249 @@
import { run, html, css, defaults } from './util/run'
import { crosscheck, run, html, css, defaults } from './util/run'
test('custom user-land utilities', () => {
let config = {
content: [
{
raw: html`<div
class="focus:hover:align-chocolate align-banana hover:align-banana uppercase"
></div>`,
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let input = css`
@layer utilities {
.align-banana {
text-align: banana;
}
crosscheck(() => {
test('custom user-land utilities', () => {
let config = {
content: [
{
raw: html`<div
class="focus:hover:align-chocolate align-banana hover:align-banana uppercase"
></div>`,
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
@tailwind base;
@tailwind components;
@tailwind utilities;
let input = css`
@layer utilities {
.align-banana {
text-align: banana;
}
}
@layer utilities {
.align-chocolate {
text-align: chocolate;
}
}
`
@tailwind base;
@tailwind components;
@tailwind utilities;
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@layer utilities {
.align-chocolate {
text-align: chocolate;
}
}
`
.uppercase {
text-transform: uppercase;
}
.align-banana {
text-align: banana;
}
.hover\:align-banana:hover {
text-align: banana;
}
.focus\:hover\:align-chocolate:hover:focus {
text-align: chocolate;
}
`)
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.uppercase {
text-transform: uppercase;
}
.align-banana {
text-align: banana;
}
.hover\:align-banana:hover {
text-align: banana;
}
.focus\:hover\:align-chocolate:hover:focus {
text-align: chocolate;
}
`)
})
})
})
test('comments can be used inside layers without crashing', () => {
let config = {
content: [
{
raw: html`<div class="important-utility important-component"></div>`,
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
/* Important base */
div {
background-color: #bada55;
}
test('comments can be used inside layers without crashing', () => {
let config = {
content: [
{
raw: html`<div class="important-utility important-component"></div>`,
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
@layer utilities {
/* Important utility */
.important-utility {
text-align: banana;
}
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
/* Important component */
@layer base {
/* Important base */
div {
background-color: #bada55;
}
}
@layer utilities {
/* Important utility */
.important-utility {
text-align: banana;
}
}
@layer components {
/* Important component */
.important-component {
text-align: banana;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
/* Important base */
div {
background-color: #bada55;
}
${defaults}
/* Important component */
.important-component {
text-align: banana;
}
}
`
text-align: banana;
}
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
/* Important base */
div {
background-color: #bada55;
}
${defaults}
/* Important component */
.important-component {
text-align: banana;
}
/* Important utility */
.important-utility {
text-align: banana;
}
`)
/* Important utility */
.important-utility {
text-align: banana;
}
`)
})
})
})
test('comments can be used inside layers (with important) without crashing', () => {
let config = {
important: true,
content: [
{
raw: html`<div class="important-utility important-component"></div>`,
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
/* Important base */
div {
background-color: #bada55;
}
test('comments can be used inside layers (with important) without crashing', () => {
let config = {
important: true,
content: [
{
raw: html`<div class="important-utility important-component"></div>`,
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
@layer utilities {
/* Important utility */
.important-utility {
text-align: banana;
}
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
/* Important component */
@layer base {
/* Important base */
div {
background-color: #bada55;
}
}
@layer utilities {
/* Important utility */
.important-utility {
text-align: banana;
}
}
@layer components {
/* Important component */
.important-component {
text-align: banana;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
/* Important base */
div {
background-color: #bada55;
}
${defaults}
/* Important component */
.important-component {
text-align: banana;
}
}
`
text-align: banana;
}
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
/* Important base */
div {
background-color: #bada55;
}
${defaults}
/* Important component */
.important-component {
text-align: banana;
}
/* Important utility */
.important-utility {
text-align: banana !important;
}
`)
/* Important utility */
.important-utility {
text-align: banana !important;
}
`)
})
})
})
test('layers are grouped and inserted at the matching @tailwind rule', () => {
let config = {
content: [
{ raw: html`<div class="input btn card float-squirrel align-banana align-sandwich"></div>` },
],
plugins: [
function ({ addBase, addComponents, addUtilities }) {
addBase({ body: { margin: 0 } })
test('layers are grouped and inserted at the matching @tailwind rule', () => {
let config = {
content: [
{
raw: html`<div class="input btn card float-squirrel align-banana align-sandwich"></div>`,
},
],
plugins: [
function ({ addBase, addComponents, addUtilities }) {
addBase({ body: { margin: 0 } })
addComponents({
'.input': { background: 'white' },
})
addComponents({
'.input': { background: 'white' },
})
addUtilities({
'.float-squirrel': { float: 'squirrel' },
})
},
],
corePlugins: { preflight: false },
}
let input = css`
@layer vanilla {
strong {
font-weight: medium;
}
addUtilities({
'.float-squirrel': { float: 'squirrel' },
})
},
],
corePlugins: { preflight: false },
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.btn {
background: blue;
}
}
@layer utilities {
.align-banana {
text-align: banana;
}
}
@layer base {
h1 {
font-weight: bold;
}
}
@layer components {
.card {
border-radius: 12px;
}
}
@layer base {
p {
font-weight: normal;
}
}
@layer utilities {
.align-sandwich {
text-align: sandwich;
}
}
@layer chocolate {
a {
text-decoration: underline;
}
}
`
expect.assertions(2)
return run(input, config).then((result) => {
expect(result.warnings().length).toBe(0)
expect(result.css).toMatchFormattedCss(css`
let input = css`
@layer vanilla {
strong {
font-weight: medium;
}
}
body {
margin: 0;
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.btn {
background: blue;
}
}
h1 {
font-weight: bold;
@layer utilities {
.align-banana {
text-align: banana;
}
}
p {
font-weight: normal;
@layer base {
h1 {
font-weight: bold;
}
}
${defaults}
.input {
background: white;
@layer components {
.card {
border-radius: 12px;
}
}
.btn {
background: blue;
@layer base {
p {
font-weight: normal;
}
}
.card {
border-radius: 12px;
}
.float-squirrel {
float: squirrel;
}
.align-banana {
text-align: banana;
}
.align-sandwich {
text-align: sandwich;
@layer utilities {
.align-sandwich {
text-align: sandwich;
}
}
@layer chocolate {
@ -304,43 +251,100 @@ test('layers are grouped and inserted at the matching @tailwind rule', () => {
text-decoration: underline;
}
}
`)
})
})
`
it('should keep `@supports` rules inside `@layer`s', () => {
let config = {
content: [{ raw: html`<div class="test"></div>` }],
plugins: [],
}
expect.assertions(2)
let input = css`
@tailwind utilities;
@layer utilities {
.test {
--tw-test: 1;
}
@supports (backdrop-filter: blur(1px)) {
.test {
--tw-test: 0.9;
return run(input, config).then((result) => {
expect(result.warnings().length).toBe(0)
expect(result.css).toMatchFormattedCss(css`
@layer vanilla {
strong {
font-weight: medium;
}
}
}
body {
margin: 0;
}
h1 {
font-weight: bold;
}
p {
font-weight: normal;
}
${defaults}
.input {
background: white;
}
.btn {
background: blue;
}
.card {
border-radius: 12px;
}
.float-squirrel {
float: squirrel;
}
.align-banana {
text-align: banana;
}
.align-sandwich {
text-align: sandwich;
}
@layer chocolate {
a {
text-decoration: underline;
}
}
`)
})
})
it('should keep `@supports` rules inside `@layer`s', () => {
let config = {
content: [{ raw: html`<div class="test"></div>` }],
plugins: [],
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.test {
--tw-test: 1;
}
let input = css`
@tailwind utilities;
@supports (backdrop-filter: blur(1px)) {
@layer utilities {
.test {
--tw-test: 0.9;
--tw-test: 1;
}
@supports (backdrop-filter: blur(1px)) {
.test {
--tw-test: 0.9;
}
}
}
`)
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.test {
--tw-test: 1;
}
@supports (backdrop-filter: blur(1px)) {
.test {
--tw-test: 0.9;
}
}
`)
})
})
})

View File

@ -1,79 +1,81 @@
import { run, html, css } from './util/run'
import { crosscheck, run, html, css } from './util/run'
test('using @layer without @tailwind', async () => {
let config = {
content: [{ raw: html`<div class="foo"></div>` }],
}
let input = css`
@layer components {
.foo {
color: black;
}
crosscheck(() => {
test('using @layer without @tailwind', async () => {
let config = {
content: [{ raw: html`<div class="foo"></div>` }],
}
`
await expect(run(input, config)).rejects.toThrowError(
'`@layer components` is used but no matching `@tailwind components` directive is present.'
)
})
test('using @responsive without @tailwind', async () => {
let config = {
content: [{ raw: html`<div class="foo"></div>` }],
}
let input = css`
@responsive {
.foo {
color: black;
let input = css`
@layer components {
.foo {
color: black;
}
}
`
await expect(run(input, config)).rejects.toThrowError(
'`@layer components` is used but no matching `@tailwind components` directive is present.'
)
})
test('using @responsive without @tailwind', async () => {
let config = {
content: [{ raw: html`<div class="foo"></div>` }],
}
`
await expect(run(input, config)).rejects.toThrowError(
'`@responsive` is used but `@tailwind utilities` is missing.'
)
})
test('using @variants without @tailwind', async () => {
let config = {
content: [{ raw: html`<div class="foo"></div>` }],
}
let input = css`
@variants hover {
.foo {
color: black;
let input = css`
@responsive {
.foo {
color: black;
}
}
`
await expect(run(input, config)).rejects.toThrowError(
'`@responsive` is used but `@tailwind utilities` is missing.'
)
})
test('using @variants without @tailwind', async () => {
let config = {
content: [{ raw: html`<div class="foo"></div>` }],
}
`
await expect(run(input, config)).rejects.toThrowError(
'`@variants` is used but `@tailwind utilities` is missing.'
)
})
test('non-Tailwind @layer rules are okay', async () => {
let config = {
content: [{ raw: html`<div class="foo"></div>` }],
}
let input = css`
@layer custom {
.foo {
color: black;
let input = css`
@variants hover {
.foo {
color: black;
}
}
}
`
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
await expect(run(input, config)).rejects.toThrowError(
'`@variants` is used but `@tailwind utilities` is missing.'
)
})
test('non-Tailwind @layer rules are okay', async () => {
let config = {
content: [{ raw: html`<div class="foo"></div>` }],
}
let input = css`
@layer custom {
.foo {
color: black;
}
}
`)
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@layer custom {
.foo {
color: black;
}
}
`)
})
})
})

View File

@ -1,91 +1,95 @@
import { run, html, css, defaults } from './util/run'
import { crosscheck, run, html, css, defaults } from './util/run'
it('should be possible to matchComponents', () => {
let config = {
content: [
{
raw: html`<div class="card-[#0088cc] hover:card-[#f0f]">
<div class="card-header font-bold"></div>
<div class="shadow"></div>
<div class="card-footer text-center"></div>
</div>`,
crosscheck(() => {
it('should be possible to matchComponents', () => {
let config = {
content: [
{
raw: html`<div class="card-[#0088cc] hover:card-[#f0f]">
<div class="card-header font-bold"></div>
<div class="shadow"></div>
<div class="card-footer text-center"></div>
</div>`,
},
],
corePlugins: {
preflight: false,
},
],
corePlugins: {
preflight: false,
},
plugins: [
function ({ matchComponents }) {
matchComponents({
card: (value) => {
return [
{ color: value },
{
'.card-header': {
borderTopWidth: 3,
borderTopColor: value,
plugins: [
function ({ matchComponents }) {
matchComponents({
card: (value) => {
return [
{ color: value },
{
'.card-header': {
borderTopWidth: 3,
borderTopColor: value,
},
},
},
{
'.card-footer': {
borderBottomWidth: 3,
borderBottomColor: value,
{
'.card-footer': {
borderBottomWidth: 3,
borderBottomColor: value,
},
},
},
]
},
})
},
],
}
]
},
})
},
],
}
return run('@tailwind base; @tailwind components; @tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
${defaults}
return run('@tailwind base; @tailwind components; @tailwind utilities', config).then(
(result) => {
return expect(result.css).toMatchFormattedCss(css`
${defaults}
.card-\[\#0088cc\] {
color: #0088cc;
.card-\[\#0088cc\] {
color: #0088cc;
}
.card-\[\#0088cc\] .card-header {
border-top-width: 3px;
border-top-color: #0088cc;
}
.card-\[\#0088cc\] .card-footer {
border-bottom-width: 3px;
border-bottom-color: #0088cc;
}
.text-center {
text-align: center;
}
.font-bold {
font-weight: 700;
}
.shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.hover\:card-\[\#f0f\]:hover {
color: #f0f;
}
.hover\:card-\[\#f0f\]:hover .card-header {
border-top-width: 3px;
border-top-color: #f0f;
}
.hover\:card-\[\#f0f\]:hover .card-footer {
border-bottom-width: 3px;
border-bottom-color: #f0f;
}
`)
}
.card-\[\#0088cc\] .card-header {
border-top-width: 3px;
border-top-color: #0088cc;
}
.card-\[\#0088cc\] .card-footer {
border-bottom-width: 3px;
border-bottom-color: #0088cc;
}
.text-center {
text-align: center;
}
.font-bold {
font-weight: 700;
}
.shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.hover\:card-\[\#f0f\]:hover {
color: #f0f;
}
.hover\:card-\[\#f0f\]:hover .card-header {
border-top-width: 3px;
border-top-color: #f0f;
}
.hover\:card-\[\#f0f\]:hover .card-footer {
border-bottom-width: 3px;
border-bottom-color: #f0f;
}
`)
)
})
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,32 @@
import { elementSelectorParser } from '../src/lib/resolveDefaultsAtRules'
import { crosscheck } from './util/run'
it.each`
before | after
${'*'} | ${'*'}
${'*:hover'} | ${'*'}
${'* > *'} | ${'* > *'}
${'.foo'} | ${'.foo'}
${'.foo:hover'} | ${'.foo'}
${'.foo:focus:hover'} | ${'.foo'}
${'li:first-child'} | ${'li'}
${'li:before'} | ${'li:before'}
${'li::before'} | ${'li::before'}
${'#app .foo'} | ${'.foo'}
${'#app'} | ${'[id=app]'}
${'#app.other'} | ${'.other'}
${'input[type="text"]'} | ${'[type="text"]'}
${'input[type="text"].foo'} | ${'.foo'}
${'.group .group\\:foo'} | ${'.group\\:foo'}
${'.group:hover .group-hover\\:foo'} | ${'.group-hover\\:foo'}
${'.owl > * + *'} | ${'.owl > *'}
${'.owl > :not([hidden]) + :not([hidden])'} | ${'.owl > *'}
${'.group:hover .group-hover\\:owl > :not([hidden]) + :not([hidden])'} | ${'.group-hover\\:owl > *'}
${'.peer:first-child ~ .peer-first\\:shadow-md'} | ${'.peer-first\\:shadow-md'}
${'.whats ~ .next > span:hover'} | ${'span'}
${'.foo .bar ~ .baz > .next > span > article:hover'} | ${'article'}
`('should generate "$after" from "$before"', ({ before, after }) => {
expect(elementSelectorParser.transformSync(before).join(', ')).toEqual(after)
crosscheck(() => {
it.each`
before | after
${'*'} | ${'*'}
${'*:hover'} | ${'*'}
${'* > *'} | ${'* > *'}
${'.foo'} | ${'.foo'}
${'.foo:hover'} | ${'.foo'}
${'.foo:focus:hover'} | ${'.foo'}
${'li:first-child'} | ${'li'}
${'li:before'} | ${'li:before'}
${'li::before'} | ${'li::before'}
${'#app .foo'} | ${'.foo'}
${'#app'} | ${'[id=app]'}
${'#app.other'} | ${'.other'}
${'input[type="text"]'} | ${'[type="text"]'}
${'input[type="text"].foo'} | ${'.foo'}
${'.group .group\\:foo'} | ${'.group\\:foo'}
${'.group:hover .group-hover\\:foo'} | ${'.group-hover\\:foo'}
${'.owl > * + *'} | ${'.owl > *'}
${'.owl > :not([hidden]) + :not([hidden])'} | ${'.owl > *'}
${'.group:hover .group-hover\\:owl > :not([hidden]) + :not([hidden])'} | ${'.group-hover\\:owl > *'}
${'.peer:first-child ~ .peer-first\\:shadow-md'} | ${'.peer-first\\:shadow-md'}
${'.whats ~ .next > span:hover'} | ${'span'}
${'.foo .bar ~ .baz > .next > span > article:hover'} | ${'article'}
`('should generate "$after" from "$before"', ({ before, after }) => {
expect(elementSelectorParser.transformSync(before).join(', ')).toEqual(after)
})
})

View File

@ -1,97 +1,99 @@
import selectorParser from 'postcss-selector-parser'
import { run, html, css } from './util/run'
import { crosscheck, run, html, css } from './util/run'
test('modify selectors', () => {
let config = {
darkMode: 'class',
content: [
{
raw: html`
<div class="font-bold"></div>
<div class="foo:font-bold"></div>
<div class="foo:hover:font-bold"></div>
<div class="sm:foo:font-bold"></div>
<div class="md:foo:focus:font-bold"></div>
<div class="markdown">
<p>Lorem ipsum dolor sit amet...</p>
</div>
<div class="foo:markdown">
<p>Lorem ipsum dolor sit amet...</p>
</div>
<div class="foo:visited:markdown">
<p>Lorem ipsum dolor sit amet...</p>
</div>
<div class="lg:foo:disabled:markdown">
<p>Lorem ipsum dolor sit amet...</p>
</div>
`,
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [
function ({ addVariant }) {
addVariant('foo', ({ modifySelectors, separator }) => {
modifySelectors(({ selector }) => {
return selectorParser((selectors) => {
selectors.walkClasses((classNode) => {
classNode.value = `foo${separator}${classNode.value}`
classNode.parent.insertBefore(classNode, selectorParser().astSync(`.foo `))
})
}).processSync(selector)
crosscheck(() => {
test('modify selectors', () => {
let config = {
darkMode: 'class',
content: [
{
raw: html`
<div class="font-bold"></div>
<div class="foo:font-bold"></div>
<div class="foo:hover:font-bold"></div>
<div class="sm:foo:font-bold"></div>
<div class="md:foo:focus:font-bold"></div>
<div class="markdown">
<p>Lorem ipsum dolor sit amet...</p>
</div>
<div class="foo:markdown">
<p>Lorem ipsum dolor sit amet...</p>
</div>
<div class="foo:visited:markdown">
<p>Lorem ipsum dolor sit amet...</p>
</div>
<div class="lg:foo:disabled:markdown">
<p>Lorem ipsum dolor sit amet...</p>
</div>
`,
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [
function ({ addVariant }) {
addVariant('foo', ({ modifySelectors, separator }) => {
modifySelectors(({ selector }) => {
return selectorParser((selectors) => {
selectors.walkClasses((classNode) => {
classNode.value = `foo${separator}${classNode.value}`
classNode.parent.insertBefore(classNode, selectorParser().astSync(`.foo `))
})
}).processSync(selector)
})
})
})
},
],
}
let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.markdown > p {
margin-top: 12px;
}
},
],
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.markdown > p {
margin-top: 12px;
}
.font-bold {
font-weight: 700;
}
.foo .foo\:markdown > p {
margin-top: 12px;
}
.foo .foo\:font-bold {
font-weight: 700;
}
.foo .foo\:visited\:markdown:visited > p {
margin-top: 12px;
}
.foo .foo\:hover\:font-bold:hover {
font-weight: 700;
}
@media (min-width: 640px) {
.foo .sm\:foo\:font-bold {
font-weight: 700;
}
}
@media (min-width: 768px) {
.foo .md\:foo\:focus\:font-bold:focus {
font-weight: 700;
}
}
@media (min-width: 1024px) {
.foo .lg\:foo\:disabled\:markdown:disabled > p {
let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.markdown > p {
margin-top: 12px;
}
}
`)
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.markdown > p {
margin-top: 12px;
}
.font-bold {
font-weight: 700;
}
.foo .foo\:markdown > p {
margin-top: 12px;
}
.foo .foo\:font-bold {
font-weight: 700;
}
.foo .foo\:visited\:markdown:visited > p {
margin-top: 12px;
}
.foo .foo\:hover\:font-bold:hover {
font-weight: 700;
}
@media (min-width: 640px) {
.foo .sm\:foo\:font-bold {
font-weight: 700;
}
}
@media (min-width: 768px) {
.foo .md\:foo\:focus\:font-bold:focus {
font-weight: 700;
}
}
@media (min-width: 1024px) {
.foo .lg\:foo\:disabled\:markdown:disabled > p {
margin-top: 12px;
}
}
`)
})
})
})

View File

@ -1,52 +1,54 @@
import path from 'path'
import postcss from 'postcss'
import { run, html, css } from './util/run'
import { crosscheck, run, html, css } from './util/run'
function pluginThatMutatesRules() {
return (root) => {
root.walkRules((rule) => {
rule.nodes
.filter((node) => node.prop === 'background-image')
.forEach((node) => {
node.value = 'url("./bar.png")'
})
crosscheck(() => {
function pluginThatMutatesRules() {
return (root) => {
root.walkRules((rule) => {
rule.nodes
.filter((node) => node.prop === 'background-image')
.forEach((node) => {
node.value = 'url("./bar.png")'
})
return rule
})
return rule
})
}
}
}
test('plugins mutating rules after tailwind doesnt break it', async () => {
let config = {
content: [{ raw: html`<div class="bg-foo"></div>` }],
theme: {
backgroundImage: {
foo: 'url("./foo.png")',
test('plugins mutating rules after tailwind doesnt break it', async () => {
let config = {
content: [{ raw: html`<div class="bg-foo"></div>` }],
theme: {
backgroundImage: {
foo: 'url("./foo.png")',
},
},
},
plugins: [],
}
plugins: [],
}
function checkResult(result) {
expect(result.css).toMatchFormattedCss(css`
.bg-foo {
background-image: url('./foo.png');
}
`)
}
function checkResult(result) {
expect(result.css).toMatchFormattedCss(css`
.bg-foo {
background-image: url('./foo.png');
}
`)
}
// Verify the first run produces the expected result
let firstRun = await run('@tailwind utilities', config)
checkResult(firstRun)
// Verify the first run produces the expected result
let firstRun = await run('@tailwind utilities', config)
checkResult(firstRun)
// Outside of the context of tailwind jit more postcss plugins may operate on the AST:
// In this case we have a plugin that mutates rules directly
await postcss([pluginThatMutatesRules()]).process(firstRun, {
from: path.resolve(__filename),
// Outside of the context of tailwind jit more postcss plugins may operate on the AST:
// In this case we have a plugin that mutates rules directly
await postcss([pluginThatMutatesRules()]).process(firstRun, {
from: path.resolve(__filename),
})
// Verify subsequent runs don't produce mutated rules
let secondRun = await run('@tailwind utilities', config)
checkResult(secondRun)
})
// Verify subsequent runs don't produce mutated rules
let secondRun = await run('@tailwind utilities', config)
checkResult(secondRun)
})

View File

@ -1,14 +1,17 @@
import negateValue from '../src/util/negateValue'
import { crosscheck } from './util/run'
test('it negates numeric CSS values', () => {
expect(negateValue('5')).toEqual('-5')
expect(negateValue('10px')).toEqual('-10px')
expect(negateValue('18rem')).toEqual('-18rem')
expect(negateValue('-10')).toEqual('10')
expect(negateValue('-7ch')).toEqual('7ch')
})
crosscheck(() => {
test('it negates numeric CSS values', () => {
expect(negateValue('5')).toEqual('-5')
expect(negateValue('10px')).toEqual('-10px')
expect(negateValue('18rem')).toEqual('-18rem')
expect(negateValue('-10')).toEqual('10')
expect(negateValue('-7ch')).toEqual('7ch')
})
test('values that cannot be negated become undefined', () => {
expect(negateValue('auto')).toBeUndefined()
expect(negateValue('cover')).toBeUndefined()
test('values that cannot be negated become undefined', () => {
expect(negateValue('auto')).toBeUndefined()
expect(negateValue('cover')).toBeUndefined()
})
})

View File

@ -1,27 +1,29 @@
import * as path from 'path'
import { run, css, defaults } from './util/run'
import { crosscheck, run, css, defaults } from './util/run'
it('should be possible to use negated content patterns', () => {
let config = {
content: [
path.resolve(__dirname, './negated-content-*.test.html'),
'!' + path.resolve(__dirname, './negated-content-ignore.test.html'),
],
corePlugins: { preflight: false },
}
crosscheck(() => {
it('should be possible to use negated content patterns', () => {
let config = {
content: [
path.resolve(__dirname, './negated-content-*.test.html'),
'!' + path.resolve(__dirname, './negated-content-ignore.test.html'),
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.uppercase {
text-transform: uppercase;
}
`)
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.uppercase {
text-transform: uppercase;
}
`)
})
})
})

View File

@ -1,344 +1,348 @@
import { run, html, css } from './util/run'
import { crosscheck, run, html, css } from './util/run'
test('using a negative prefix with a negative scale value', () => {
let config = {
content: [{ raw: html`<div class="mt-2 -mt-2"></div>` }],
theme: {
margin: {
2: '8px',
'-2': '-4px',
crosscheck(() => {
test('using a negative prefix with a negative scale value', () => {
let config = {
content: [{ raw: html`<div class="mt-2 -mt-2"></div>` }],
theme: {
margin: {
2: '8px',
'-2': '-4px',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-2 {
margin-top: -4px;
}
.mt-2 {
margin-top: 8px;
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-2 {
margin-top: -4px;
}
.mt-2 {
margin-top: 8px;
}
`)
})
})
})
test('using a negative scale value with a plugin that does not support dynamic negative values', () => {
let config = {
content: [{ raw: html`<div class="-opacity-50"></div>` }],
theme: {
opacity: {
'-50': '0.5',
test('using a negative scale value with a plugin that does not support dynamic negative values', () => {
let config = {
content: [{ raw: html`<div class="-opacity-50"></div>` }],
theme: {
opacity: {
'-50': '0.5',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-opacity-50 {
opacity: 0.5;
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-opacity-50 {
opacity: 0.5;
}
`)
})
})
})
test('using a negative prefix without a negative scale value', () => {
let config = {
content: [{ raw: html`<div class="mt-5 -mt-5"></div>` }],
theme: {
margin: {
5: '20px',
test('using a negative prefix without a negative scale value', () => {
let config = {
content: [{ raw: html`<div class="mt-5 -mt-5"></div>` }],
theme: {
margin: {
5: '20px',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-5 {
margin-top: -20px;
}
.mt-5 {
margin-top: 20px;
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-5 {
margin-top: -20px;
}
.mt-5 {
margin-top: 20px;
}
`)
})
})
})
test('being an asshole', () => {
let config = {
content: [{ raw: html`<div class="-mt-[10px]"></div>` }],
theme: {
margin: {
'-[10px]': '55px',
test('being an asshole', () => {
let config = {
content: [{ raw: html`<div class="-mt-[10px]"></div>` }],
theme: {
margin: {
'-[10px]': '55px',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-\[10px\] {
margin-top: 55px;
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-\[10px\] {
margin-top: 55px;
}
`)
})
})
})
test('being a real asshole', () => {
let config = {
content: [{ raw: html`<div class="-mt-[10px]"></div>` }],
theme: {
margin: {
'[10px]': '55px',
test('being a real asshole', () => {
let config = {
content: [{ raw: html`<div class="-mt-[10px]"></div>` }],
theme: {
margin: {
'[10px]': '55px',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-\[10px\] {
margin-top: -55px;
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-\[10px\] {
margin-top: -55px;
}
`)
})
})
})
test('a value that includes a variable', () => {
let config = {
content: [{ raw: html`<div class="mt-5 -mt-5"></div>` }],
theme: {
margin: {
5: 'var(--sizing-5)',
test('a value that includes a variable', () => {
let config = {
content: [{ raw: html`<div class="mt-5 -mt-5"></div>` }],
theme: {
margin: {
5: 'var(--sizing-5)',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-5 {
margin-top: calc(var(--sizing-5) * -1);
}
.mt-5 {
margin-top: var(--sizing-5);
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-5 {
margin-top: calc(var(--sizing-5) * -1);
}
.mt-5 {
margin-top: var(--sizing-5);
}
`)
})
})
})
test('a value that includes a calc', () => {
let config = {
content: [{ raw: html`<div class="mt-5 -mt-5"></div>` }],
theme: {
margin: {
5: 'calc(52px * -3)',
test('a value that includes a calc', () => {
let config = {
content: [{ raw: html`<div class="mt-5 -mt-5"></div>` }],
theme: {
margin: {
5: 'calc(52px * -3)',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-5 {
margin-top: calc(calc(52px * -3) * -1);
}
.mt-5 {
margin-top: calc(52px * -3);
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-5 {
margin-top: calc(calc(52px * -3) * -1);
}
.mt-5 {
margin-top: calc(52px * -3);
}
`)
})
})
})
test('a value that includes min/max/clamp functions', () => {
let config = {
content: [{ raw: html`<div class="mt-min -mt-min mt-max -mt-max mt-clamp -mt-clamp"></div>` }],
theme: {
margin: {
min: 'min(100vmin, 3rem)',
max: 'max(100vmax, 3rem)',
clamp: 'clamp(1rem, 100vh, 3rem)',
test('a value that includes min/max/clamp functions', () => {
let config = {
content: [
{ raw: html`<div class="mt-min -mt-min mt-max -mt-max mt-clamp -mt-clamp"></div>` },
],
theme: {
margin: {
min: 'min(100vmin, 3rem)',
max: 'max(100vmax, 3rem)',
clamp: 'clamp(1rem, 100vh, 3rem)',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-clamp {
margin-top: calc(clamp(1rem, 100vh, 3rem) * -1);
}
.-mt-max {
margin-top: calc(max(100vmax, 3rem) * -1);
}
.-mt-min {
margin-top: calc(min(100vmin, 3rem) * -1);
}
.mt-clamp {
margin-top: clamp(1rem, 100vh, 3rem);
}
.mt-max {
margin-top: max(100vmax, 3rem);
}
.mt-min {
margin-top: min(100vmin, 3rem);
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-clamp {
margin-top: calc(clamp(1rem, 100vh, 3rem) * -1);
}
.-mt-max {
margin-top: calc(max(100vmax, 3rem) * -1);
}
.-mt-min {
margin-top: calc(min(100vmin, 3rem) * -1);
}
.mt-clamp {
margin-top: clamp(1rem, 100vh, 3rem);
}
.mt-max {
margin-top: max(100vmax, 3rem);
}
.mt-min {
margin-top: min(100vmin, 3rem);
}
`)
})
})
})
test('a keyword value', () => {
let config = {
content: [{ raw: html`<div class="-mt-auto mt-auto"></div>` }],
theme: {
margin: {
auto: 'auto',
test('a keyword value', () => {
let config = {
content: [{ raw: html`<div class="-mt-auto mt-auto"></div>` }],
theme: {
margin: {
auto: 'auto',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.mt-auto {
margin-top: auto;
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.mt-auto {
margin-top: auto;
}
`)
})
})
})
test('a zero value', () => {
let config = {
content: [{ raw: html`<div class="mt-0 -mt-0"></div>` }],
theme: {
margin: {
0: '0',
test('a zero value', () => {
let config = {
content: [{ raw: html`<div class="mt-0 -mt-0"></div>` }],
theme: {
margin: {
0: '0',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-0 {
margin-top: 0;
}
.mt-0 {
margin-top: 0;
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-0 {
margin-top: 0;
}
.mt-0 {
margin-top: 0;
}
`)
})
})
})
test('a color', () => {
let config = {
content: [{ raw: html`<div class="-bg-red"></div>` }],
theme: {
colors: {
red: 'red',
test('a color', () => {
let config = {
content: [{ raw: html`<div class="-bg-red"></div>` }],
theme: {
colors: {
red: 'red',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css``)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css``)
})
})
})
test('arbitrary values', () => {
let config = {
content: [{ raw: html`<div class="-mt-[10px]"></div>` }],
}
test('arbitrary values', () => {
let config = {
content: [{ raw: html`<div class="-mt-[10px]"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-\[10px\] {
margin-top: -10px;
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-\[10px\] {
margin-top: -10px;
}
`)
})
})
})
test('negating a negative scale value', () => {
let config = {
content: [{ raw: html`<div class="-mt-weird"></div>` }],
theme: {
margin: {
weird: '-15px',
test('negating a negative scale value', () => {
let config = {
content: [{ raw: html`<div class="-mt-weird"></div>` }],
theme: {
margin: {
weird: '-15px',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-weird {
margin-top: 15px;
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-weird {
margin-top: 15px;
}
`)
})
})
})
test('negating a default value', () => {
let config = {
content: [{ raw: html`<div class="-mt"></div>` }],
theme: {
margin: {
DEFAULT: '15px',
test('negating a default value', () => {
let config = {
content: [{ raw: html`<div class="-mt"></div>` }],
theme: {
margin: {
DEFAULT: '15px',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt {
margin-top: -15px;
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt {
margin-top: -15px;
}
`)
})
})
})
test('using a negative prefix with a negative default scale value', () => {
let config = {
content: [{ raw: html`<div class="mt -mt"></div>` }],
theme: {
margin: {
DEFAULT: '8px',
'-DEFAULT': '-4px',
test('using a negative prefix with a negative default scale value', () => {
let config = {
content: [{ raw: html`<div class="mt -mt"></div>` }],
theme: {
margin: {
DEFAULT: '8px',
'-DEFAULT': '-4px',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt {
margin-top: -4px;
}
.mt {
margin-top: 8px;
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt {
margin-top: -4px;
}
.mt {
margin-top: 8px;
}
`)
})
})
})
test('negating a default value with a configured prefix', () => {
let config = {
prefix: 'tw-',
content: [{ raw: html`<div class="tw--mt"></div>` }],
theme: {
margin: {
DEFAULT: '15px',
test('negating a default value with a configured prefix', () => {
let config = {
prefix: 'tw-',
content: [{ raw: html`<div class="tw--mt"></div>` }],
theme: {
margin: {
DEFAULT: '15px',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.tw--mt {
margin-top: -15px;
}
`)
})
})
test('arbitrary value keywords should be ignored', () => {
let config = {
content: [{ raw: html`<div class="-mt-[auto]"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css``)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.tw--mt {
margin-top: -15px;
}
`)
})
})
test('arbitrary value keywords should be ignored', () => {
let config = {
content: [{ raw: html`<div class="-mt-[auto]"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css``)
})
})
})

View File

@ -1,42 +1,41 @@
import { normalizeConfig } from '../src/util/normalizeConfig'
import { run, css } from './util/run'
import resolveConfig from '../src/public/resolve-config'
import { env } from '../src/lib/sharedState'
import log from '../src/util/log'
import { crosscheck, run, css } from './util/run'
let t = env.OXIDE ? test.skip : test
it.each`
config
${{ purge: [{ raw: 'text-center' }] }}
${{ purge: { content: [{ raw: 'text-center' }] } }}
${{ content: { content: [{ raw: 'text-center' }] } }}
`('should normalize content $config', ({ config }) => {
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.text-center {
text-align: center;
}
`)
crosscheck(({ stable, oxide }) => {
it.each`
config
${{ purge: [{ raw: 'text-center' }] }}
${{ purge: { content: [{ raw: 'text-center' }] } }}
${{ content: { content: [{ raw: 'text-center' }] } }}
`('should normalize content $config', ({ config }) => {
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.text-center {
text-align: center;
}
`)
})
})
})
it.each`
config
${{ purge: { safelist: ['text-center'] } }}
${{ purge: { options: { safelist: ['text-center'] } } }}
${{ content: { safelist: ['text-center'] } }}
`('should normalize safelist $config', ({ config }) => {
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.text-center {
text-align: center;
}
`)
it.each`
config
${{ purge: { safelist: ['text-center'] } }}
${{ purge: { options: { safelist: ['text-center'] } } }}
${{ content: { safelist: ['text-center'] } }}
`('should normalize safelist $config', ({ config }) => {
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.text-center {
text-align: center;
}
`)
})
})
})
t.each`
oxide.test.todo('should normalize extractors')
stable.test.each`
config
${{ content: [{ raw: 'text-center' }], purge: { extract: () => ['font-bold'] } }}
${{ content: [{ raw: 'text-center' }], purge: { extract: { DEFAULT: () => ['font-bold'] } } }}
@ -50,103 +49,104 @@ t.each`
}}
${{ content: [{ raw: 'text-center' }], purge: { extract: { html: () => ['font-bold'] } } }}
`('should normalize extractors $config', ({ config }) => {
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.font-bold {
font-weight: 700;
}
`)
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.font-bold {
font-weight: 700;
}
`)
})
})
})
t('should still be possible to use the "old" v2 config', () => {
let config = {
purge: {
oxide.test.todo('should still be possible to use the "old" v2 config')
stable.test('should still be possible to use the "old" v2 config', () => {
let config = {
purge: {
content: [
{ raw: 'text-svelte', extension: 'svelte' },
{ raw: '# My Big Heading', extension: 'md' },
],
options: {
defaultExtractor(content) {
return content.split(' ').concat(['font-bold'])
},
},
extract: {
svelte(content) {
return content.replace('svelte', 'center').split(' ')
},
},
transform: {
md() {
return 'text-4xl'
},
},
},
theme: {
extends: {},
},
variants: {
extends: {},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.text-center {
text-align: center;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.font-bold {
font-weight: 700;
}
`)
})
})
it('should keep content files with globs', () => {
let config = {
content: ['./example-folder/**/*.{html,js}'],
}
expect(normalizeConfig(resolveConfig(config)).content).toEqual({
files: ['./example-folder/**/*.{html,js}'],
relative: false,
extract: {},
transform: {},
})
})
it('should warn when we detect invalid globs with incorrect brace expansion', () => {
let spy = jest.spyOn(log, 'warn')
let config = {
content: [
{ raw: 'text-svelte', extension: 'svelte' },
{ raw: '# My Big Heading', extension: 'md' },
'./{example-folder}/**/*.{html,js}',
'./{example-folder}/**/*.{html}',
'./example-folder/**/*.{html}',
],
options: {
defaultExtractor(content) {
return content.split(' ').concat(['font-bold'])
},
},
extract: {
svelte(content) {
return content.replace('svelte', 'center').split(' ')
},
},
transform: {
md() {
return 'text-4xl'
},
},
},
theme: {
extends: {},
},
variants: {
extends: {},
},
}
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.text-center {
text-align: center;
}
// No rewrite happens
expect(normalizeConfig(resolveConfig(config)).content).toEqual({
files: [
'./{example-folder}/**/*.{html,js}',
'./{example-folder}/**/*.{html}',
'./example-folder/**/*.{html}',
],
relative: false,
extract: {},
transform: {},
})
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.font-bold {
font-weight: 700;
}
`)
// But a warning should happen
expect(spy).toHaveBeenCalledTimes(2)
expect(spy.mock.calls.map((x) => x[0])).toEqual(['invalid-glob-braces', 'invalid-glob-braces'])
spy.mockRestore()
})
})
it('should keep content files with globs', () => {
let config = {
content: ['./example-folder/**/*.{html,js}'],
}
expect(normalizeConfig(resolveConfig(config)).content).toEqual({
files: ['./example-folder/**/*.{html,js}'],
relative: false,
extract: {},
transform: {},
})
})
it('should warn when we detect invalid globs with incorrect brace expansion', () => {
let spy = jest.spyOn(log, 'warn')
let config = {
content: [
'./{example-folder}/**/*.{html,js}',
'./{example-folder}/**/*.{html}',
'./example-folder/**/*.{html}',
],
}
// No rewrite happens
expect(normalizeConfig(resolveConfig(config)).content).toEqual({
files: [
'./{example-folder}/**/*.{html,js}',
'./{example-folder}/**/*.{html}',
'./example-folder/**/*.{html}',
],
relative: false,
extract: {},
transform: {},
})
// But a warning should happen
expect(spy).toHaveBeenCalledTimes(2)
expect(spy.mock.calls.map((x) => x[0])).toEqual(['invalid-glob-braces', 'invalid-glob-braces'])
spy.mockClear()
})

View File

@ -1,4 +1,5 @@
import { normalize } from '../src/util/dataTypes'
import { crosscheck } from './util/run'
let table = [
['foo', 'foo'],
@ -46,6 +47,8 @@ let table = [
['color(0_0_0_/_1.0)', 'color(0 0 0 / 1.0)'],
]
it.each(table)('normalize data: %s', (input, output) => {
expect(normalize(input)).toBe(output)
crosscheck(() => {
it.each(table)('normalize data: %s', (input, output) => {
expect(normalize(input)).toBe(output)
})
})

View File

@ -1,63 +1,66 @@
import { normalizeScreens } from '../src/util/normalizeScreens'
import { crosscheck } from './util/run'
it('should normalize an array of string values', () => {
let screens = ['768px', '1200px']
crosscheck(() => {
it('should normalize an array of string values', () => {
let screens = ['768px', '1200px']
expect(normalizeScreens(screens)).toEqual([
{ name: '768px', not: false, values: [{ min: '768px', max: undefined }] },
{ name: '1200px', not: false, values: [{ min: '1200px', max: undefined }] },
])
})
it('should normalize an object with string values', () => {
let screens = {
a: '768px',
b: '1200px',
}
expect(normalizeScreens(screens)).toEqual([
{ name: 'a', not: false, values: [{ min: '768px', max: undefined }] },
{ name: 'b', not: false, values: [{ min: '1200px', max: undefined }] },
])
})
it('should normalize an object with object values', () => {
let screens = {
a: { min: '768px' },
b: { max: '1200px' },
}
expect(normalizeScreens(screens)).toEqual([
{ name: 'a', not: false, values: [{ min: '768px', max: undefined }] },
{ name: 'b', not: false, values: [{ min: undefined, max: '1200px' }] },
])
})
it('should normalize an object with multiple object values', () => {
let screens = {
a: [{ min: '768px' }, { max: '1200px' }],
}
expect(normalizeScreens(screens)).toEqual([
{
name: 'a',
not: false,
values: [
{ max: undefined, min: '768px', raw: undefined },
{ max: '1200px', min: undefined, raw: undefined },
],
},
])
})
it('should normalize an object with object values (min-width normalized to width)', () => {
let screens = {
a: { 'min-width': '768px' },
b: { max: '1200px' },
}
expect(normalizeScreens(screens)).toEqual([
{ name: 'a', not: false, values: [{ min: '768px', max: undefined }] },
{ name: 'b', not: false, values: [{ min: undefined, max: '1200px' }] },
])
expect(normalizeScreens(screens)).toEqual([
{ name: '768px', not: false, values: [{ min: '768px', max: undefined }] },
{ name: '1200px', not: false, values: [{ min: '1200px', max: undefined }] },
])
})
it('should normalize an object with string values', () => {
let screens = {
a: '768px',
b: '1200px',
}
expect(normalizeScreens(screens)).toEqual([
{ name: 'a', not: false, values: [{ min: '768px', max: undefined }] },
{ name: 'b', not: false, values: [{ min: '1200px', max: undefined }] },
])
})
it('should normalize an object with object values', () => {
let screens = {
a: { min: '768px' },
b: { max: '1200px' },
}
expect(normalizeScreens(screens)).toEqual([
{ name: 'a', not: false, values: [{ min: '768px', max: undefined }] },
{ name: 'b', not: false, values: [{ min: undefined, max: '1200px' }] },
])
})
it('should normalize an object with multiple object values', () => {
let screens = {
a: [{ min: '768px' }, { max: '1200px' }],
}
expect(normalizeScreens(screens)).toEqual([
{
name: 'a',
not: false,
values: [
{ max: undefined, min: '768px', raw: undefined },
{ max: '1200px', min: undefined, raw: undefined },
],
},
])
})
it('should normalize an object with object values (min-width normalized to width)', () => {
let screens = {
a: { 'min-width': '768px' },
b: { max: '1200px' },
}
expect(normalizeScreens(screens)).toEqual([
{ name: 'a', not: false, values: [{ min: '768px', max: undefined }] },
{ name: 'b', not: false, values: [{ min: undefined, max: '1200px' }] },
])
})
})

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,23 @@
import { run, html, css, defaults } from './util/run'
import { env } from '../src/lib/sharedState'
import { crosscheck, run, html, css, defaults } from './util/run'
let t = env.ENGINE === 'oxide' ? test : test.skip
crosscheck(({ stable, oxide }) => {
stable.test.todo('space-x uses physical properties')
oxide.test('space-x uses logical properties', () => {
let config = {
content: [{ raw: html`<div class="space-x-4"></div>` }],
corePlugins: { preflight: false },
}
beforeEach(() => {
env.OXIDE = true
})
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
${defaults}
afterEach(() => {
env.OXIDE = false
})
t('space-x uses logical properties', () => {
let config = {
content: [{ raw: html`<div class="space-x-4"></div>` }],
corePlugins: { preflight: false },
}
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
${defaults}
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-inline-end: calc(1rem * var(--tw-space-x-reverse));
margin-inline-start: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
`)
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-inline-end: calc(1rem * var(--tw-space-x-reverse));
margin-inline-start: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
`)
})
})
})

View File

@ -1,136 +1,138 @@
import { run, html, css } from './util/run'
import { crosscheck, run, html, css } from './util/run'
test('basic parallel variants', async () => {
let config = {
content: [
{
raw: html`<div
class="hover:test:font-black test:font-bold test:font-medium font-normal"
></div>`,
},
],
plugins: [
function test({ addVariant }) {
addVariant('test', ['& *::test', '&::test'])
},
],
}
crosscheck(() => {
test('basic parallel variants', async () => {
let config = {
content: [
{
raw: html`<div
class="hover:test:font-black test:font-bold test:font-medium font-normal"
></div>`,
},
],
plugins: [
function test({ addVariant }) {
addVariant('test', ['& *::test', '&::test'])
},
],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-normal {
font-weight: 400;
}
.test\:font-bold *::test {
font-weight: 700;
}
.test\:font-medium *::test {
font-weight: 500;
}
.hover\:test\:font-black *:hover::test {
font-weight: 900;
}
.test\:font-bold::test {
font-weight: 700;
}
.test\:font-medium::test {
font-weight: 500;
}
.hover\:test\:font-black:hover::test {
font-weight: 900;
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-normal {
font-weight: 400;
}
.test\:font-bold *::test {
font-weight: 700;
}
.test\:font-medium *::test {
font-weight: 500;
}
.hover\:test\:font-black *:hover::test {
font-weight: 900;
}
.test\:font-bold::test {
font-weight: 700;
}
.test\:font-medium::test {
font-weight: 500;
}
.hover\:test\:font-black:hover::test {
font-weight: 900;
}
`)
})
})
})
test('parallel variants can be generated using a function that returns parallel variants', async () => {
let config = {
content: [
{
raw: html`<div
class="hover:test:font-black test:font-bold test:font-medium font-normal"
></div>`,
},
],
plugins: [
function test({ addVariant }) {
addVariant('test', () => ['& *::test', '&::test'])
},
],
}
test('parallel variants can be generated using a function that returns parallel variants', async () => {
let config = {
content: [
{
raw: html`<div
class="hover:test:font-black test:font-bold test:font-medium font-normal"
></div>`,
},
],
plugins: [
function test({ addVariant }) {
addVariant('test', () => ['& *::test', '&::test'])
},
],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-normal {
font-weight: 400;
}
.test\:font-bold *::test {
font-weight: 700;
}
.test\:font-medium *::test {
font-weight: 500;
}
.test\:font-bold::test {
font-weight: 700;
}
.test\:font-medium::test {
font-weight: 500;
}
.hover\:test\:font-black *:hover::test {
font-weight: 900;
}
.hover\:test\:font-black:hover::test {
font-weight: 900;
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-normal {
font-weight: 400;
}
.test\:font-bold *::test {
font-weight: 700;
}
.test\:font-medium *::test {
font-weight: 500;
}
.test\:font-bold::test {
font-weight: 700;
}
.test\:font-medium::test {
font-weight: 500;
}
.hover\:test\:font-black *:hover::test {
font-weight: 900;
}
.hover\:test\:font-black:hover::test {
font-weight: 900;
}
`)
})
})
})
test('a function that returns parallel variants can modify the container', async () => {
let config = {
content: [
{
raw: html`<div
class="hover:test:font-black test:font-bold test:font-medium font-normal"
></div>`,
},
],
plugins: [
function test({ addVariant }) {
addVariant('test', ({ container }) => {
container.walkDecls((decl) => {
decl.value = `calc(0 + ${decl.value})`
test('a function that returns parallel variants can modify the container', async () => {
let config = {
content: [
{
raw: html`<div
class="hover:test:font-black test:font-bold test:font-medium font-normal"
></div>`,
},
],
plugins: [
function test({ addVariant }) {
addVariant('test', ({ container }) => {
container.walkDecls((decl) => {
decl.value = `calc(0 + ${decl.value})`
})
return ['& *::test', '&::test']
})
},
],
}
return ['& *::test', '&::test']
})
},
],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-normal {
font-weight: 400;
}
.test\:font-bold *::test {
font-weight: calc(0 + 700);
}
.test\:font-medium *::test {
font-weight: calc(0 + 500);
}
.test\:font-bold::test {
font-weight: calc(0 + 700);
}
.test\:font-medium::test {
font-weight: calc(0 + 500);
}
.hover\:test\:font-black *:hover::test {
font-weight: calc(0 + 900);
}
.hover\:test\:font-black:hover::test {
font-weight: calc(0 + 900);
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-normal {
font-weight: 400;
}
.test\:font-bold *::test {
font-weight: calc(0 + 700);
}
.test\:font-medium *::test {
font-weight: calc(0 + 500);
}
.test\:font-bold::test {
font-weight: calc(0 + 700);
}
.test\:font-medium::test {
font-weight: calc(0 + 500);
}
.hover\:test\:font-black *:hover::test {
font-weight: calc(0 + 900);
}
.hover\:test\:font-black:hover::test {
font-weight: calc(0 + 900);
}
`)
})
})
})

View File

@ -1,200 +1,203 @@
import parseAnimationValue from '../src/util/parseAnimationValue'
import { crosscheck } from './util/run'
describe('Tailwind Defaults', () => {
it.each([
[
'spin 1s linear infinite',
{
value: 'spin 1s linear infinite',
name: 'spin',
duration: '1s',
timingFunction: 'linear',
iterationCount: 'infinite',
},
],
[
'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
{
value: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
name: 'ping',
duration: '1s',
timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
iterationCount: 'infinite',
},
],
[
'bounce 1s infinite',
{
value: 'bounce 1s infinite',
name: 'bounce',
duration: '1s',
iterationCount: 'infinite',
},
],
])('should be possible to parse: "%s"', (input, expected) => {
const parsed = parseAnimationValue(input)
expect(parsed).toHaveLength(1)
expect(parsed[0]).toEqual(expected)
})
})
describe('css variables', () => {
it('should be possible to use css variables', () => {
let parsed = parseAnimationValue('jump var(--animation-duration, 10s) linear infinite')
expect(parsed[0]).toEqual({
value: 'jump var(--animation-duration, 10s) linear infinite',
name: 'jump',
timingFunction: 'linear',
iterationCount: 'infinite',
unknown: ['var(--animation-duration, 10s)'],
})
})
})
describe('MDN Examples', () => {
it.each([
[
'3s ease-in 1s 2 reverse both paused slidein',
{
value: '3s ease-in 1s 2 reverse both paused slidein',
delay: '1s',
direction: 'reverse',
duration: '3s',
fillMode: 'both',
iterationCount: '2',
name: 'slidein',
playState: 'paused',
timingFunction: 'ease-in',
},
],
[
'slidein 3s linear 1s',
{
value: 'slidein 3s linear 1s',
delay: '1s',
duration: '3s',
name: 'slidein',
timingFunction: 'linear',
},
],
['slidein 3s', { value: 'slidein 3s', duration: '3s', name: 'slidein' }],
])('should be possible to parse: "%s"', (input, expected) => {
const parsed = parseAnimationValue(input)
expect(parsed).toHaveLength(1)
expect(parsed[0]).toEqual(expected)
})
})
describe('duration & delay', () => {
it.each([
// Positive seconds (integer)
['spin 2s 1s linear', { duration: '2s', delay: '1s' }],
// Negative seconds (integer)
['spin -2s -1s linear', { duration: '-2s', delay: '-1s' }],
// Positive seconds (float)
['spin 2.321s 1.321s linear', { duration: '2.321s', delay: '1.321s' }],
// Negative seconds (float)
['spin -2.321s -1.321s linear', { duration: '-2.321s', delay: '-1.321s' }],
// Positive milliseconds (integer)
['spin 200ms 100ms linear', { duration: '200ms', delay: '100ms' }],
// Negative milliseconds (integer)
['spin -200ms -100ms linear', { duration: '-200ms', delay: '-100ms' }],
// Positive milliseconds (float)
['spin 200.321ms 100.321ms linear', { duration: '200.321ms', delay: '100.321ms' }],
// Negative milliseconds (float)
['spin -200.321ms -100.321ms linear', { duration: '-200.321ms', delay: '-100.321ms' }],
])('should be possible to parse "%s" into %o', (input, { duration, delay }) => {
const parsed = parseAnimationValue(input)
expect(parsed).toHaveLength(1)
expect(parsed[0].duration).toEqual(duration)
expect(parsed[0].delay).toEqual(delay)
})
})
describe('iteration count', () => {
it.each([
// Number
['1 spin 200s 100s linear', '1'],
['spin 2 200s 100s linear', '2'],
['spin 200s 3 100s linear', '3'],
['spin 200s 100s 4 linear', '4'],
['spin 200s 100s linear 5', '5'],
// Infinite
['infinite spin 200s 100s linear', 'infinite'],
['spin infinite 200s 100s linear', 'infinite'],
['spin 200s infinite 100s linear', 'infinite'],
['spin 200s 100s infinite linear', 'infinite'],
['spin 200s 100s linear infinite', 'infinite'],
])(
'should be possible to parse "%s" with an iteraction count of "%s"',
(input, iterationCount) => {
crosscheck(() => {
describe('Tailwind Defaults', () => {
it.each([
[
'spin 1s linear infinite',
{
value: 'spin 1s linear infinite',
name: 'spin',
duration: '1s',
timingFunction: 'linear',
iterationCount: 'infinite',
},
],
[
'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
{
value: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
name: 'ping',
duration: '1s',
timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
iterationCount: 'infinite',
},
],
[
'bounce 1s infinite',
{
value: 'bounce 1s infinite',
name: 'bounce',
duration: '1s',
iterationCount: 'infinite',
},
],
])('should be possible to parse: "%s"', (input, expected) => {
const parsed = parseAnimationValue(input)
expect(parsed).toHaveLength(1)
expect(parsed[0].iterationCount).toEqual(iterationCount)
}
)
})
expect(parsed[0]).toEqual(expected)
})
})
describe('multiple animations', () => {
it('should be possible to parse multiple applications at once', () => {
const input = [
'spin 1s linear infinite',
'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite',
].join(',')
const parsed = parseAnimationValue(input)
expect(parsed).toHaveLength(3)
expect(parsed).toEqual([
{
value: 'spin 1s linear infinite',
name: 'spin',
duration: '1s',
describe('css variables', () => {
it('should be possible to use css variables', () => {
let parsed = parseAnimationValue('jump var(--animation-duration, 10s) linear infinite')
expect(parsed[0]).toEqual({
value: 'jump var(--animation-duration, 10s) linear infinite',
name: 'jump',
timingFunction: 'linear',
iterationCount: 'infinite',
},
{
value: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
name: 'ping',
duration: '1s',
timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
iterationCount: 'infinite',
},
{
value: 'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite',
name: 'pulse',
duration: '2s',
timingFunction: 'cubic-bezier(0.4, 0, 0.6)',
iterationCount: 'infinite',
},
])
unknown: ['var(--animation-duration, 10s)'],
})
})
})
describe('MDN Examples', () => {
it.each([
[
'3s ease-in 1s 2 reverse both paused slidein',
{
value: '3s ease-in 1s 2 reverse both paused slidein',
delay: '1s',
direction: 'reverse',
duration: '3s',
fillMode: 'both',
iterationCount: '2',
name: 'slidein',
playState: 'paused',
timingFunction: 'ease-in',
},
],
[
'slidein 3s linear 1s',
{
value: 'slidein 3s linear 1s',
delay: '1s',
duration: '3s',
name: 'slidein',
timingFunction: 'linear',
},
],
['slidein 3s', { value: 'slidein 3s', duration: '3s', name: 'slidein' }],
])('should be possible to parse: "%s"', (input, expected) => {
const parsed = parseAnimationValue(input)
expect(parsed).toHaveLength(1)
expect(parsed[0]).toEqual(expected)
})
})
describe('duration & delay', () => {
it.each([
// Positive seconds (integer)
['spin 2s 1s linear', { duration: '2s', delay: '1s' }],
// Negative seconds (integer)
['spin -2s -1s linear', { duration: '-2s', delay: '-1s' }],
// Positive seconds (float)
['spin 2.321s 1.321s linear', { duration: '2.321s', delay: '1.321s' }],
// Negative seconds (float)
['spin -2.321s -1.321s linear', { duration: '-2.321s', delay: '-1.321s' }],
// Positive milliseconds (integer)
['spin 200ms 100ms linear', { duration: '200ms', delay: '100ms' }],
// Negative milliseconds (integer)
['spin -200ms -100ms linear', { duration: '-200ms', delay: '-100ms' }],
// Positive milliseconds (float)
['spin 200.321ms 100.321ms linear', { duration: '200.321ms', delay: '100.321ms' }],
// Negative milliseconds (float)
['spin -200.321ms -100.321ms linear', { duration: '-200.321ms', delay: '-100.321ms' }],
])('should be possible to parse "%s" into %o', (input, { duration, delay }) => {
const parsed = parseAnimationValue(input)
expect(parsed).toHaveLength(1)
expect(parsed[0].duration).toEqual(duration)
expect(parsed[0].delay).toEqual(delay)
})
})
describe('iteration count', () => {
it.each([
// Number
['1 spin 200s 100s linear', '1'],
['spin 2 200s 100s linear', '2'],
['spin 200s 3 100s linear', '3'],
['spin 200s 100s 4 linear', '4'],
['spin 200s 100s linear 5', '5'],
// Infinite
['infinite spin 200s 100s linear', 'infinite'],
['spin infinite 200s 100s linear', 'infinite'],
['spin 200s infinite 100s linear', 'infinite'],
['spin 200s 100s infinite linear', 'infinite'],
['spin 200s 100s linear infinite', 'infinite'],
])(
'should be possible to parse "%s" with an iteraction count of "%s"',
(input, iterationCount) => {
const parsed = parseAnimationValue(input)
expect(parsed).toHaveLength(1)
expect(parsed[0].iterationCount).toEqual(iterationCount)
}
)
})
describe('multiple animations', () => {
it('should be possible to parse multiple applications at once', () => {
const input = [
'spin 1s linear infinite',
'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite',
].join(',')
const parsed = parseAnimationValue(input)
expect(parsed).toHaveLength(3)
expect(parsed).toEqual([
{
value: 'spin 1s linear infinite',
name: 'spin',
duration: '1s',
timingFunction: 'linear',
iterationCount: 'infinite',
},
{
value: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
name: 'ping',
duration: '1s',
timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
iterationCount: 'infinite',
},
{
value: 'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite',
name: 'pulse',
duration: '2s',
timingFunction: 'cubic-bezier(0.4, 0, 0.6)',
iterationCount: 'infinite',
},
])
})
})
it.each`
input | value | direction | playState | fillMode | iterationCount | timingFunction | duration | delay | name
${'1s spin 1s infinite'} | ${'1s spin 1s infinite'} | ${undefined} | ${undefined} | ${undefined} | ${'infinite'} | ${undefined} | ${'1s'} | ${'1s'} | ${'spin'}
${'infinite infinite 1s 1s'} | ${'infinite infinite 1s 1s'} | ${undefined} | ${undefined} | ${undefined} | ${'infinite'} | ${undefined} | ${'1s'} | ${'1s'} | ${'infinite'}
${'ease 1s ease 1s'} | ${'ease 1s ease 1s'} | ${undefined} | ${undefined} | ${undefined} | ${undefined} | ${'ease'} | ${'1s'} | ${'1s'} | ${'ease'}
${'normal paused backwards infinite ease-in 1s 2s name'} | ${'normal paused backwards infinite ease-in 1s 2s name'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
${'paused backwards infinite ease-in 1s 2s name normal'} | ${'paused backwards infinite ease-in 1s 2s name normal'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
${'backwards infinite ease-in 1s 2s name normal paused'} | ${'backwards infinite ease-in 1s 2s name normal paused'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
${'infinite ease-in 1s 2s name normal paused backwards'} | ${'infinite ease-in 1s 2s name normal paused backwards'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
${'ease-in 1s 2s name normal paused backwards infinite'} | ${'ease-in 1s 2s name normal paused backwards infinite'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
${'1s 2s name normal paused backwards infinite ease-in'} | ${'1s 2s name normal paused backwards infinite ease-in'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
${'2s name normal paused backwards infinite ease-in 1s'} | ${'2s name normal paused backwards infinite ease-in 1s'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'2s'} | ${'1s'} | ${'name'}
${'name normal paused backwards infinite ease-in 1s 2s'} | ${'name normal paused backwards infinite ease-in 1s 2s'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
${' name normal paused backwards infinite ease-in 1s 2s '} | ${'name normal paused backwards infinite ease-in 1s 2s'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
`('should parse "$input" correctly', ({ input, ...expected }) => {
let parsed = parseAnimationValue(input)
expect(parsed).toHaveLength(1)
expect(parsed[0]).toEqual(expected)
})
})
it.each`
input | value | direction | playState | fillMode | iterationCount | timingFunction | duration | delay | name
${'1s spin 1s infinite'} | ${'1s spin 1s infinite'} | ${undefined} | ${undefined} | ${undefined} | ${'infinite'} | ${undefined} | ${'1s'} | ${'1s'} | ${'spin'}
${'infinite infinite 1s 1s'} | ${'infinite infinite 1s 1s'} | ${undefined} | ${undefined} | ${undefined} | ${'infinite'} | ${undefined} | ${'1s'} | ${'1s'} | ${'infinite'}
${'ease 1s ease 1s'} | ${'ease 1s ease 1s'} | ${undefined} | ${undefined} | ${undefined} | ${undefined} | ${'ease'} | ${'1s'} | ${'1s'} | ${'ease'}
${'normal paused backwards infinite ease-in 1s 2s name'} | ${'normal paused backwards infinite ease-in 1s 2s name'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
${'paused backwards infinite ease-in 1s 2s name normal'} | ${'paused backwards infinite ease-in 1s 2s name normal'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
${'backwards infinite ease-in 1s 2s name normal paused'} | ${'backwards infinite ease-in 1s 2s name normal paused'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
${'infinite ease-in 1s 2s name normal paused backwards'} | ${'infinite ease-in 1s 2s name normal paused backwards'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
${'ease-in 1s 2s name normal paused backwards infinite'} | ${'ease-in 1s 2s name normal paused backwards infinite'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
${'1s 2s name normal paused backwards infinite ease-in'} | ${'1s 2s name normal paused backwards infinite ease-in'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
${'2s name normal paused backwards infinite ease-in 1s'} | ${'2s name normal paused backwards infinite ease-in 1s'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'2s'} | ${'1s'} | ${'name'}
${'name normal paused backwards infinite ease-in 1s 2s'} | ${'name normal paused backwards infinite ease-in 1s 2s'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
${' name normal paused backwards infinite ease-in 1s 2s '} | ${'name normal paused backwards infinite ease-in 1s 2s'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'}
`('should parse "$input" correctly', ({ input, ...expected }) => {
let parsed = parseAnimationValue(input)
expect(parsed).toHaveLength(1)
expect(parsed[0]).toEqual(expected)
})

View File

@ -1,316 +1,319 @@
import parseObjectStyles from '../src/util/parseObjectStyles'
import postcss from 'postcss'
import { crosscheck, css } from './util/run'
function css(nodes) {
function toCss(nodes) {
return postcss.root({ nodes }).toString()
}
test('it parses simple single class definitions', () => {
const result = parseObjectStyles({
'.foobar': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
},
})
expect(css(result)).toMatchCss(`
.foobar {
background-color: red;
color: white;
padding: 1rem
}
`)
})
test('it parses multiple class definitions', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
},
'.bar': {
width: '200px',
height: '100px',
},
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem
}
.bar {
width: 200px;
height: 100px
}
`)
})
test('it parses nested pseudo-selectors', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
':hover': {
backgroundColor: 'orange',
crosscheck(() => {
test('it parses simple single class definitions', () => {
const result = parseObjectStyles({
'.foobar': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
},
':focus': {
backgroundColor: 'blue',
},
},
})
expect(toCss(result)).toMatchFormattedCss(css`
.foobar {
background-color: red;
color: white;
padding: 1rem;
}
`)
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
.foo:hover {
background-color: orange;
}
.foo:focus {
background-color: blue;
}
`)
})
test('it parses top-level media queries', () => {
const result = parseObjectStyles({
'@media (min-width: 200px)': {
test('it parses multiple class definitions', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'orange',
backgroundColor: 'red',
color: 'white',
padding: '1rem',
},
},
'.bar': {
width: '200px',
height: '100px',
},
})
expect(toCss(result)).toMatchFormattedCss(css`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
.bar {
width: 200px;
height: 100px;
}
`)
})
expect(css(result)).toMatchCss(`
@media (min-width: 200px) {
.foo {
background-color: orange
}
}
`)
})
test('it parses nested pseudo-selectors', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'&:hover': {
backgroundColor: 'orange',
},
'&:focus': {
backgroundColor: 'blue',
},
},
})
test('it parses nested media queries', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
expect(toCss(result)).toMatchFormattedCss(css`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
.foo:hover {
background-color: orange;
}
.foo:focus {
background-color: blue;
}
`)
})
test('it parses top-level media queries', () => {
const result = parseObjectStyles({
'@media (min-width: 200px)': {
backgroundColor: 'orange',
'.foo': {
backgroundColor: 'orange',
},
},
},
})
expect(toCss(result)).toMatchFormattedCss(css`
@media (min-width: 200px) {
.foo {
background-color: orange;
}
}
`)
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
@media (min-width: 200px) {
.foo {
background-color: orange;
}
}
`)
})
test('it bubbles nested screen rules', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'@screen sm': {
backgroundColor: 'orange',
},
},
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
@screen sm {
.foo {
background-color: orange;
}
}
`)
})
test('it parses pseudo-selectors in nested media queries', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
':hover': {
test('it parses nested media queries', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'@media (min-width: 200px)': {
backgroundColor: 'orange',
},
},
},
})
expect(toCss(result)).toMatchFormattedCss(css`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
@media (min-width: 200px) {
.foo {
background-color: orange;
}
}
`)
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
@media (min-width: 200px) {
.foo:hover {
test('it bubbles nested screen rules', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'@screen sm': {
backgroundColor: 'orange',
},
},
})
expect(toCss(result)).toMatchFormattedCss(css`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
@screen sm {
.foo {
background-color: orange;
}
}
`)
})
test('it parses pseudo-selectors in nested media queries', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'&:hover': {
'@media (min-width: 200px)': {
backgroundColor: 'orange',
},
},
},
})
expect(toCss(result)).toMatchFormattedCss(css`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
@media (min-width: 200px) {
.foo:hover {
background-color: orange;
}
}
`)
})
test('it parses descendant selectors', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'.bar': {
backgroundColor: 'orange',
},
},
})
expect(toCss(result)).toMatchFormattedCss(css`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
.foo .bar {
background-color: orange;
}
}
`)
})
test('it parses descendant selectors', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'.bar': {
backgroundColor: 'orange',
},
},
`)
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
.foo .bar {
background-color: orange;
}
`)
})
test('it parses nested multi-class selectors', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'&.bar': {
backgroundColor: 'orange',
},
},
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
.foo.bar {
background-color: orange;
}
`)
})
test('it parses nested multi-class selectors in media queries', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'@media (min-width: 200px)': {
test('it parses nested multi-class selectors', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'&.bar': {
backgroundColor: 'orange',
},
},
},
})
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
@media (min-width: 200px) {
expect(toCss(result)).toMatchFormattedCss(css`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
.foo.bar {
background-color: orange;
}
}
`)
})
test('it strips empty selectors when nesting', () => {
const result = parseObjectStyles({
'.foo': {
'.bar': {
backgroundColor: 'orange',
},
},
`)
})
expect(css(result)).toMatchCss(`
.foo .bar {
background-color: orange
}
`)
})
test('it can parse an array of styles', () => {
const result = parseObjectStyles([
{
test('it parses nested multi-class selectors in media queries', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'orange',
},
},
{
'.bar': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'@media (min-width: 200px)': {
'&.bar': {
backgroundColor: 'orange',
},
},
},
},
{
'.foo': {
backgroundColor: 'blue',
},
},
])
})
expect(css(result)).toMatchCss(`
.foo {
background-color: orange
}
.bar {
background-color: red
}
.foo {
background-color: blue
}
`)
})
test('custom properties preserve their case', () => {
const result = parseObjectStyles({
':root': {
'--colors-aColor-500': '0',
},
expect(toCss(result)).toMatchFormattedCss(css`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
@media (min-width: 200px) {
.foo.bar {
background-color: orange;
}
}
`)
})
expect(css(result)).toMatchCss(`
:root {
--colors-aColor-500: 0;
}
`)
test('it strips empty selectors when nesting', () => {
const result = parseObjectStyles({
'.foo': {
'.bar': {
backgroundColor: 'orange',
},
},
})
expect(toCss(result)).toMatchFormattedCss(css`
.foo .bar {
background-color: orange;
}
`)
})
test('it can parse an array of styles', () => {
const result = parseObjectStyles([
{
'.foo': {
backgroundColor: 'orange',
},
},
{
'.bar': {
backgroundColor: 'red',
},
},
{
'.foo': {
backgroundColor: 'blue',
},
},
])
expect(toCss(result)).toMatchFormattedCss(css`
.foo {
background-color: orange;
}
.bar {
background-color: red;
}
.foo {
background-color: blue;
}
`)
})
test('custom properties preserve their case', () => {
const result = parseObjectStyles({
':root': {
'--colors-aColor-500': '0',
},
})
expect(toCss(result)).toMatchFormattedCss(css`
:root {
--colors-aColor-500: 0;
}
`)
})
})

View File

@ -1,42 +1,33 @@
import { run, html, css, defaults } from '../util/run'
import { env } from '../../src/lib/sharedState'
import { crosscheck, run, html, css, defaults } from '../util/run'
it('should add the divide styles for divide-y and a default border color', () => {
let config = {
content: [{ raw: html`<div class="divide-y"></div>` }],
corePlugins: { preflight: false },
}
crosscheck(({ stable, oxide }) => {
it('should add the divide styles for divide-y and a default border color', () => {
let config = {
content: [{ raw: html`<div class="divide-y"></div>` }],
corePlugins: { preflight: false },
}
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
${defaults}
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
`)
})
})
it('should add the divide styles for divide-x and a default border color', () => {
let config = {
content: [{ raw: html`<div class="divide-x"></div>` }],
corePlugins: { preflight: false },
}
let expected = env.OXIDE
? css`
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
${defaults}
.divide-x > :not([hidden]) ~ :not([hidden]) {
--tw-divide-x-reverse: 0;
border-inline-end-width: calc(1px * var(--tw-divide-x-reverse));
border-inline-start-width: calc(1px * calc(1 - var(--tw-divide-x-reverse)));
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
`
: css`
`)
})
})
it('should add the divide styles for divide-x and a default border color', () => {
let config = {
content: [{ raw: html`<div class="divide-x"></div>` }],
corePlugins: { preflight: false },
}
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
stable.expect(result.css).toMatchCss(css`
${defaults}
.divide-x > :not([hidden]) ~ :not([hidden]) {
@ -44,66 +35,74 @@ it('should add the divide styles for divide-x and a default border color', () =>
border-right-width: calc(1px * var(--tw-divide-x-reverse));
border-left-width: calc(1px * calc(1 - var(--tw-divide-x-reverse)));
}
`
`)
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(expected)
})
})
it('should add the divide styles for divide-y-reverse and a default border color', () => {
let config = {
content: [{ raw: html`<div class="divide-y-reverse"></div>` }],
corePlugins: { preflight: false },
}
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
${defaults}
.divide-y-reverse > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 1;
}
`)
})
})
it('should add the divide styles for divide-x-reverse and a default border color', () => {
let config = {
content: [{ raw: html`<div class="divide-x-reverse"></div>` }],
corePlugins: { preflight: false },
}
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
${defaults}
.divide-x-reverse > :not([hidden]) ~ :not([hidden]) {
--tw-divide-x-reverse: 1;
}
`)
})
})
it('should only inject the base styles once if we use divide and border at the same time', () => {
let config = {
content: [{ raw: html`<div class="divide-y border-r"></div>` }],
corePlugins: { preflight: false },
}
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
${defaults}
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
.border-r {
border-right-width: 1px;
}
`)
oxide.expect(result.css).toMatchCss(css`
${defaults}
.divide-x > :not([hidden]) ~ :not([hidden]) {
--tw-divide-x-reverse: 0;
border-inline-end-width: calc(1px * var(--tw-divide-x-reverse));
border-inline-start-width: calc(1px * calc(1 - var(--tw-divide-x-reverse)));
}
`)
})
})
it('should add the divide styles for divide-y-reverse and a default border color', () => {
let config = {
content: [{ raw: html`<div class="divide-y-reverse"></div>` }],
corePlugins: { preflight: false },
}
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
${defaults}
.divide-y-reverse > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 1;
}
`)
})
})
it('should add the divide styles for divide-x-reverse and a default border color', () => {
let config = {
content: [{ raw: html`<div class="divide-x-reverse"></div>` }],
corePlugins: { preflight: false },
}
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
${defaults}
.divide-x-reverse > :not([hidden]) ~ :not([hidden]) {
--tw-divide-x-reverse: 1;
}
`)
})
})
it('should only inject the base styles once if we use divide and border at the same time', () => {
let config = {
content: [{ raw: html`<div class="divide-y border-r"></div>` }],
corePlugins: { preflight: false },
}
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
${defaults}
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
.border-r {
border-right-width: 1px;
}
`)
})
})
})

View File

@ -1,98 +1,100 @@
import { run, html, css } from '../util/run'
import { crosscheck, run, html, css } from '../util/run'
test('font-family utilities can be defined as a string', () => {
let config = {
content: [{ raw: html`<div class="font-sans"></div>` }],
theme: {
fontFamily: {
sans: 'Helvetica, Arial, sans-serif',
crosscheck(() => {
test('font-family utilities can be defined as a string', () => {
let config = {
content: [{ raw: html`<div class="font-sans"></div>` }],
theme: {
fontFamily: {
sans: 'Helvetica, Arial, sans-serif',
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.font-sans {
font-family: Helvetica, Arial, sans-serif;
}
`)
})
})
test('font-family utilities can be defined as an array', () => {
let config = {
content: [{ raw: html`<div class="font-sans"></div>` }],
theme: {
fontFamily: {
sans: ['Helvetica', 'Arial', 'sans-serif'],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.font-sans {
font-family: Helvetica, Arial, sans-serif;
}
`)
})
})
test('font-family values are not automatically escaped', () => {
let config = {
content: [{ raw: html`<div class="font-sans"></div>` }],
theme: {
fontFamily: {
sans: ["'Exo 2'", 'sans-serif'],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.font-sans {
font-family: 'Exo 2', sans-serif;
}
`)
})
})
test('font-feature-settings can be provided when families are defined as a string', () => {
let config = {
content: [{ raw: html`<div class="font-sans"></div>` }],
theme: {
fontFamily: {
sans: ['Helvetica, Arial, sans-serif', { fontFeatureSettings: '"cv11", "ss01"' }],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(`
.font-sans {
font-family: Helvetica, Arial, sans-serif;
font-feature-settings: "cv11", "ss01";
}
`)
})
})
test('font-feature-settings can be provided when families are defined as an array', () => {
let config = {
content: [{ raw: html`<div class="font-sans"></div>` }],
theme: {
fontFamily: {
sans: [['Helvetica', 'Arial', 'sans-serif'], { fontFeatureSettings: '"cv11", "ss01"' }],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(`
.font-sans {
font-family: Helvetica, Arial, sans-serif;
font-feature-settings: "cv11", "ss01";
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-sans {
font-family: Helvetica, Arial, sans-serif;
}
`)
})
})
test('font-family utilities can be defined as an array', () => {
let config = {
content: [{ raw: html`<div class="font-sans"></div>` }],
theme: {
fontFamily: {
sans: ['Helvetica', 'Arial', 'sans-serif'],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-sans {
font-family: Helvetica, Arial, sans-serif;
}
`)
})
})
test('font-family values are not automatically escaped', () => {
let config = {
content: [{ raw: html`<div class="font-sans"></div>` }],
theme: {
fontFamily: {
sans: ["'Exo 2'", 'sans-serif'],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-sans {
font-family: 'Exo 2', sans-serif;
}
`)
})
})
test('font-feature-settings can be provided when families are defined as a string', () => {
let config = {
content: [{ raw: html`<div class="font-sans"></div>` }],
theme: {
fontFamily: {
sans: ['Helvetica, Arial, sans-serif', { fontFeatureSettings: '"cv11", "ss01"' }],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-sans {
font-family: Helvetica, Arial, sans-serif;
font-feature-settings: 'cv11', 'ss01';
}
`)
})
})
test('font-feature-settings can be provided when families are defined as an array', () => {
let config = {
content: [{ raw: html`<div class="font-sans"></div>` }],
theme: {
fontFamily: {
sans: [['Helvetica', 'Arial', 'sans-serif'], { fontFeatureSettings: '"cv11", "ss01"' }],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-sans {
font-family: Helvetica, Arial, sans-serif;
font-feature-settings: 'cv11', 'ss01';
}
`)
})
})
})

View File

@ -1,195 +1,197 @@
import { run, html, css } from '../util/run'
import { crosscheck, run, html, css } from '../util/run'
test('font-size utilities can include a default line-height', () => {
let config = {
content: [{ raw: html`<div class="text-md text-sm text-lg"></div>` }],
theme: {
fontSize: {
sm: '12px',
md: ['16px', '24px'],
lg: ['20px', '28px'],
crosscheck(() => {
test('font-size utilities can include a default line-height', () => {
let config = {
content: [{ raw: html`<div class="text-md text-sm text-lg"></div>` }],
theme: {
fontSize: {
sm: '12px',
md: ['16px', '24px'],
lg: ['20px', '28px'],
},
},
},
}
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.text-lg {
font-size: 20px;
line-height: 28px;
}
.text-md {
font-size: 16px;
line-height: 24px;
}
.text-sm {
font-size: 12px;
}
`)
})
})
test('font-size utilities can include a default letter-spacing', () => {
let config = {
content: [{ raw: html`<div class="text-md text-sm text-lg"></div>` }],
theme: {
fontSize: {
sm: '12px',
md: ['16px', { letterSpacing: '-0.01em' }],
lg: ['20px', { letterSpacing: '-0.02em' }],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.text-lg {
font-size: 20px;
letter-spacing: -0.02em;
}
.text-md {
font-size: 16px;
letter-spacing: -0.01em;
}
.text-sm {
font-size: 12px;
}
`)
})
})
test('font-size utilities can include a default line-height and letter-spacing', () => {
let config = {
content: [{ raw: html`<div class="text-md text-sm text-lg"></div>` }],
theme: {
fontSize: {
sm: '12px',
md: ['16px', { lineHeight: '24px', letterSpacing: '-0.01em' }],
lg: ['20px', { lineHeight: '28px', letterSpacing: '-0.02em' }],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.text-lg {
font-size: 20px;
line-height: 28px;
letter-spacing: -0.02em;
}
.text-md {
font-size: 16px;
line-height: 24px;
letter-spacing: -0.01em;
}
.text-sm {
font-size: 12px;
}
`)
})
})
test('font-size utilities can include a font-weight', () => {
let config = {
content: [{ raw: html`<div class="text-md text-sm text-lg"></div>` }],
theme: {
fontSize: {
sm: '12px',
md: ['16px', { lineHeight: '24px', fontWeight: 500 }],
lg: ['20px', { lineHeight: '28px', fontWeight: 'bold' }],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.text-lg {
font-size: 20px;
line-height: 28px;
font-weight: bold;
}
.text-md {
font-size: 16px;
line-height: 24px;
font-weight: 500;
}
.text-sm {
font-size: 12px;
}
`)
})
})
test('font-size utilities can include a line-height modifier', () => {
let config = {
content: [
{
raw: html`<div class="text-sm md:text-base">
<div class="text-sm/6 md:text-base/7"></div>
<div class="text-sm/[21px] md:text-base/[33px]"></div>
<div class="text-[13px]/6 md:text-[19px]/8"></div>
<div class="text-[17px]/[23px] md:text-[21px]/[29px]"></div>
<div class="text-sm/999 md:text-base/000"></div>
</div>`,
},
],
theme: {
fontSize: {
sm: ['12px', '20px'],
base: ['16px', '24px'],
},
lineHeight: {
6: '24px',
7: '28px',
8: '32px',
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.text-\[13px\]\/6 {
font-size: 13px;
line-height: 24px;
}
.text-\[17px\]\/\[23px\] {
font-size: 17px;
line-height: 23px;
}
.text-sm {
font-size: 12px;
line-height: 20px;
}
.text-sm\/6 {
font-size: 12px;
line-height: 24px;
}
.text-sm\/\[21px\] {
font-size: 12px;
line-height: 21px;
}
@media (min-width: 768px) {
.md\:text-\[19px\]\/8 {
font-size: 19px;
line-height: 32px;
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.text-lg {
font-size: 20px;
line-height: 28px;
}
.md\:text-\[21px\]\/\[29px\] {
font-size: 21px;
line-height: 29px;
}
.md\:text-base {
.text-md {
font-size: 16px;
line-height: 24px;
}
.md\:text-base\/7 {
.text-sm {
font-size: 12px;
}
`)
})
})
test('font-size utilities can include a default letter-spacing', () => {
let config = {
content: [{ raw: html`<div class="text-md text-sm text-lg"></div>` }],
theme: {
fontSize: {
sm: '12px',
md: ['16px', { letterSpacing: '-0.01em' }],
lg: ['20px', { letterSpacing: '-0.02em' }],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.text-lg {
font-size: 20px;
letter-spacing: -0.02em;
}
.text-md {
font-size: 16px;
letter-spacing: -0.01em;
}
.text-sm {
font-size: 12px;
}
`)
})
})
test('font-size utilities can include a default line-height and letter-spacing', () => {
let config = {
content: [{ raw: html`<div class="text-md text-sm text-lg"></div>` }],
theme: {
fontSize: {
sm: '12px',
md: ['16px', { lineHeight: '24px', letterSpacing: '-0.01em' }],
lg: ['20px', { lineHeight: '28px', letterSpacing: '-0.02em' }],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.text-lg {
font-size: 20px;
line-height: 28px;
letter-spacing: -0.02em;
}
.md\:text-base\/\[33px\] {
.text-md {
font-size: 16px;
line-height: 33px;
line-height: 24px;
letter-spacing: -0.01em;
}
}
`)
.text-sm {
font-size: 12px;
}
`)
})
})
test('font-size utilities can include a font-weight', () => {
let config = {
content: [{ raw: html`<div class="text-md text-sm text-lg"></div>` }],
theme: {
fontSize: {
sm: '12px',
md: ['16px', { lineHeight: '24px', fontWeight: 500 }],
lg: ['20px', { lineHeight: '28px', fontWeight: 'bold' }],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.text-lg {
font-size: 20px;
line-height: 28px;
font-weight: bold;
}
.text-md {
font-size: 16px;
line-height: 24px;
font-weight: 500;
}
.text-sm {
font-size: 12px;
}
`)
})
})
test('font-size utilities can include a line-height modifier', () => {
let config = {
content: [
{
raw: html`<div class="text-sm md:text-base">
<div class="text-sm/6 md:text-base/7"></div>
<div class="text-sm/[21px] md:text-base/[33px]"></div>
<div class="text-[13px]/6 md:text-[19px]/8"></div>
<div class="text-[17px]/[23px] md:text-[21px]/[29px]"></div>
<div class="text-sm/999 md:text-base/000"></div>
</div>`,
},
],
theme: {
fontSize: {
sm: ['12px', '20px'],
base: ['16px', '24px'],
},
lineHeight: {
6: '24px',
7: '28px',
8: '32px',
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.text-\[13px\]\/6 {
font-size: 13px;
line-height: 24px;
}
.text-\[17px\]\/\[23px\] {
font-size: 17px;
line-height: 23px;
}
.text-sm {
font-size: 12px;
line-height: 20px;
}
.text-sm\/6 {
font-size: 12px;
line-height: 24px;
}
.text-sm\/\[21px\] {
font-size: 12px;
line-height: 21px;
}
@media (min-width: 768px) {
.md\:text-\[19px\]\/8 {
font-size: 19px;
line-height: 32px;
}
.md\:text-\[21px\]\/\[29px\] {
font-size: 21px;
line-height: 29px;
}
.md\:text-base {
font-size: 16px;
line-height: 24px;
}
.md\:text-base\/7 {
font-size: 16px;
line-height: 28px;
}
.md\:text-base\/\[33px\] {
font-size: 16px;
line-height: 33px;
}
}
`)
})
})
})

View File

@ -1,72 +1,74 @@
import { run, html, css } from '../util/run'
import { crosscheck, run, html, css } from '../util/run'
test('opacity variables are given to colors defined as closures', () => {
let config = {
content: [
{
raw: html`<div
class="text-primary text-secondary from-primary from-secondary via-primary via-secondary to-primary to-secondary text-opacity-50"
></div>`,
},
],
theme: {
colors: {
primary: ({ opacityVariable, opacityValue }) => {
if (opacityValue !== undefined) {
return `rgba(31,31,31,${opacityValue})`
}
if (opacityVariable !== undefined) {
return `rgba(31,31,31,var(${opacityVariable},1))`
}
return `rgb(31,31,31)`
crosscheck(() => {
test('opacity variables are given to colors defined as closures', () => {
let config = {
content: [
{
raw: html`<div
class="text-primary text-secondary from-primary from-secondary via-primary via-secondary to-primary to-secondary text-opacity-50"
></div>`,
},
secondary: 'hsl(10, 50%, 50%)',
},
opacity: {
50: '0.5',
},
},
}
],
theme: {
colors: {
primary: ({ opacityVariable, opacityValue }) => {
if (opacityValue !== undefined) {
return `rgba(31,31,31,${opacityValue})`
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.from-primary {
--tw-gradient-from: rgb(31, 31, 31);
--tw-gradient-to: rgba(31, 31, 31, 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.from-secondary {
--tw-gradient-from: hsl(10, 50%, 50%);
--tw-gradient-to: hsl(10 50% 50% / 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.via-primary {
--tw-gradient-to: rgba(31, 31, 31, 0);
--tw-gradient-stops: var(--tw-gradient-from), rgb(31, 31, 31), var(--tw-gradient-to);
}
.via-secondary {
--tw-gradient-to: hsl(10 50% 50% / 0);
--tw-gradient-stops: var(--tw-gradient-from), hsl(10, 50%, 50%), var(--tw-gradient-to);
}
.to-primary {
--tw-gradient-to: rgb(31, 31, 31);
}
.to-secondary {
--tw-gradient-to: hsl(10, 50%, 50%);
}
.text-primary {
--tw-text-opacity: 1;
color: rgba(31, 31, 31, var(--tw-text-opacity));
}
.text-secondary {
--tw-text-opacity: 1;
color: hsl(10 50% 50% / var(--tw-text-opacity));
}
.text-opacity-50 {
--tw-text-opacity: 0.5;
}
`)
if (opacityVariable !== undefined) {
return `rgba(31,31,31,var(${opacityVariable},1))`
}
return `rgb(31,31,31)`
},
secondary: 'hsl(10, 50%, 50%)',
},
opacity: {
50: '0.5',
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.from-primary {
--tw-gradient-from: rgb(31, 31, 31);
--tw-gradient-to: rgba(31, 31, 31, 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.from-secondary {
--tw-gradient-from: hsl(10, 50%, 50%);
--tw-gradient-to: hsl(10 50% 50% / 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.via-primary {
--tw-gradient-to: rgba(31, 31, 31, 0);
--tw-gradient-stops: var(--tw-gradient-from), rgb(31, 31, 31), var(--tw-gradient-to);
}
.via-secondary {
--tw-gradient-to: hsl(10 50% 50% / 0);
--tw-gradient-stops: var(--tw-gradient-from), hsl(10, 50%, 50%), var(--tw-gradient-to);
}
.to-primary {
--tw-gradient-to: rgb(31, 31, 31);
}
.to-secondary {
--tw-gradient-to: hsl(10, 50%, 50%);
}
.text-primary {
--tw-text-opacity: 1;
color: rgba(31, 31, 31, var(--tw-text-opacity));
}
.text-secondary {
--tw-text-opacity: 1;
color: hsl(10 50% 50% / var(--tw-text-opacity));
}
.text-opacity-50 {
--tw-text-opacity: 0.5;
}
`)
})
})
})

View File

@ -1,74 +1,78 @@
import { run, html, css, defaults } from './util/run'
import { crosscheck, run, html, css, defaults } from './util/run'
it('should be possible to use contrast-more and contrast-less variants', () => {
let config = {
content: [
{ raw: html`<div class="bg-white contrast-more:bg-pink-500 contrast-less:bg-black"></div>` },
],
corePlugins: { preflight: false },
}
crosscheck(() => {
it('should be possible to use contrast-more and contrast-less variants', () => {
let config = {
content: [
{
raw: html`<div class="bg-white contrast-more:bg-pink-500 contrast-less:bg-black"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
@media (prefers-contrast: more) {
.contrast-more\:bg-pink-500 {
--tw-bg-opacity: 1;
background-color: rgb(236 72 153 / var(--tw-bg-opacity));
}
}
@media (prefers-contrast: less) {
.contrast-less\:bg-black {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
}
`)
})
})
it('dark mode should appear after the contrast variants', () => {
let config = {
content: [{ raw: html`<div class="contrast-more:bg-black dark:bg-white"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-contrast: more) {
.contrast-more\:bg-black {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
}
@media (prefers-color-scheme: dark) {
.dark\:bg-white {
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
}
`)
@media (prefers-contrast: more) {
.contrast-more\:bg-pink-500 {
--tw-bg-opacity: 1;
background-color: rgb(236 72 153 / var(--tw-bg-opacity));
}
}
@media (prefers-contrast: less) {
.contrast-less\:bg-black {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
}
`)
})
})
it('dark mode should appear after the contrast variants', () => {
let config = {
content: [{ raw: html`<div class="contrast-more:bg-black dark:bg-white"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-contrast: more) {
.contrast-more\:bg-black {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
}
@media (prefers-color-scheme: dark) {
.dark\:bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
}
`)
})
})
})

View File

@ -1,10 +1,8 @@
import { run, html, css, defaults } from './util/run'
import { env } from '../src/lib/sharedState'
import { crosscheck, run, html, css, defaults } from './util/run'
let group = env.OXIDE ? describe.skip : describe
group('prefix', () => {
test('prefix', () => {
crosscheck(({ stable, oxide }) => {
oxide.test.todo('prefix')
stable.test('prefix', () => {
let config = {
prefix: 'tw-',
darkMode: 'class',
@ -191,7 +189,8 @@ group('prefix', () => {
})
})
it('negative values: marker before prefix', async () => {
oxide.test.todo('negative values: marker before prefix')
stable.test('negative values: marker before prefix', async () => {
let config = {
prefix: 'tw-',
content: [{ raw: html`<div class="-tw-top-1"></div>` }],
@ -213,7 +212,8 @@ group('prefix', () => {
`)
})
it('negative values: marker after prefix', async () => {
oxide.test.todo('negative values: marker after prefix')
stable.test('negative values: marker after prefix', async () => {
let config = {
prefix: 'tw-',
content: [{ raw: html`<div class="tw--top-1"></div>` }],
@ -235,7 +235,8 @@ group('prefix', () => {
`)
})
it('negative values: marker before prefix and arbitrary value', async () => {
oxide.test.todo('negative values: marker before prefix and arbitrary value')
stable.test('negative values: marker before prefix and arbitrary value', async () => {
let config = {
prefix: 'tw-',
content: [{ raw: html`<div class="-tw-top-[1px]"></div>` }],
@ -257,7 +258,8 @@ group('prefix', () => {
`)
})
it('negative values: marker after prefix and arbitrary value', async () => {
oxide.test.todo('negative values: marker after prefix and arbitrary value')
stable.test('negative values: marker after prefix and arbitrary value', async () => {
let config = {
prefix: 'tw-',
content: [{ raw: html`<div class="tw--top-[1px]"></div>` }],
@ -279,7 +281,8 @@ group('prefix', () => {
`)
})
it('negative values: no marker and arbitrary value', async () => {
oxide.test.todo('negative values: no marker and arbitrary value')
stable.test('negative values: no marker and arbitrary value', async () => {
let config = {
prefix: 'tw-',
content: [{ raw: html`<div class="tw-top-[-1px]"></div>` }],
@ -301,7 +304,8 @@ group('prefix', () => {
`)
})
it('negative values: variant versions', async () => {
oxide.test.todo('negative values: variant versions')
stable.test('negative values: variant versions', async () => {
let config = {
prefix: 'tw-',
content: [
@ -346,7 +350,8 @@ group('prefix', () => {
`)
})
it('negative values: prefix and apply', async () => {
oxide.test.todo('negative values: prefix and apply')
stable.test('negative values: prefix and apply', async () => {
let config = {
prefix: 'tw-',
content: [{ raw: html`` }],
@ -396,7 +401,8 @@ group('prefix', () => {
`)
})
it('negative values: prefix in the safelist', async () => {
oxide.test.todo('negative values: prefix in the safelist')
stable.test('negative values: prefix in the safelist', async () => {
let config = {
prefix: 'tw-',
safelist: [{ pattern: /-tw-top-1/g }, { pattern: /tw--top-1/g }],
@ -427,7 +433,8 @@ group('prefix', () => {
`)
})
it('prefix with negative values and variants in the safelist', async () => {
oxide.test.todo('prefix with negative values and variants in the safelist')
stable.test('prefix with negative values and variants in the safelist', async () => {
let config = {
prefix: 'tw-',
safelist: [
@ -476,7 +483,8 @@ group('prefix', () => {
`)
})
it('prefix does not detect and generate unnecessary classes', async () => {
oxide.test.todo('prefix does not detect and generate unnecessary classes')
stable.test('prefix does not detect and generate unnecessary classes', async () => {
let config = {
prefix: 'tw-_',
content: [{ raw: html`-aaa-filter aaaa-table aaaa-hidden` }],
@ -492,7 +500,8 @@ group('prefix', () => {
expect(result.css).toMatchFormattedCss(css``)
})
it('supports prefixed utilities using arbitrary values', async () => {
oxide.test.todo('supports prefixed utilities using arbitrary values')
stable.test('supports prefixed utilities using arbitrary values', async () => {
let config = {
prefix: 'tw-',
content: [{ raw: html`foo` }],
@ -518,7 +527,8 @@ group('prefix', () => {
`)
})
it('supports non-word prefixes (1)', async () => {
oxide.test.todo('supports non-word prefixes (1)')
stable.test('supports non-word prefixes (1)', async () => {
let config = {
prefix: '@',
content: [
@ -577,7 +587,8 @@ group('prefix', () => {
`)
})
it('supports non-word prefixes (2)', async () => {
oxide.test.todo('supports non-word prefixes (2)')
stable.test('supports non-word prefixes (2)', async () => {
let config = {
prefix: '@]$',
content: [

View File

@ -1,17 +1,20 @@
import prefix from '../src/util/prefixSelector'
import { crosscheck } from './util/run'
test('it prefixes classes with the provided prefix', () => {
expect(prefix('tw-', '.foo')).toEqual('.tw-foo')
})
crosscheck(() => {
test('it prefixes classes with the provided prefix', () => {
expect(prefix('tw-', '.foo')).toEqual('.tw-foo')
})
test('it properly prefixes selectors with non-standard characters', () => {
expect(prefix('tw-', '.hello\\:world')).toEqual('.tw-hello\\:world')
expect(prefix('tw-', '.foo\\/bar')).toEqual('.tw-foo\\/bar')
expect(prefix('tw-', '.wew\\.lad')).toEqual('.tw-wew\\.lad')
})
test('it properly prefixes selectors with non-standard characters', () => {
expect(prefix('tw-', '.hello\\:world')).toEqual('.tw-hello\\:world')
expect(prefix('tw-', '.foo\\/bar')).toEqual('.tw-foo\\/bar')
expect(prefix('tw-', '.wew\\.lad')).toEqual('.tw-wew\\.lad')
})
test('it prefixes all classes in a selector', () => {
expect(prefix('tw-', '.btn-blue .w-1\\/4 > h1.text-xl + a .bar')).toEqual(
'.tw-btn-blue .tw-w-1\\/4 > h1.tw-text-xl + a .tw-bar'
)
test('it prefixes all classes in a selector', () => {
expect(prefix('tw-', '.btn-blue .w-1\\/4 > h1.text-xl + a .bar')).toEqual(
'.tw-btn-blue .tw-w-1\\/4 > h1.tw-text-xl + a .tw-bar'
)
})
})

View File

@ -1,21 +1,23 @@
import { run, html, css } from './util/run'
import { crosscheck, run, html, css } from './util/run'
it('preflight has a correct border color fallback', () => {
let config = {
content: [{ raw: html`<div class="border-black"></div>` }],
theme: {
borderColor: ({ theme }) => theme('colors'),
},
plugins: [],
corePlugins: { preflight: true },
}
crosscheck(() => {
it('preflight has a correct border color fallback', () => {
let config = {
content: [{ raw: html`<div class="border-black"></div>` }],
theme: {
borderColor: ({ theme }) => theme('colors'),
},
plugins: [],
corePlugins: { preflight: true },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toContain(`border-color: currentColor;`)
return run(input, config).then((result) => {
expect(result.css).toContain(`border-color: currentColor;`)
})
})
})

View File

@ -1,167 +1,171 @@
import fs from 'fs'
import path from 'path'
import { crosscheck, run, html, css } from './util/run'
import { run, html, css } from './util/run'
import { env } from '../src/lib/sharedState'
crosscheck(({ stable, oxide }) => {
it('raw content', () => {
let config = {
content: [
{
raw: html`
<div class="sr-only"></div>
<div class="content-center"></div>
<div class="items-start"></div>
<div class="self-end"></div>
<div class="animate-none"></div>
<div class="animate-spin"></div>
<div class="appearance-none"></div>
<div class="bg-local"></div>
<div class="bg-clip-border"></div>
<div class="bg-green-500"></div>
<div class="bg-gradient-to-r"></div>
<div class="bg-opacity-20"></div>
<div class="bg-top"></div>
<div class="bg-no-repeat"></div>
<div class="bg-cover"></div>
<div class="bg-origin-border bg-origin-padding bg-origin-content"></div>
<div class="border-collapse"></div>
<div class="border-black"></div>
<div class="border-opacity-10"></div>
<div class="rounded-md"></div>
<div class="border-solid"></div>
<div class="border"></div>
<div class="border-2"></div>
<div class="shadow"></div>
<div class="shadow-md"></div>
<div class="shadow-lg"></div>
<div class="decoration-clone decoration-slice"></div>
<div class="box-border"></div>
<div class="clear-left"></div>
<div class="container"></div>
<div class="cursor-pointer"></div>
<div class="hidden inline-grid"></div>
<div class="divide-gray-200"></div>
<div class="divide-opacity-50"></div>
<div class="divide-dotted"></div>
<div class="divide-x-2 divide-y-4 divide-x-0 divide-y-0"></div>
<div class="fill-current"></div>
<div class="flex-1"></div>
<div class="flex-row-reverse"></div>
<div class="flex-grow"></div>
<div class="flex-grow-0"></div>
<div class="flex-shrink"></div>
<div class="flex-shrink-0"></div>
<div class="flex-wrap"></div>
<div class="float-right"></div>
<div class="font-sans"></div>
<div class="text-2xl"></div>
<div class="antialiased"></div>
<div class="not-italic"></div>
<div class="tabular-nums ordinal diagonal-fractions"></div>
<div class="font-medium"></div>
<div class="gap-x-2 gap-y-3 gap-4"></div>
<div class="from-red-300 via-purple-200 to-blue-400"></div>
<div class="auto-cols-min"></div>
<div class="grid-flow-row"></div>
<div class="auto-rows-max"></div>
<div class="col-span-3"></div>
<div class="col-start-1"></div>
<div class="col-end-4"></div>
<div class="row-span-2"></div>
<div class="row-start-3"></div>
<div class="row-end-5"></div>
<div class="grid-cols-4"></div>
<div class="grid-rows-3"></div>
<div class="h-16"></div>
<div class="inset-0 inset-y-4 inset-x-2 top-6 right-8 bottom-12 left-16"></div>
<div class="isolate isolation-auto"></div>
<div class="justify-center"></div>
<div class="justify-items-end"></div>
<div class="justify-self-start"></div>
<div class="tracking-tight"></div>
<div class="leading-relaxed leading-5"></div>
<div class="list-inside"></div>
<div class="list-disc"></div>
<div class="m-4 my-2 mx-auto mt-0 mr-1 mb-3 ml-4"></div>
<div class="max-h-screen"></div>
<div class="max-w-full"></div>
<div class="min-h-0"></div>
<div class="min-w-min"></div>
<div class="object-cover"></div>
<div class="object-bottom"></div>
<div class="opacity-90"></div>
<div class="bg-blend-darken bg-blend-difference"></div>
<div class="mix-blend-multiply mix-blend-saturation"></div>
<div class="order-last order-2"></div>
<div class="overflow-hidden"></div>
<div class="overscroll-contain"></div>
<div class="scroll-smooth"></div>
<div class="p-4 py-2 px-3 pt-1 pr-2 pb-3 pl-4"></div>
<div class="place-content-start"></div>
<div class="placeholder-green-300"></div>
<div class="placeholder-opacity-60"></div>
<div class="place-items-end"></div>
<div class="place-self-center"></div>
<div class="pointer-events-none"></div>
<div class="absolute"></div>
<div class="resize-none"></div>
<div class="ring-white"></div>
<div class="ring-offset-blue-300"></div>
<div class="ring-offset-2"></div>
<div class="ring-opacity-40"></div>
<div class="ring ring-4"></div>
<div
class="filter filter-none blur-md brightness-150 contrast-50 drop-shadow-md grayscale hue-rotate-60 invert saturate-200 sepia"
></div>
<div
class="backdrop-filter backdrop-filter-none backdrop-blur-lg backdrop-brightness-50 backdrop-contrast-0 backdrop-grayscale backdrop-hue-rotate-90 backdrop-invert backdrop-opacity-75 backdrop-saturate-150 backdrop-sepia"
></div>
<div class="rotate-3"></div>
<div class="scale-95"></div>
<div class="skew-y-12 skew-x-12"></div>
<div class="space-x-4 space-y-3 space-x-reverse space-y-reverse"></div>
<div class="stroke-current"></div>
<div class="stroke-2"></div>
<div class="table-fixed"></div>
<div class="text-center"></div>
<div class="text-indigo-500"></div>
<div class="underline"></div>
<div class="text-opacity-10"></div>
<div class="overflow-ellipsis truncate"></div>
<div class="uppercase"></div>
<div class="transform transform-gpu"></div>
<div class="origin-top-right"></div>
<div class="delay-300"></div>
<div class="duration-200"></div>
<div class="transition transition-all"></div>
<div class="ease-in-out"></div>
<div class="translate-x-5 -translate-x-4 translate-y-6 -translate-x-3"></div>
<div class="select-none"></div>
<div class="align-middle"></div>
<div class="invisible"></div>
<div class="collapse"></div>
<div class="whitespace-nowrap"></div>
<div class="w-12"></div>
<div class="break-words"></div>
<div class="z-30"></div>
`,
},
],
corePlugins: { preflight: false },
}
it('raw content', () => {
let config = {
content: [
{
raw: html`
<div class="sr-only"></div>
<div class="content-center"></div>
<div class="items-start"></div>
<div class="self-end"></div>
<div class="animate-none"></div>
<div class="animate-spin"></div>
<div class="appearance-none"></div>
<div class="bg-local"></div>
<div class="bg-clip-border"></div>
<div class="bg-green-500"></div>
<div class="bg-gradient-to-r"></div>
<div class="bg-opacity-20"></div>
<div class="bg-top"></div>
<div class="bg-no-repeat"></div>
<div class="bg-cover"></div>
<div class="bg-origin-border bg-origin-padding bg-origin-content"></div>
<div class="border-collapse"></div>
<div class="border-black"></div>
<div class="border-opacity-10"></div>
<div class="rounded-md"></div>
<div class="border-solid"></div>
<div class="border"></div>
<div class="border-2"></div>
<div class="shadow"></div>
<div class="shadow-md"></div>
<div class="shadow-lg"></div>
<div class="decoration-clone decoration-slice"></div>
<div class="box-border"></div>
<div class="clear-left"></div>
<div class="container"></div>
<div class="cursor-pointer"></div>
<div class="hidden inline-grid"></div>
<div class="divide-gray-200"></div>
<div class="divide-opacity-50"></div>
<div class="divide-dotted"></div>
<div class="divide-x-2 divide-y-4 divide-x-0 divide-y-0"></div>
<div class="fill-current"></div>
<div class="flex-1"></div>
<div class="flex-row-reverse"></div>
<div class="flex-grow"></div>
<div class="flex-grow-0"></div>
<div class="flex-shrink"></div>
<div class="flex-shrink-0"></div>
<div class="flex-wrap"></div>
<div class="float-right"></div>
<div class="font-sans"></div>
<div class="text-2xl"></div>
<div class="antialiased"></div>
<div class="not-italic"></div>
<div class="tabular-nums ordinal diagonal-fractions"></div>
<div class="font-medium"></div>
<div class="gap-x-2 gap-y-3 gap-4"></div>
<div class="from-red-300 via-purple-200 to-blue-400"></div>
<div class="auto-cols-min"></div>
<div class="grid-flow-row"></div>
<div class="auto-rows-max"></div>
<div class="col-span-3"></div>
<div class="col-start-1"></div>
<div class="col-end-4"></div>
<div class="row-span-2"></div>
<div class="row-start-3"></div>
<div class="row-end-5"></div>
<div class="grid-cols-4"></div>
<div class="grid-rows-3"></div>
<div class="h-16"></div>
<div class="inset-0 inset-y-4 inset-x-2 top-6 right-8 bottom-12 left-16"></div>
<div class="isolate isolation-auto"></div>
<div class="justify-center"></div>
<div class="justify-items-end"></div>
<div class="justify-self-start"></div>
<div class="tracking-tight"></div>
<div class="leading-relaxed leading-5"></div>
<div class="list-inside"></div>
<div class="list-disc"></div>
<div class="m-4 my-2 mx-auto mt-0 mr-1 mb-3 ml-4"></div>
<div class="max-h-screen"></div>
<div class="max-w-full"></div>
<div class="min-h-0"></div>
<div class="min-w-min"></div>
<div class="object-cover"></div>
<div class="object-bottom"></div>
<div class="opacity-90"></div>
<div class="bg-blend-darken bg-blend-difference"></div>
<div class="mix-blend-multiply mix-blend-saturation"></div>
<div class="order-last order-2"></div>
<div class="overflow-hidden"></div>
<div class="overscroll-contain"></div>
<div class="scroll-smooth"></div>
<div class="p-4 py-2 px-3 pt-1 pr-2 pb-3 pl-4"></div>
<div class="place-content-start"></div>
<div class="placeholder-green-300"></div>
<div class="placeholder-opacity-60"></div>
<div class="place-items-end"></div>
<div class="place-self-center"></div>
<div class="pointer-events-none"></div>
<div class="absolute"></div>
<div class="resize-none"></div>
<div class="ring-white"></div>
<div class="ring-offset-blue-300"></div>
<div class="ring-offset-2"></div>
<div class="ring-opacity-40"></div>
<div class="ring ring-4"></div>
<div
class="filter filter-none blur-md brightness-150 contrast-50 drop-shadow-md grayscale hue-rotate-60 invert saturate-200 sepia"
></div>
<div
class="backdrop-filter backdrop-filter-none backdrop-blur-lg backdrop-brightness-50 backdrop-contrast-0 backdrop-grayscale backdrop-hue-rotate-90 backdrop-invert backdrop-opacity-75 backdrop-saturate-150 backdrop-sepia"
></div>
<div class="rotate-3"></div>
<div class="scale-95"></div>
<div class="skew-y-12 skew-x-12"></div>
<div class="space-x-4 space-y-3 space-x-reverse space-y-reverse"></div>
<div class="stroke-current"></div>
<div class="stroke-2"></div>
<div class="table-fixed"></div>
<div class="text-center"></div>
<div class="text-indigo-500"></div>
<div class="underline"></div>
<div class="text-opacity-10"></div>
<div class="overflow-ellipsis truncate"></div>
<div class="uppercase"></div>
<div class="transform transform-gpu"></div>
<div class="origin-top-right"></div>
<div class="delay-300"></div>
<div class="duration-200"></div>
<div class="transition transition-all"></div>
<div class="ease-in-out"></div>
<div class="translate-x-5 -translate-x-4 translate-y-6 -translate-x-3"></div>
<div class="select-none"></div>
<div class="align-middle"></div>
<div class="invisible"></div>
<div class="collapse"></div>
<div class="whitespace-nowrap"></div>
<div class="w-12"></div>
<div class="break-words"></div>
<div class="z-30"></div>
`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
let expectedPath = env.OXIDE
? path.resolve(__dirname, './raw-content.oxide.test.css')
: path.resolve(__dirname, './raw-content.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
return run(input, config).then((result) => {
stable
.expect(result.css)
.toMatchFormattedCss(
fs.readFileSync(path.resolve(__dirname, './raw-content.test.css'), 'utf8')
)
oxide
.expect(result.css)
.toMatchFormattedCss(
fs.readFileSync(path.resolve(__dirname, './raw-content.oxide.test.css'), 'utf8')
)
})
})
})

View File

@ -1,22 +1,24 @@
import { run, css } from './util/run'
import { crosscheck, run, css } from './util/run'
test('relative purge paths', () => {
let config = {
content: ['./tests/relative-purge-paths.test.html'],
corePlugins: { preflight: false },
}
crosscheck(() => {
test('relative purge paths', () => {
let config = {
content: ['./tests/relative-purge-paths.test.html'],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toIncludeCss(css`
.font-bold {
font-weight: 700;
}
`)
return run(input, config).then((result) => {
expect(result.css).toIncludeCss(css`
.font-bold {
font-weight: 700;
}
`)
})
})
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,155 +1,157 @@
import { run, html, css } from './util/run'
import { crosscheck, run, html, css } from './util/run'
test('responsive and variants atrules', () => {
let config = {
content: [
{
raw: html`
<div class="responsive-in-utilities"></div>
<div class="variants-in-utilities"></div>
<div class="both-in-utilities"></div>
<div class="responsive-at-root"></div>
<div class="variants-at-root"></div>
<div class="both-at-root"></div>
<div class="responsive-in-components"></div>
<div class="variants-in-components"></div>
<div class="both-in-components"></div>
<div class="md:focus:responsive-in-utilities"></div>
<div class="md:focus:variants-in-utilities"></div>
<div class="md:focus:both-in-utilities"></div>
<div class="md:focus:responsive-at-root"></div>
<div class="md:focus:variants-at-root"></div>
<div class="md:focus:both-at-root"></div>
<div class="md:focus:responsive-in-components"></div>
<div class="md:focus:variants-in-components"></div>
<div class="md:focus:both-in-components"></div>
`,
},
],
corePlugins: { preflight: false },
}
crosscheck(() => {
test('responsive and variants atrules', () => {
let config = {
content: [
{
raw: html`
<div class="responsive-in-utilities"></div>
<div class="variants-in-utilities"></div>
<div class="both-in-utilities"></div>
<div class="responsive-at-root"></div>
<div class="variants-at-root"></div>
<div class="both-at-root"></div>
<div class="responsive-in-components"></div>
<div class="variants-in-components"></div>
<div class="both-in-components"></div>
<div class="md:focus:responsive-in-utilities"></div>
<div class="md:focus:variants-in-utilities"></div>
<div class="md:focus:both-in-utilities"></div>
<div class="md:focus:responsive-at-root"></div>
<div class="md:focus:variants-at-root"></div>
<div class="md:focus:both-at-root"></div>
<div class="md:focus:responsive-in-components"></div>
<div class="md:focus:variants-in-components"></div>
<div class="md:focus:both-in-components"></div>
`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind components;
@tailwind utilities;
let input = css`
@tailwind components;
@tailwind utilities;
@layer utilities {
@responsive {
.responsive-in-utilities {
color: blue;
}
}
@variants {
.variants-in-utilities {
color: red;
}
}
@responsive {
@variants {
.both-in-utilities {
color: green;
}
}
}
}
@layer utilities {
@responsive {
.responsive-in-utilities {
color: blue;
.responsive-at-root {
color: white;
}
}
@variants {
.variants-in-utilities {
color: red;
.variants-at-root {
color: orange;
}
}
@responsive {
@variants {
.both-in-utilities {
color: green;
.both-at-root {
color: pink;
}
}
}
}
@responsive {
.responsive-at-root {
color: white;
}
}
@variants {
.variants-at-root {
color: orange;
}
}
@responsive {
@variants {
.both-at-root {
color: pink;
@layer components {
@responsive {
.responsive-in-components {
color: blue;
}
}
@variants {
.variants-in-components {
color: red;
}
}
@responsive {
@variants {
.both-in-components {
color: green;
}
}
}
}
}
`
@layer components {
@responsive {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.responsive-in-components {
color: blue;
}
}
@variants {
.variants-in-components {
color: red;
}
}
@responsive {
@variants {
.both-in-components {
color: green;
}
}
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.responsive-in-components {
color: blue;
}
.variants-in-components {
color: red;
}
.both-in-components {
color: green;
}
.responsive-in-utilities {
color: blue;
}
.variants-in-utilities {
color: red;
}
.both-in-utilities {
color: green;
}
.responsive-at-root {
color: white;
}
.variants-at-root {
color: orange;
}
.both-at-root {
color: pink;
}
@media (min-width: 768px) {
.md\:focus\:responsive-in-components:focus {
color: blue;
}
.md\:focus\:variants-in-components:focus {
color: red;
}
.md\:focus\:both-in-components:focus {
.both-in-components {
color: green;
}
.md\:focus\:responsive-in-utilities:focus {
.responsive-in-utilities {
color: blue;
}
.md\:focus\:variants-in-utilities:focus {
.variants-in-utilities {
color: red;
}
.md\:focus\:both-in-utilities:focus {
.both-in-utilities {
color: green;
}
.md\:focus\:responsive-at-root:focus {
.responsive-at-root {
color: white;
}
.md\:focus\:variants-at-root:focus {
.variants-at-root {
color: orange;
}
.md\:focus\:both-at-root:focus {
.both-at-root {
color: pink;
}
}
`)
@media (min-width: 768px) {
.md\:focus\:responsive-in-components:focus {
color: blue;
}
.md\:focus\:variants-in-components:focus {
color: red;
}
.md\:focus\:both-in-components:focus {
color: green;
}
.md\:focus\:responsive-in-utilities:focus {
color: blue;
}
.md\:focus\:variants-in-utilities:focus {
color: red;
}
.md\:focus\:both-in-utilities:focus {
color: green;
}
.md\:focus\:responsive-at-root:focus {
color: white;
}
.md\:focus\:variants-at-root:focus {
color: orange;
}
.md\:focus\:both-at-root:focus {
color: pink;
}
}
`)
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,15 @@
import postcss from 'postcss'
import plugin from '../src/lib/substituteScreenAtRules'
import config from '../stubs/defaultConfig.stub.js'
import { crosscheck } from './util/run'
function run(input, opts = config) {
return postcss([plugin({ tailwindConfig: opts })]).process(input, { from: undefined })
}
test('it can generate media queries from configured screen sizes', () => {
const input = `
crosscheck(() => {
test('it can generate media queries from configured screen sizes', () => {
const input = `
@screen sm {
.banana { color: yellow; }
}
@ -19,7 +21,7 @@ test('it can generate media queries from configured screen sizes', () => {
}
`
const output = `
const output = `
@media (min-width: 500px) {
.banana { color: yellow; }
}
@ -31,17 +33,18 @@ test('it can generate media queries from configured screen sizes', () => {
}
`
return run(input, {
theme: {
screens: {
sm: '500px',
md: '750px',
lg: '1000px',
return run(input, {
theme: {
screens: {
sm: '500px',
md: '750px',
lg: '1000px',
},
},
},
separator: ':',
}).then((result) => {
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
separator: ':',
}).then((result) => {
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
})
})
})

View File

@ -1,20 +1,23 @@
import { resolveDebug } from '../src/lib/sharedState'
import { crosscheck } from './util/run'
it.each`
value | expected
${'true'} | ${true}
${'1'} | ${true}
${'false'} | ${false}
${'0'} | ${false}
${'*'} | ${true}
${'tailwindcss'} | ${true}
${'tailwindcss:*'} | ${true}
${'other,tailwindcss'} | ${true}
${'other,tailwindcss:*'} | ${true}
${'other,-tailwindcss'} | ${false}
${'other,-tailwindcss:*'} | ${false}
${'-tailwindcss'} | ${false}
${'-tailwindcss:*'} | ${false}
`('should resolve the debug ($value) flag correctly ($expected)', ({ value, expected }) => {
expect(resolveDebug(value)).toBe(expected)
crosscheck(() => {
it.each`
value | expected
${'true'} | ${true}
${'1'} | ${true}
${'false'} | ${false}
${'0'} | ${false}
${'*'} | ${true}
${'tailwindcss'} | ${true}
${'tailwindcss:*'} | ${true}
${'other,tailwindcss'} | ${true}
${'other,tailwindcss:*'} | ${true}
${'other,-tailwindcss'} | ${false}
${'other,-tailwindcss:*'} | ${false}
${'-tailwindcss'} | ${false}
${'-tailwindcss:*'} | ${false}
`('should resolve the debug ($value) flag correctly ($expected)', ({ value, expected }) => {
expect(resolveDebug(value)).toBe(expected)
})
})

View File

@ -1,149 +1,517 @@
import postcss from 'postcss'
import { runWithSourceMaps as run, html, css, map } from './util/run'
import { parseSourceMaps } from './util/source-maps'
import { crosscheck, runWithSourceMaps as run, html, css, map } from './util/run'
it('apply generates source maps', async () => {
let config = {
content: [
{
raw: html`
<div class="with-declaration"></div>
<div class="with-comment"></div>
<div class="just-apply"></div>
`,
},
],
corePlugins: { preflight: false },
}
let input = css`
.with-declaration {
background-color: red;
@apply h-4 w-4 bg-green-500;
crosscheck(() => {
it('apply generates source maps', async () => {
let config = {
content: [
{
raw: html`
<div class="with-declaration"></div>
<div class="with-comment"></div>
<div class="just-apply"></div>
`,
},
],
corePlugins: { preflight: false },
}
.with-comment {
/* sourcemap will work here too */
@apply h-4 w-4 bg-red-500;
}
.just-apply {
@apply h-4 w-4 bg-black;
}
`
let result = await run(input, config)
let { sources, annotations } = parseSourceMaps(result)
// All CSS generated by Tailwind CSS should be annotated with source maps
// And always be able to point to the original source file
expect(sources).not.toContain('<no source>')
expect(sources.length).toBe(1)
expect(annotations).toMatchSnapshot()
})
it('preflight + base have source maps', async () => {
let config = {
content: [],
}
let input = css`
@tailwind base;
`
let result = await run(input, config)
let { sources, annotations } = parseSourceMaps(result)
// All CSS generated by Tailwind CSS should be annotated with source maps
// And always be able to point to the original source file
expect(sources).not.toContain('<no source>')
expect(sources.length).toBe(1)
expect(annotations).toMatchSnapshot()
})
it('utilities have source maps', async () => {
let config = {
content: [{ raw: `text-red-500` }],
}
let input = css`
@tailwind utilities;
`
let result = await run(input, config)
let { sources, annotations } = parseSourceMaps(result)
// All CSS generated by Tailwind CSS should be annotated with source maps
// And always be able to point to the original source file
expect(sources).not.toContain('<no source>')
expect(sources.length).toBe(1)
expect(annotations).toStrictEqual(['2:4 -> 1:0', '2:4-23 -> 2:4-24', '2:4 -> 3:4', '2:23 -> 4:0'])
})
it('components have source maps', async () => {
let config = {
content: [{ raw: `container` }],
}
let input = css`
@tailwind components;
`
let result = await run(input, config)
let { sources, annotations } = parseSourceMaps(result)
// All CSS generated by Tailwind CSS should be annotated with source maps
// And always be able to point to the original source file
expect(sources).not.toContain('<no source>')
expect(sources.length).toBe(1)
expect(annotations).toMatchSnapshot()
})
it('source maps for layer rules are not rewritten to point to @tailwind directives', async () => {
let config = {
content: [{ raw: `font-normal foo hover:foo` }],
}
let utilitiesFile = postcss.parse(
css`
@tailwind utilities;
`,
{ from: 'components.css', map: { prev: map } }
)
let mainCssFile = postcss.parse(
css`
@layer utilities {
.foo {
background-color: red;
}
let input = css`
.with-declaration {
background-color: red;
@apply h-4 w-4 bg-green-500;
}
`,
{ from: 'input.css', map: { prev: map } }
)
// Just pretend that there's an @import in `mainCssFile` that imports the nodes from `utilitiesFile`
let input = postcss.root({
nodes: [...utilitiesFile.nodes, ...mainCssFile.nodes],
source: mainCssFile.source,
.with-comment {
/* sourcemap will work here too */
@apply h-4 w-4 bg-red-500;
}
.just-apply {
@apply h-4 w-4 bg-black;
}
`
let result = await run(input, config)
let { sources, annotations } = parseSourceMaps(result)
// All CSS generated by Tailwind CSS should be annotated with source maps
// And always be able to point to the original source file
expect(sources).not.toContain('<no source>')
expect(sources.length).toBe(1)
expect(annotations).toEqual([
'2:6 -> 2:6',
'3:8-29 -> 3:8-29',
'4:8-35 -> 4:8-20',
'4:8-35 -> 5:8-19',
'4:8-35 -> 6:8-26',
'4:8-35 -> 7:8-63',
'5:6 -> 8:6',
'7:6 -> 10:6',
'8:8-41 -> 11:8-41',
'9:8-33 -> 12:8-20',
'9:8-33 -> 13:8-19',
'9:8-33 -> 14:8-26',
'9:8-33 -> 15:8-63',
'10:6 -> 16:6',
'13:8 -> 18:6',
'13:8-31 -> 19:8-20',
'13:8-31 -> 20:8-19',
'13:8-31 -> 21:8-26',
'13:8 -> 22:8',
'13:31 -> 23:0',
])
})
let result = await run(input, config)
it('preflight + base have source maps', async () => {
let config = {
content: [],
}
let { sources, annotations } = parseSourceMaps(result)
let input = css`
@tailwind base;
`
// All CSS generated by Tailwind CSS should be annotated with source maps
// And always be able to point to the original source file
expect(sources).not.toContain('<no source>')
let result = await run(input, config)
let { sources, annotations } = parseSourceMaps(result)
// And we should see that the source map for the layer rule is not rewritten
// to point to the @tailwind directive but instead points to the original
expect(sources.length).toBe(2)
expect(sources).toEqual(['components.css', 'input.css'])
// All CSS generated by Tailwind CSS should be annotated with source maps
// And always be able to point to the original source file
expect(sources).not.toContain('<no source>')
expect(sources.length).toBe(1)
expect(annotations).toMatchSnapshot()
expect(annotations).toEqual([
'2:6 -> 1:0',
'2:20-6 -> 3:1-2',
'2:20 -> 6:1',
'2:6 -> 8:0',
'2:6-20 -> 11:2-32',
'2:6-20 -> 12:2-25',
'2:6-20 -> 13:2-29',
'2:6-20 -> 14:2-31',
'2:20 -> 15:0',
'2:6 -> 17:0',
'2:6-20 -> 19:2-18',
'2:20 -> 20:0',
'2:6 -> 22:0',
'2:20 -> 28:1',
'2:6 -> 30:0',
'2:6-20 -> 31:2-26',
'2:6-20 -> 32:2-40',
'2:6-20 -> 33:2-26',
'2:6-20 -> 34:2-21',
'2:6-20 -> 35:2-230',
'2:6-20 -> 36:2-39',
'2:20 -> 37:0',
'2:6 -> 39:0',
'2:20 -> 42:1',
'2:6 -> 44:0',
'2:6-20 -> 45:2-19',
'2:6-20 -> 46:2-30',
'2:20 -> 47:0',
'2:6 -> 49:0',
'2:20 -> 53:1',
'2:6 -> 55:0',
'2:6-20 -> 56:2-19',
'2:6-20 -> 57:2-24',
'2:6-20 -> 58:2-31',
'2:20 -> 59:0',
'2:6 -> 61:0',
'2:20 -> 63:1',
'2:6 -> 65:0',
'2:6-20 -> 66:2-35',
'2:20 -> 67:0',
'2:6 -> 69:0',
'2:20 -> 71:1',
'2:6 -> 73:0',
'2:6-20 -> 79:2-20',
'2:6-20 -> 80:2-22',
'2:20 -> 81:0',
'2:6 -> 83:0',
'2:20 -> 85:1',
'2:6 -> 87:0',
'2:6-20 -> 88:2-16',
'2:6-20 -> 89:2-26',
'2:20 -> 90:0',
'2:6 -> 92:0',
'2:20 -> 94:1',
'2:6 -> 96:0',
'2:6-20 -> 98:2-21',
'2:20 -> 99:0',
'2:6 -> 101:0',
'2:20 -> 104:1',
'2:6 -> 106:0',
'2:6-20 -> 110:2-121',
'2:6-20 -> 111:2-24',
'2:20 -> 112:0',
'2:6 -> 114:0',
'2:20 -> 116:1',
'2:6 -> 118:0',
'2:6-20 -> 119:2-16',
'2:20 -> 120:0',
'2:6 -> 122:0',
'2:20 -> 124:1',
'2:6 -> 126:0',
'2:6-20 -> 128:2-16',
'2:6-20 -> 129:2-16',
'2:6-20 -> 130:2-20',
'2:6-20 -> 131:2-26',
'2:20 -> 132:0',
'2:6 -> 134:0',
'2:6-20 -> 135:2-17',
'2:20 -> 136:0',
'2:6 -> 138:0',
'2:6-20 -> 139:2-13',
'2:20 -> 140:0',
'2:6 -> 142:0',
'2:20 -> 146:1',
'2:6 -> 148:0',
'2:6-20 -> 149:2-24',
'2:6-20 -> 150:2-31',
'2:6-20 -> 151:2-35',
'2:20 -> 152:0',
'2:6 -> 154:0',
'2:20 -> 158:1',
'2:6 -> 160:0',
'2:6-20 -> 165:2-30',
'2:6-20 -> 166:2-25',
'2:6-20 -> 167:2-30',
'2:6-20 -> 168:2-30',
'2:6-20 -> 169:2-24',
'2:6-20 -> 170:2-19',
'2:6-20 -> 171:2-20',
'2:20 -> 172:0',
'2:6 -> 174:0',
'2:20 -> 176:1',
'2:6 -> 178:0',
'2:6-20 -> 180:2-22',
'2:20 -> 181:0',
'2:6 -> 183:0',
'2:20 -> 186:1',
'2:6 -> 188:0',
'2:6-20 -> 192:2-36',
'2:6-20 -> 193:2-39',
'2:6-20 -> 194:2-32',
'2:20 -> 195:0',
'2:6 -> 197:0',
'2:20 -> 199:1',
'2:6 -> 201:0',
'2:6-20 -> 202:2-15',
'2:20 -> 203:0',
'2:6 -> 205:0',
'2:20 -> 207:1',
'2:6 -> 209:0',
'2:6-20 -> 210:2-18',
'2:20 -> 211:0',
'2:6 -> 213:0',
'2:20 -> 215:1',
'2:6 -> 217:0',
'2:6-20 -> 218:2-26',
'2:20 -> 219:0',
'2:6 -> 221:0',
'2:20 -> 223:1',
'2:6 -> 225:0',
'2:6-20 -> 227:2-14',
'2:20 -> 228:0',
'2:6 -> 230:0',
'2:20 -> 233:1',
'2:6 -> 235:0',
'2:6-20 -> 236:2-39',
'2:6-20 -> 237:2-30',
'2:20 -> 238:0',
'2:6 -> 240:0',
'2:20 -> 242:1',
'2:6 -> 244:0',
'2:6-20 -> 245:2-26',
'2:20 -> 246:0',
'2:6 -> 248:0',
'2:20 -> 251:1',
'2:6 -> 253:0',
'2:6-20 -> 254:2-36',
'2:6-20 -> 255:2-23',
'2:20 -> 256:0',
'2:6 -> 258:0',
'2:20 -> 260:1',
'2:6 -> 262:0',
'2:6-20 -> 263:2-20',
'2:20 -> 264:0',
'2:6 -> 266:0',
'2:20 -> 268:1',
'2:6 -> 270:0',
'2:6-20 -> 283:2-11',
'2:20 -> 284:0',
'2:6 -> 286:0',
'2:6-20 -> 287:2-11',
'2:6-20 -> 288:2-12',
'2:20 -> 289:0',
'2:6 -> 291:0',
'2:6-20 -> 292:2-12',
'2:20 -> 293:0',
'2:6 -> 295:0',
'2:6-20 -> 298:2-18',
'2:6-20 -> 299:2-11',
'2:6-20 -> 300:2-12',
'2:20 -> 301:0',
'2:6 -> 303:0',
'2:20 -> 305:1',
'2:6 -> 307:0',
'2:6-20 -> 308:2-18',
'2:20 -> 309:0',
'2:6 -> 311:0',
'2:20 -> 314:1',
'2:6 -> 316:0',
'2:6-20 -> 318:2-20',
'2:6-20 -> 319:2-24',
'2:20 -> 320:0',
'2:6 -> 322:0',
'2:20 -> 324:1',
'2:6 -> 326:0',
'2:6-20 -> 328:2-17',
'2:20 -> 329:0',
'2:6 -> 331:0',
'2:20 -> 333:1',
'2:6 -> 334:0',
'2:6-20 -> 335:2-17',
'2:20 -> 336:0',
'2:6 -> 338:0',
'2:20 -> 342:1',
'2:6 -> 344:0',
'2:6-20 -> 352:2-24',
'2:6-20 -> 353:2-32',
'2:20 -> 354:0',
'2:6 -> 356:0',
'2:20 -> 358:1',
'2:6 -> 360:0',
'2:6-20 -> 362:2-17',
'2:6-20 -> 363:2-14',
'2:20 -> 364:0',
'2:6-20 -> 366:0-72',
'2:6 -> 367:0',
'2:6-20 -> 368:2-15',
'2:20 -> 369:0',
'2:6 -> 371:0',
'2:6-20 -> 372:2-26',
'2:6-20 -> 373:2-26',
'2:6-20 -> 374:2-21',
'2:6-20 -> 375:2-21',
'2:6-20 -> 376:2-16',
'2:6-20 -> 377:2-16',
'2:6-20 -> 378:2-16',
'2:6-20 -> 379:2-17',
'2:6-20 -> 380:2-17',
'2:6-20 -> 381:2-15',
'2:6-20 -> 382:2-15',
'2:6-20 -> 383:2-20',
'2:6-20 -> 384:2-40',
'2:6-20 -> 385:2-17',
'2:6-20 -> 386:2-22',
'2:6-20 -> 387:2-24',
'2:6-20 -> 388:2-25',
'2:6-20 -> 389:2-26',
'2:6-20 -> 390:2-20',
'2:6-20 -> 391:2-29',
'2:6-20 -> 392:2-30',
'2:6-20 -> 393:2-40',
'2:6-20 -> 394:2-36',
'2:6-20 -> 395:2-29',
'2:6-20 -> 396:2-24',
'2:6-20 -> 397:2-32',
'2:6-20 -> 398:2-14',
'2:6-20 -> 399:2-20',
'2:6-20 -> 400:2-18',
'2:6-20 -> 401:2-19',
'2:6-20 -> 402:2-20',
'2:6-20 -> 403:2-16',
'2:6-20 -> 404:2-18',
'2:6-20 -> 405:2-15',
'2:6-20 -> 406:2-21',
'2:6-20 -> 407:2-23',
'2:6-20 -> 408:2-29',
'2:6-20 -> 409:2-27',
'2:6-20 -> 410:2-28',
'2:6-20 -> 411:2-29',
'2:6-20 -> 412:2-25',
'2:6-20 -> 413:2-26',
'2:6-20 -> 414:2-27',
'2:6 -> 415:2',
'2:20 -> 416:0',
'2:6 -> 418:0',
'2:6-20 -> 419:2-26',
'2:6-20 -> 420:2-26',
'2:6-20 -> 421:2-21',
'2:6-20 -> 422:2-21',
'2:6-20 -> 423:2-16',
'2:6-20 -> 424:2-16',
'2:6-20 -> 425:2-16',
'2:6-20 -> 426:2-17',
'2:6-20 -> 427:2-17',
'2:6-20 -> 428:2-15',
'2:6-20 -> 429:2-15',
'2:6-20 -> 430:2-20',
'2:6-20 -> 431:2-40',
'2:6-20 -> 432:2-17',
'2:6-20 -> 433:2-22',
'2:6-20 -> 434:2-24',
'2:6-20 -> 435:2-25',
'2:6-20 -> 436:2-26',
'2:6-20 -> 437:2-20',
'2:6-20 -> 438:2-29',
'2:6-20 -> 439:2-30',
'2:6-20 -> 440:2-40',
'2:6-20 -> 441:2-36',
'2:6-20 -> 442:2-29',
'2:6-20 -> 443:2-24',
'2:6-20 -> 444:2-32',
'2:6-20 -> 445:2-14',
'2:6-20 -> 446:2-20',
'2:6-20 -> 447:2-18',
'2:6-20 -> 448:2-19',
'2:6-20 -> 449:2-20',
'2:6-20 -> 450:2-16',
'2:6-20 -> 451:2-18',
'2:6-20 -> 452:2-15',
'2:6-20 -> 453:2-21',
'2:6-20 -> 454:2-23',
'2:6-20 -> 455:2-29',
'2:6-20 -> 456:2-27',
'2:6-20 -> 457:2-28',
'2:6-20 -> 458:2-29',
'2:6-20 -> 459:2-25',
'2:6-20 -> 460:2-26',
'2:6-20 -> 461:2-27',
'2:6 -> 462:2',
'2:20 -> 463:0',
])
})
test('utilities have source maps', async () => {
let config = {
content: [{ raw: `text-red-500` }],
}
let input = css`
@tailwind utilities;
`
let result = await run(input, config)
let { sources, annotations } = parseSourceMaps(result)
// All CSS generated by Tailwind CSS should be annotated with source maps
// And always be able to point to the original source file
expect(sources).not.toContain('<no source>')
expect(sources.length).toBe(1)
expect(annotations).toStrictEqual([
'2:6 -> 1:0',
'2:6-25 -> 2:4-24',
'2:6 -> 3:4',
'2:25 -> 4:0',
])
})
it('components have source maps', async () => {
let config = {
content: [{ raw: `container` }],
}
let input = css`
@tailwind components;
`
let result = await run(input, config)
let { sources, annotations } = parseSourceMaps(result)
// All CSS generated by Tailwind CSS should be annotated with source maps
// And always be able to point to the original source file
expect(sources).not.toContain('<no source>')
expect(sources.length).toBe(1)
expect(annotations).toEqual([
'2:6 -> 1:0',
'2:6 -> 2:4',
'2:26 -> 3:0',
'2:6 -> 4:0',
'2:6 -> 5:4',
'2:6 -> 6:8',
'2:26 -> 7:4',
'2:26 -> 8:0',
'2:6 -> 9:0',
'2:6 -> 10:4',
'2:6 -> 11:8',
'2:26 -> 12:4',
'2:26 -> 13:0',
'2:6 -> 14:0',
'2:6 -> 15:4',
'2:6 -> 16:8',
'2:26 -> 17:4',
'2:26 -> 18:0',
'2:6 -> 19:0',
'2:6 -> 20:4',
'2:6 -> 21:8',
'2:26 -> 22:4',
'2:26 -> 23:0',
'2:6 -> 24:0',
'2:6 -> 25:4',
'2:6 -> 26:8',
'2:26 -> 27:4',
'2:26 -> 28:0',
])
})
it('source maps for layer rules are not rewritten to point to @tailwind directives', async () => {
let config = {
content: [{ raw: `font-normal foo hover:foo` }],
}
let utilitiesFile = postcss.parse(
css`
@tailwind utilities;
`,
{ from: 'components.css', map: { prev: map } }
)
let mainCssFile = postcss.parse(
css`
@layer utilities {
.foo {
background-color: red;
}
}
`,
{ from: 'input.css', map: { prev: map } }
)
// Just pretend that there's an @import in `mainCssFile` that imports the nodes from `utilitiesFile`
let input = postcss.root({
nodes: [...utilitiesFile.nodes, ...mainCssFile.nodes],
source: mainCssFile.source,
})
let result = await run(input, config)
let { sources, annotations } = parseSourceMaps(result)
// All CSS generated by Tailwind CSS should be annotated with source maps
// And always be able to point to the original source file
expect(sources).not.toContain('<no source>')
// And we should see that the source map for the layer rule is not rewritten
// to point to the @tailwind directive but instead points to the original
expect(sources.length).toBe(2)
expect(sources).toEqual(['components.css', 'input.css'])
expect(annotations).toEqual([
'2:8 -> 1:0',
'2:8 -> 2:12',
'2:27 -> 3:0',
'3:10 -> 4:10',
'4:12-33 -> 5:12-33',
'5:10 -> 6:10',
'3:10 -> 7:10',
'4:12-33 -> 8:12-33',
'5:10 -> 9:10',
])
})
})

View File

@ -1,47 +1,47 @@
import { run } from './util/run'
import { crosscheck, run, css } from './util/run'
let css = String.raw
crosscheck(() => {
test('it detects classes in lit-html templates', () => {
let config = {
content: [
{
raw: `html\`<button class="bg-blue-400 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">\${data.title}</button>\`;`,
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
test('it detects classes in lit-html templates', () => {
let config = {
content: [
{
raw: `html\`<button class="bg-blue-400 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">\${data.title}</button>\`;`,
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.rounded {
border-radius: 0.25rem;
}
.bg-blue-400 {
--tw-bg-opacity: 1;
background-color: rgb(96 165 250 / var(--tw-bg-opacity));
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.font-bold {
font-weight: 700;
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.hover\:bg-blue-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
}
`)
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchCss(css`
.rounded {
border-radius: 0.25rem;
}
.bg-blue-400 {
--tw-bg-opacity: 1;
background-color: rgb(96 165 250 / var(--tw-bg-opacity));
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.font-bold {
font-weight: 700;
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.hover\:bg-blue-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
}
`)
})
})
})

View File

@ -1,46 +1,43 @@
import path from 'path'
import { crosscheck, run, css } from './util/run'
import { run } from './util/run'
import { env } from '../src/lib/sharedState'
crosscheck(({ stable, oxide }) => {
oxide.test.todo('it detects svelte based on the file extension')
stable.test('it detects svelte based on the file extension', () => {
let config = {
content: [path.resolve(__dirname, './syntax-svelte.test.svelte')],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let css = String.raw
let input = css`
@tailwind components;
@tailwind utilities;
`
let t = env.OXIDE ? test.skip : test
t('it detects svelte based on the file extension', () => {
let config = {
content: [path.resolve(__dirname, './syntax-svelte.test.svelte')],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let input = css`
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
@media (min-width: 1024px) {
.lg\:hover\:bg-blue-500:hover {
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
}
`)
@media (min-width: 1024px) {
.lg\:hover\:bg-blue-500:hover {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}
}
`)
})
})
})
t('using raw with svelte extension', () => {
let config = {
content: [
{
raw: `
oxide.test.todo('using raw with svelte extension')
stable.test('using raw with svelte extension', () => {
let config = {
content: [
{
raw: `
<script>
let current = 'foo'
</script>
@ -53,31 +50,32 @@ t('using raw with svelte extension', () => {
Click me
</button>
`,
extension: 'svelte',
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
extension: 'svelte',
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let input = css`
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
@media (min-width: 1024px) {
.lg\:hover\:bg-blue-500:hover {
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
}
`)
@media (min-width: 1024px) {
.lg\:hover\:bg-blue-500:hover {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}
}
`)
})
})
})

View File

@ -1,67 +1,69 @@
import { run, html, css } from './util/run'
import { crosscheck, run, html, css } from './util/run'
test('class variants are inserted at `@tailwind variants`', async () => {
let config = {
content: [{ raw: html`<div class="font-bold hover:font-bold md:font-bold"></div>` }],
}
let input = css`
@tailwind utilities;
@tailwind variants;
.foo {
color: black;
crosscheck(() => {
test('class variants are inserted at `@tailwind variants`', async () => {
let config = {
content: [{ raw: html`<div class="font-bold hover:font-bold md:font-bold"></div>` }],
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-bold {
font-weight: 700;
}
.hover\:font-bold:hover {
font-weight: 700;
}
@media (min-width: 768px) {
.md\:font-bold {
font-weight: 700;
}
}
let input = css`
@tailwind utilities;
@tailwind variants;
.foo {
color: black;
}
`)
})
})
`
test('`@tailwind screens` works as an alias for `@tailwind variants`', async () => {
let config = {
content: [{ raw: html`<div class="font-bold hover:font-bold md:font-bold"></div>` }],
}
let input = css`
@tailwind utilities;
@tailwind screens;
.foo {
color: black;
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-bold {
font-weight: 700;
}
.hover\:font-bold:hover {
font-weight: 700;
}
@media (min-width: 768px) {
.md\:font-bold {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-bold {
font-weight: 700;
}
}
.hover\:font-bold:hover {
font-weight: 700;
}
@media (min-width: 768px) {
.md\:font-bold {
font-weight: 700;
}
}
.foo {
color: black;
}
`)
})
})
test('`@tailwind screens` works as an alias for `@tailwind variants`', async () => {
let config = {
content: [{ raw: html`<div class="font-bold hover:font-bold md:font-bold"></div>` }],
}
let input = css`
@tailwind utilities;
@tailwind screens;
.foo {
color: black;
}
`)
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.font-bold {
font-weight: 700;
}
.hover\:font-bold:hover {
font-weight: 700;
}
@media (min-width: 768px) {
.md\:font-bold {
font-weight: 700;
}
}
.foo {
color: black;
}
`)
})
})
})

View File

@ -1,18 +1,21 @@
import { toPath } from '../src/util/toPath'
import { crosscheck } from './util/run'
it('should keep an array as an array', () => {
let input = ['a', 'b', '0', 'c']
crosscheck(() => {
it('should keep an array as an array', () => {
let input = ['a', 'b', '0', 'c']
expect(toPath(input)).toBe(input)
})
it.each`
input | output
${'a.b.c'} | ${['a', 'b', 'c']}
${'a[0].b.c'} | ${['a', '0', 'b', 'c']}
${'.a'} | ${['a']}
${'[].a'} | ${['a']}
${'a[1.5][b][c]'} | ${['a', '1.5', 'b', 'c']}
`('should convert "$input" to "$output"', ({ input, output }) => {
expect(toPath(input)).toEqual(output)
expect(toPath(input)).toBe(input)
})
it.each`
input | output
${'a.b.c'} | ${['a', 'b', 'c']}
${'a[0].b.c'} | ${['a', '0', 'b', 'c']}
${'.a'} | ${['a']}
${'[].a'} | ${['a']}
${'a[1.5][b][c]'} | ${['a', '1.5', 'b', 'c']}
`('should convert "$input" to "$output"', ({ input, output }) => {
expect(toPath(input)).toEqual(output)
})
})

View File

@ -0,0 +1,18 @@
import { crosscheck } from './run'
crosscheck(({ stable, oxide, engine }) => {
stable.test('should run on stable', () => {
expect(engine.stable).toBe(true)
expect(engine.oxide).toBe(false)
})
oxide.test('should run on oxide', () => {
expect(engine.stable).toBe(false)
expect(engine.oxide).toBe(true)
})
test('should run on both', () => {
oxide.expect(engine.oxide).toBe(true)
oxide.expect(engine.stable).toBe(false)
stable.expect(engine.oxide).toBe(false)
stable.expect(engine.stable).toBe(true)
})
})

View File

@ -1,6 +1,7 @@
import path from 'path'
import postcss from 'postcss'
import tailwind from '../../src'
import { env } from '../../src/lib/sharedState'
export * from './strings'
export * from './defaults'
@ -31,3 +32,49 @@ export function runWithSourceMaps(input, config, plugin = tailwind) {
},
})
}
let nullTest = Object.assign(function () {}, {
skip: () => {},
only: () => {},
each: () => () => {},
todo: () => {},
})
let nullProxy = new Proxy(
{
test: nullTest,
it: nullTest,
xit: nullTest.skip,
fit: nullTest.only,
xdescribe: nullTest.skip,
fdescribe: nullTest.only,
},
{
get(target, prop, _receiver) {
if (prop in target) {
return target[prop]
}
return Object.assign(() => {
return nullProxy
}, nullProxy)
},
}
)
export function crosscheck(fn) {
let engines =
env.ENGINE === 'oxide' ? [{ engine: 'Stable' }, { engine: 'Oxide' }] : [{ engine: 'Stable' }]
describe.each(engines)('$engine', ({ engine }) => {
let engines = {
oxide: engine === 'Oxide' ? globalThis : nullProxy,
stable: engine === 'Stable' ? globalThis : nullProxy,
engine: { oxide: engine === 'Oxide', stable: engine === 'Stable' },
}
beforeEach(() => {
env.OXIDE = engines.engine.oxide
})
fn(engines)
})
}

File diff suppressed because it is too large Load Diff

View File

@ -1,74 +1,76 @@
import { html, run, css } from './util/run'
import log from '../src/util/log'
import { crosscheck, run, html, css } from './util/run'
let warn
crosscheck(() => {
let warn
beforeEach(() => {
warn = jest.spyOn(log, 'warn')
})
afterEach(() => {
warn.mockClear()
})
test('it warns when there is no content key', async () => {
let config = {
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
`
await run(input, config)
expect(warn).toHaveBeenCalledTimes(1)
expect(warn.mock.calls.map((x) => x[0])).toEqual(['content-problems'])
})
test('it warns when there is an empty content key', async () => {
let config = {
content: [],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
`
await run(input, config)
expect(warn).toHaveBeenCalledTimes(1)
expect(warn.mock.calls.map((x) => x[0])).toEqual(['content-problems'])
})
test('it warns when there are no utilities generated', async () => {
let config = {
content: [{ raw: html`nothing here matching a utility` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
`
await run(input, config)
expect(warn).toHaveBeenCalledTimes(1)
expect(warn.mock.calls.map((x) => x[0])).toEqual(['content-problems'])
})
it('warnings are not thrown when only variant utilities are generated', async () => {
let config = {
content: [{ raw: html`<div class="sm:underline"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
`
await run(input, config)
expect(warn).toHaveBeenCalledTimes(0)
beforeEach(() => {
warn = jest.spyOn(log, 'warn')
})
afterEach(() => {
warn.mockClear()
})
test('it warns when there is no content key', async () => {
let config = {
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
`
await run(input, config)
expect(warn).toHaveBeenCalledTimes(1)
expect(warn.mock.calls.map((x) => x[0])).toEqual(['content-problems'])
})
test('it warns when there is an empty content key', async () => {
let config = {
content: [],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
`
await run(input, config)
expect(warn).toHaveBeenCalledTimes(1)
expect(warn.mock.calls.map((x) => x[0])).toEqual(['content-problems'])
})
test('it warns when there are no utilities generated', async () => {
let config = {
content: [{ raw: html`nothing here matching a utility` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
`
await run(input, config)
expect(warn).toHaveBeenCalledTimes(1)
expect(warn.mock.calls.map((x) => x[0])).toEqual(['content-problems'])
})
it('warnings are not thrown when only variant utilities are generated', async () => {
let config = {
content: [{ raw: html`<div class="sm:underline"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
`
await run(input, config)
expect(warn).toHaveBeenCalledTimes(0)
})
})

View File

@ -1,240 +1,243 @@
import withAlphaVariable from '../src/util/withAlphaVariable'
import { crosscheck } from './util/run'
test('it adds the right custom property', () => {
expect(
withAlphaVariable({ color: '#ff0000', property: 'color', variable: '--tw-text-opacity' })
).toEqual({
'--tw-text-opacity': '1',
color: 'rgb(255 0 0 / var(--tw-text-opacity))',
})
expect(
withAlphaVariable({
color: 'hsl(240 100% 50%)',
property: 'color',
variable: '--tw-text-opacity',
crosscheck(() => {
test('it adds the right custom property', () => {
expect(
withAlphaVariable({ color: '#ff0000', property: 'color', variable: '--tw-text-opacity' })
).toEqual({
'--tw-text-opacity': '1',
color: 'rgb(255 0 0 / var(--tw-text-opacity))',
})
expect(
withAlphaVariable({
color: 'hsl(240 100% 50%)',
property: 'color',
variable: '--tw-text-opacity',
})
).toEqual({
'--tw-text-opacity': '1',
color: 'hsl(240 100% 50% / var(--tw-text-opacity))',
})
})
test('it ignores colors that cannot be parsed', () => {
expect(
withAlphaVariable({
color: 'currentColor',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'currentColor',
})
expect(
withAlphaVariable({
color: 'rgb(255, 0)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'rgb(255, 0)',
})
expect(
withAlphaVariable({
color: 'rgb(255)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'rgb(255)',
})
expect(
withAlphaVariable({
color: 'rgb(255, 0, 0, 255)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'rgb(255, 0, 0, 255)',
})
expect(
withAlphaVariable({
color: 'rgb(var(--color))',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'rgb(var(--color))',
})
})
test('it ignores colors that already have an alpha channel', () => {
expect(
withAlphaVariable({
color: '#ff0000ff',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': '#ff0000ff',
})
expect(
withAlphaVariable({
color: '#ff000080',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': '#ff000080',
})
expect(
withAlphaVariable({
color: '#f00a',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': '#f00a',
})
expect(
withAlphaVariable({
color: '#f00f',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': '#f00f',
})
expect(
withAlphaVariable({
color: 'rgba(255, 255, 255, 1)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'rgba(255, 255, 255, 1)',
})
expect(
withAlphaVariable({
color: 'rgba(255, 255, 255, 0.5)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'rgba(255, 255, 255, 0.5)',
})
expect(
withAlphaVariable({
color: 'rgba(255 255 255 / 0.5)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'rgba(255 255 255 / 0.5)',
})
expect(
withAlphaVariable({
color: 'hsla(240, 100%, 50%, 1)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'hsla(240, 100%, 50%, 1)',
})
expect(
withAlphaVariable({
color: 'hsla(240, 100%, 50%, 0.5)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'hsla(240, 100%, 50%, 0.5)',
})
expect(
withAlphaVariable({
color: 'hsl(240 100% 50% / 0.5)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'hsl(240 100% 50% / 0.5)',
})
})
test('it allows a closure to be passed', () => {
expect(
withAlphaVariable({
color: ({ opacityVariable }) => `rgba(0, 0, 0, var(${opacityVariable}))`,
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'--tw-bg-opacity': '1',
'background-color': 'rgba(0, 0, 0, var(--tw-bg-opacity))',
})
expect(
withAlphaVariable({
color: ({ opacityValue }) => `rgba(0, 0, 0, ${opacityValue})`,
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'--tw-bg-opacity': '1',
'background-color': 'rgba(0, 0, 0, var(--tw-bg-opacity))',
})
})
test('it transforms rgb and hsl to space-separated rgb and hsl', () => {
expect(
withAlphaVariable({
color: 'rgb(50, 50, 50)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'--tw-bg-opacity': '1',
'background-color': 'rgb(50 50 50 / var(--tw-bg-opacity))',
})
expect(
withAlphaVariable({
color: 'rgb(50 50 50)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'--tw-bg-opacity': '1',
'background-color': 'rgb(50 50 50 / var(--tw-bg-opacity))',
})
expect(
withAlphaVariable({
color: 'hsl(50, 50%, 50%)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'--tw-bg-opacity': '1',
'background-color': 'hsl(50 50% 50% / var(--tw-bg-opacity))',
})
expect(
withAlphaVariable({
color: 'hsl(50 50% 50%)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'--tw-bg-opacity': '1',
'background-color': 'hsl(50 50% 50% / var(--tw-bg-opacity))',
})
})
test('it transforms named colors to rgb', () => {
expect(
withAlphaVariable({
color: 'red',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'--tw-bg-opacity': '1',
'background-color': 'rgb(255 0 0 / var(--tw-bg-opacity))',
})
).toEqual({
'--tw-text-opacity': '1',
color: 'hsl(240 100% 50% / var(--tw-text-opacity))',
})
})
test('it ignores colors that cannot be parsed', () => {
expect(
withAlphaVariable({
color: 'currentColor',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'currentColor',
})
expect(
withAlphaVariable({
color: 'rgb(255, 0)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'rgb(255, 0)',
})
expect(
withAlphaVariable({
color: 'rgb(255)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'rgb(255)',
})
expect(
withAlphaVariable({
color: 'rgb(255, 0, 0, 255)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'rgb(255, 0, 0, 255)',
})
expect(
withAlphaVariable({
color: 'rgb(var(--color))',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'rgb(var(--color))',
})
})
test('it ignores colors that already have an alpha channel', () => {
expect(
withAlphaVariable({
color: '#ff0000ff',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': '#ff0000ff',
})
expect(
withAlphaVariable({
color: '#ff000080',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': '#ff000080',
})
expect(
withAlphaVariable({
color: '#f00a',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': '#f00a',
})
expect(
withAlphaVariable({
color: '#f00f',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': '#f00f',
})
expect(
withAlphaVariable({
color: 'rgba(255, 255, 255, 1)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'rgba(255, 255, 255, 1)',
})
expect(
withAlphaVariable({
color: 'rgba(255, 255, 255, 0.5)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'rgba(255, 255, 255, 0.5)',
})
expect(
withAlphaVariable({
color: 'rgba(255 255 255 / 0.5)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'rgba(255 255 255 / 0.5)',
})
expect(
withAlphaVariable({
color: 'hsla(240, 100%, 50%, 1)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'hsla(240, 100%, 50%, 1)',
})
expect(
withAlphaVariable({
color: 'hsla(240, 100%, 50%, 0.5)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'hsla(240, 100%, 50%, 0.5)',
})
expect(
withAlphaVariable({
color: 'hsl(240 100% 50% / 0.5)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'background-color': 'hsl(240 100% 50% / 0.5)',
})
})
test('it allows a closure to be passed', () => {
expect(
withAlphaVariable({
color: ({ opacityVariable }) => `rgba(0, 0, 0, var(${opacityVariable}))`,
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'--tw-bg-opacity': '1',
'background-color': 'rgba(0, 0, 0, var(--tw-bg-opacity))',
})
expect(
withAlphaVariable({
color: ({ opacityValue }) => `rgba(0, 0, 0, ${opacityValue})`,
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'--tw-bg-opacity': '1',
'background-color': 'rgba(0, 0, 0, var(--tw-bg-opacity))',
})
})
test('it transforms rgb and hsl to space-separated rgb and hsl', () => {
expect(
withAlphaVariable({
color: 'rgb(50, 50, 50)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'--tw-bg-opacity': '1',
'background-color': 'rgb(50 50 50 / var(--tw-bg-opacity))',
})
expect(
withAlphaVariable({
color: 'rgb(50 50 50)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'--tw-bg-opacity': '1',
'background-color': 'rgb(50 50 50 / var(--tw-bg-opacity))',
})
expect(
withAlphaVariable({
color: 'hsl(50, 50%, 50%)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'--tw-bg-opacity': '1',
'background-color': 'hsl(50 50% 50% / var(--tw-bg-opacity))',
})
expect(
withAlphaVariable({
color: 'hsl(50 50% 50%)',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'--tw-bg-opacity': '1',
'background-color': 'hsl(50 50% 50% / var(--tw-bg-opacity))',
})
})
test('it transforms named colors to rgb', () => {
expect(
withAlphaVariable({
color: 'red',
property: 'background-color',
variable: '--tw-bg-opacity',
})
).toEqual({
'--tw-bg-opacity': '1',
'background-color': 'rgb(255 0 0 / var(--tw-bg-opacity))',
})
})