variants.test.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170
  1. import fs from 'fs'
  2. import path from 'path'
  3. import postcss from 'postcss'
  4. import { run, html, css, defaults } from './util/run'
  5. test('variants', () => {
  6. let config = {
  7. darkMode: 'class',
  8. content: [path.resolve(__dirname, './variants.test.html')],
  9. corePlugins: { preflight: false },
  10. }
  11. let input = css`
  12. @tailwind base;
  13. @tailwind components;
  14. @tailwind utilities;
  15. `
  16. return run(input, config).then((result) => {
  17. expect(result.css).toMatchFormattedCss(
  18. fs.readFileSync(path.resolve(__dirname, './variants.test.css'), 'utf8')
  19. )
  20. })
  21. })
  22. test('order matters and produces different behaviour', () => {
  23. let config = {
  24. content: [
  25. {
  26. raw: html`
  27. <div class="hover:file:[--value:1]"></div>
  28. <div class="file:hover:[--value:2]"></div>
  29. `,
  30. },
  31. ],
  32. }
  33. return run('@tailwind utilities', config).then((result) => {
  34. expect(result.css).toMatchFormattedCss(css`
  35. .file\:hover\:\[--value\:2\]:hover::-webkit-file-upload-button {
  36. --value: 2;
  37. }
  38. .file\:hover\:\[--value\:2\]:hover::file-selector-button {
  39. --value: 2;
  40. }
  41. .hover\:file\:\[--value\:1\]::-webkit-file-upload-button:hover {
  42. --value: 1;
  43. }
  44. .hover\:file\:\[--value\:1\]::file-selector-button:hover {
  45. --value: 1;
  46. }
  47. `)
  48. })
  49. })
  50. describe('custom advanced variants', () => {
  51. test('at-rules without params', () => {
  52. let config = {
  53. content: [
  54. {
  55. raw: html` <div class="ogre:text-center"></div> `,
  56. },
  57. ],
  58. plugins: [
  59. function ({ addVariant }) {
  60. addVariant('ogre', '@layer')
  61. },
  62. ],
  63. }
  64. return run('@tailwind components; @tailwind utilities', config).then((result) => {
  65. return expect(result.css).toMatchFormattedCss(css`
  66. @layer {
  67. .ogre\:text-center {
  68. text-align: center;
  69. }
  70. }
  71. `)
  72. })
  73. })
  74. test('prose-headings usage on its own', () => {
  75. let config = {
  76. content: [
  77. {
  78. raw: html` <div class="prose-headings:text-center"></div> `,
  79. },
  80. ],
  81. plugins: [
  82. function ({ addVariant }) {
  83. addVariant('prose-headings', ':where(&) :is(h1, h2, h3, h4)')
  84. },
  85. ],
  86. }
  87. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  88. return expect(result.css).toMatchFormattedCss(css`
  89. :where(.prose-headings\:text-center) :is(h1, h2, h3, h4) {
  90. text-align: center;
  91. }
  92. `)
  93. })
  94. })
  95. test('prose-headings with another "simple" variant', () => {
  96. let config = {
  97. content: [
  98. {
  99. raw: html`
  100. <div class="hover:prose-headings:text-center"></div>
  101. <div class="prose-headings:hover:text-center"></div>
  102. `,
  103. },
  104. ],
  105. plugins: [
  106. function ({ addVariant }) {
  107. addVariant('prose-headings', ':where(&) :is(h1, h2, h3, h4)')
  108. },
  109. ],
  110. }
  111. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  112. return expect(result.css).toMatchFormattedCss(css`
  113. :where(.hover\:prose-headings\:text-center) :is(h1, h2, h3, h4):hover,
  114. :where(.prose-headings\:hover\:text-center:hover) :is(h1, h2, h3, h4) {
  115. text-align: center;
  116. }
  117. `)
  118. })
  119. })
  120. test('prose-headings with another "complex" variant', () => {
  121. let config = {
  122. content: [
  123. {
  124. raw: html`
  125. <div class="group-hover:prose-headings:text-center"></div>
  126. <div class="prose-headings:group-hover:text-center"></div>
  127. `,
  128. },
  129. ],
  130. plugins: [
  131. function ({ addVariant }) {
  132. addVariant('prose-headings', ':where(&) :is(h1, h2, h3, h4)')
  133. },
  134. ],
  135. }
  136. return run('@tailwind utilities', config).then((result) => {
  137. return expect(result.css).toMatchFormattedCss(css`
  138. .group:hover :where(.group-hover\:prose-headings\:text-center) :is(h1, h2, h3, h4),
  139. :where(.group:hover .prose-headings\:group-hover\:text-center) :is(h1, h2, h3, h4) {
  140. text-align: center;
  141. }
  142. `)
  143. })
  144. })
  145. test('using variants with multi-class selectors', () => {
  146. let config = {
  147. content: [
  148. {
  149. raw: html` <div class="screen:parent screen:child"></div> `,
  150. },
  151. ],
  152. plugins: [
  153. function ({ addVariant, addComponents }) {
  154. addComponents({
  155. '.parent .child': {
  156. foo: 'bar',
  157. },
  158. })
  159. addVariant('screen', '@media screen')
  160. },
  161. ],
  162. }
  163. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  164. return expect(result.css).toMatchFormattedCss(css`
  165. @media screen {
  166. .screen\:parent .child,
  167. .parent .screen\:child {
  168. foo: bar;
  169. }
  170. }
  171. `)
  172. })
  173. })
  174. test('using multiple classNames in your custom variant', () => {
  175. let config = {
  176. content: [
  177. {
  178. raw: html` <div class="my-variant:underline test"></div> `,
  179. },
  180. ],
  181. plugins: [
  182. function ({ addVariant }) {
  183. addVariant('my-variant', '&:where(.one, .two, .three)')
  184. },
  185. ],
  186. }
  187. let input = css`
  188. @tailwind components;
  189. @tailwind utilities;
  190. @layer components {
  191. .test {
  192. @apply my-variant:italic;
  193. }
  194. }
  195. `
  196. return run(input, config).then((result) => {
  197. return expect(result.css).toMatchFormattedCss(css`
  198. .test:where(.one, .two, .three) {
  199. font-style: italic;
  200. }
  201. .my-variant\:underline:where(.one, .two, .three) {
  202. text-decoration-line: underline;
  203. }
  204. `)
  205. })
  206. })
  207. test('variant format string must include at-rule or & (1)', async () => {
  208. let config = {
  209. content: [
  210. {
  211. raw: html` <div class="wtf-bbq:text-center"></div> `,
  212. },
  213. ],
  214. plugins: [
  215. function ({ addVariant }) {
  216. addVariant('wtf-bbq', 'lol')
  217. },
  218. ],
  219. }
  220. await expect(run('@tailwind components;@tailwind utilities', config)).rejects.toThrowError(
  221. "Your custom variant `wtf-bbq` has an invalid format string. Make sure it's an at-rule or contains a `&` placeholder."
  222. )
  223. })
  224. test('variant format string must include at-rule or & (2)', async () => {
  225. let config = {
  226. content: [
  227. {
  228. raw: html` <div class="wtf-bbq:text-center"></div> `,
  229. },
  230. ],
  231. plugins: [
  232. function ({ addVariant }) {
  233. addVariant('wtf-bbq', () => 'lol')
  234. },
  235. ],
  236. }
  237. await expect(run('@tailwind components;@tailwind utilities', config)).rejects.toThrowError(
  238. "Your custom variant `wtf-bbq` has an invalid format string. Make sure it's an at-rule or contains a `&` placeholder."
  239. )
  240. })
  241. })
  242. test('stacked peer variants', async () => {
  243. let config = {
  244. content: [{ raw: 'peer-disabled:peer-focus:peer-hover:flex' }],
  245. corePlugins: { preflight: false },
  246. }
  247. let input = css`
  248. @tailwind base;
  249. @tailwind components;
  250. @tailwind utilities;
  251. `
  252. let result = await run(input, config)
  253. expect(result.css).toIncludeCss(css`
  254. .peer:disabled:focus:hover ~ .peer-disabled\:peer-focus\:peer-hover\:flex {
  255. display: flex;
  256. }
  257. `)
  258. })
  259. it('should properly handle keyframes with multiple variants', async () => {
  260. let config = {
  261. content: [
  262. {
  263. raw: 'animate-spin hover:animate-spin focus:animate-spin hover:animate-bounce focus:animate-bounce',
  264. },
  265. ],
  266. }
  267. let input = css`
  268. @tailwind components;
  269. @tailwind utilities;
  270. `
  271. let result = await run(input, config)
  272. expect(result.css).toMatchFormattedCss(css`
  273. @keyframes spin {
  274. to {
  275. transform: rotate(360deg);
  276. }
  277. }
  278. .animate-spin {
  279. animation: 1s linear infinite spin;
  280. }
  281. @keyframes bounce {
  282. 0%,
  283. 100% {
  284. animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
  285. transform: translateY(-25%);
  286. }
  287. 50% {
  288. animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
  289. transform: none;
  290. }
  291. }
  292. .hover\:animate-bounce:hover {
  293. animation: 1s infinite bounce;
  294. }
  295. .hover\:animate-spin:hover {
  296. animation: 1s linear infinite spin;
  297. }
  298. .focus\:animate-bounce:focus {
  299. animation: 1s infinite bounce;
  300. }
  301. .focus\:animate-spin:focus {
  302. animation: 1s linear infinite spin;
  303. }
  304. `)
  305. })
  306. test('custom addVariant with more complex media query params', () => {
  307. let config = {
  308. content: [
  309. {
  310. raw: html` <div class="magic:text-center"></div> `,
  311. },
  312. ],
  313. plugins: [
  314. function ({ addVariant }) {
  315. addVariant('magic', '@media screen and (max-width: 600px)')
  316. },
  317. ],
  318. }
  319. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  320. return expect(result.css).toMatchFormattedCss(css`
  321. @media screen and (max-width: 600px) {
  322. .magic\:text-center {
  323. text-align: center;
  324. }
  325. }
  326. `)
  327. })
  328. })
  329. test('custom addVariant with nested media & format shorthand', () => {
  330. let config = {
  331. content: [
  332. {
  333. raw: html` <div class="magic:text-center"></div> `,
  334. },
  335. ],
  336. plugins: [
  337. function ({ addVariant }) {
  338. addVariant('magic', '@supports (hover: hover) { @media print { &:disabled } }')
  339. },
  340. ],
  341. }
  342. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  343. return expect(result.css).toMatchFormattedCss(css`
  344. @supports (hover: hover) {
  345. @media print {
  346. .magic\:text-center:disabled {
  347. text-align: center;
  348. }
  349. }
  350. }
  351. `)
  352. })
  353. })
  354. test('before and after variants are a bit special, and forced to the end', () => {
  355. let config = {
  356. content: [
  357. {
  358. raw: html`
  359. <div class="before:hover:text-center"></div>
  360. <div class="hover:before:text-center"></div>
  361. `,
  362. },
  363. ],
  364. plugins: [],
  365. }
  366. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  367. return expect(result.css).toMatchFormattedCss(css`
  368. .before\:hover\:text-center:hover:before,
  369. .hover\:before\:text-center:hover:before {
  370. content: var(--tw-content);
  371. text-align: center;
  372. }
  373. `)
  374. })
  375. })
  376. test('before and after variants are a bit special, and forced to the end (2)', () => {
  377. let config = {
  378. content: [
  379. {
  380. raw: html`
  381. <div class="before:prose-headings:text-center"></div>
  382. <div class="prose-headings:before:text-center"></div>
  383. `,
  384. },
  385. ],
  386. plugins: [
  387. function ({ addVariant }) {
  388. addVariant('prose-headings', ':where(&) :is(h1, h2, h3, h4)')
  389. },
  390. ],
  391. }
  392. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  393. return expect(result.css).toMatchFormattedCss(css`
  394. :where(.before\:prose-headings\:text-center) :is(h1, h2, h3, h4):before,
  395. :where(.prose-headings\:before\:text-center) :is(h1, h2, h3, h4):before {
  396. content: var(--tw-content);
  397. text-align: center;
  398. }
  399. `)
  400. })
  401. })
  402. test('returning non-strings and non-selectors in addVariant', () => {
  403. /** @type {import('../types/config').Config} */
  404. let config = {
  405. content: [
  406. {
  407. raw: html`
  408. <div class="peer-aria-expanded:text-center"></div>
  409. <div class="peer-aria-expanded-2:text-center"></div>
  410. `,
  411. },
  412. ],
  413. plugins: [
  414. function ({ addVariant, e }) {
  415. addVariant('peer-aria-expanded', ({ modifySelectors, separator }) =>
  416. // Returning anything other string | string[] | undefined here is not supported
  417. // But we're trying to be lenient here and just throw it out
  418. modifySelectors(
  419. ({ className }) =>
  420. `.peer[aria-expanded="true"] ~ .${e(`peer-aria-expanded${separator}${className}`)}`
  421. )
  422. )
  423. addVariant('peer-aria-expanded-2', ({ modifySelectors, separator }) => {
  424. let nodes = modifySelectors(
  425. ({ className }) => `.${e(`peer-aria-expanded-2${separator}${className}`)}`
  426. )
  427. return [
  428. // Returning anything other than strings here is not supported
  429. // But we're trying to be lenient here and just throw it out
  430. nodes,
  431. '.peer[aria-expanded="false"] ~ &',
  432. ]
  433. })
  434. },
  435. ],
  436. }
  437. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  438. return expect(result.css).toMatchFormattedCss(css`
  439. .peer[aria-expanded='true'] ~ .peer-aria-expanded\:text-center,
  440. .peer[aria-expanded='false'] ~ .peer-aria-expanded-2\:text-center {
  441. text-align: center;
  442. }
  443. `)
  444. })
  445. })
  446. it('should not generate variants of user css if it is not inside a layer', () => {
  447. let config = {
  448. content: [{ raw: html`<div class="hover:foo"></div>` }],
  449. plugins: [],
  450. }
  451. let input = css`
  452. @tailwind components;
  453. @tailwind utilities;
  454. .foo {
  455. color: red;
  456. }
  457. `
  458. return run(input, config).then((result) => {
  459. return expect(result.css).toMatchFormattedCss(css`
  460. .foo {
  461. color: red;
  462. }
  463. `)
  464. })
  465. })
  466. it('should be possible to use responsive modifiers that are defined with special characters', () => {
  467. let config = {
  468. content: [{ raw: html`<div class="<sm:underline"></div>` }],
  469. theme: {
  470. screens: {
  471. '<sm': { max: '399px' },
  472. },
  473. },
  474. plugins: [],
  475. }
  476. return run('@tailwind utilities', config).then((result) => {
  477. return expect(result.css).toMatchFormattedCss(css`
  478. @media (max-width: 399px) {
  479. .\<sm\:underline {
  480. text-decoration-line: underline;
  481. }
  482. }
  483. `)
  484. })
  485. })
  486. it('including just the base layer should not produce variants', () => {
  487. let config = {
  488. content: [{ raw: html`<div class="sm:container sm:underline"></div>` }],
  489. corePlugins: { preflight: false },
  490. }
  491. return run('@tailwind base', config).then((result) => {
  492. return expect(result.css).toMatchFormattedCss(
  493. css`
  494. ${defaults}
  495. `
  496. )
  497. })
  498. })
  499. it('variants for components should not be produced in a file without a components layer', () => {
  500. let config = {
  501. content: [{ raw: html`<div class="sm:container sm:underline"></div>` }],
  502. }
  503. return run('@tailwind utilities', config).then((result) => {
  504. return expect(result.css).toMatchFormattedCss(css`
  505. @media (min-width: 640px) {
  506. .sm\:underline {
  507. text-decoration-line: underline;
  508. }
  509. }
  510. `)
  511. })
  512. })
  513. it('variants for utilities should not be produced in a file without a utilities layer', () => {
  514. let config = {
  515. content: [{ raw: html`<div class="sm:container sm:underline"></div>` }],
  516. }
  517. return run('@tailwind components', config).then((result) => {
  518. return expect(result.css).toMatchFormattedCss(css`
  519. @media (min-width: 640px) {
  520. .sm\:container {
  521. width: 100%;
  522. }
  523. @media (min-width: 640px) {
  524. .sm\:container {
  525. max-width: 640px;
  526. }
  527. }
  528. @media (min-width: 768px) {
  529. .sm\:container {
  530. max-width: 768px;
  531. }
  532. }
  533. @media (min-width: 1024px) {
  534. .sm\:container {
  535. max-width: 1024px;
  536. }
  537. }
  538. @media (min-width: 1280px) {
  539. .sm\:container {
  540. max-width: 1280px;
  541. }
  542. }
  543. @media (min-width: 1536px) {
  544. .sm\:container {
  545. max-width: 1536px;
  546. }
  547. }
  548. }
  549. `)
  550. })
  551. })
  552. test('The visited variant removes opacity support', () => {
  553. let config = {
  554. content: [
  555. {
  556. raw: html`
  557. <a class="visited:border-red-500 visited:bg-red-500 visited:text-red-500"
  558. >Look, it's a link!</a
  559. >
  560. `,
  561. },
  562. ],
  563. plugins: [],
  564. }
  565. return run('@tailwind utilities', config).then((result) => {
  566. return expect(result.css).toMatchFormattedCss(css`
  567. .visited\:border-red-500:visited {
  568. border-color: #ef4444;
  569. }
  570. .visited\:bg-red-500:visited {
  571. background-color: #ef4444;
  572. }
  573. .visited\:text-red-500:visited {
  574. color: #ef4444;
  575. }
  576. `)
  577. })
  578. })
  579. it('appends variants to the correct place when using postcss documents', () => {
  580. let config = {
  581. content: [{ raw: html`<div class="underline sm:underline"></div>` }],
  582. plugins: [],
  583. corePlugins: { preflight: false },
  584. }
  585. const doc = postcss.document()
  586. doc.append(postcss.parse(`a {}`))
  587. doc.append(postcss.parse(`@tailwind base`))
  588. doc.append(postcss.parse(`@tailwind utilities`))
  589. doc.append(postcss.parse(`b {}`))
  590. const result = doc.toResult()
  591. return run(result, config).then((result) => {
  592. return expect(result.css).toMatchFormattedCss(css`
  593. ${defaults}
  594. .underline {
  595. text-decoration-line: underline;
  596. }
  597. @media (min-width: 640px) {
  598. .sm\:underline {
  599. text-decoration-line: underline;
  600. }
  601. }
  602. `)
  603. })
  604. })
  605. it('variants support multiple, grouped selectors (html)', () => {
  606. let config = {
  607. content: [{ raw: html`<div class="sm:base1 sm:base2"></div>` }],
  608. plugins: [],
  609. corePlugins: { preflight: false },
  610. }
  611. let input = css`
  612. @tailwind utilities;
  613. @layer utilities {
  614. .base1 .foo,
  615. .base1 .bar {
  616. color: red;
  617. }
  618. .base2 .bar .base2-foo {
  619. color: red;
  620. }
  621. }
  622. `
  623. return run(input, config).then((result) => {
  624. return expect(result.css).toMatchFormattedCss(css`
  625. @media (min-width: 640px) {
  626. .sm\:base1 .foo,
  627. .sm\:base1 .bar,
  628. .sm\:base2 .bar .base2-foo {
  629. color: red;
  630. }
  631. }
  632. `)
  633. })
  634. })
  635. it('variants support multiple, grouped selectors (apply)', () => {
  636. let config = {
  637. content: [{ raw: html`<div class="baz"></div>` }],
  638. plugins: [],
  639. corePlugins: { preflight: false },
  640. }
  641. let input = css`
  642. @tailwind utilities;
  643. @layer utilities {
  644. .base .foo,
  645. .base .bar {
  646. color: red;
  647. }
  648. }
  649. .baz {
  650. @apply sm:base;
  651. }
  652. `
  653. return run(input, config).then((result) => {
  654. return expect(result.css).toMatchFormattedCss(css`
  655. @media (min-width: 640px) {
  656. .baz .foo,
  657. .baz .bar {
  658. color: red;
  659. }
  660. }
  661. `)
  662. })
  663. })
  664. it('variants only picks the used selectors in a group (html)', () => {
  665. let config = {
  666. content: [{ raw: html`<div class="sm:b"></div>` }],
  667. plugins: [],
  668. corePlugins: { preflight: false },
  669. }
  670. let input = css`
  671. @tailwind utilities;
  672. @layer utilities {
  673. .a,
  674. .b {
  675. color: red;
  676. }
  677. }
  678. `
  679. return run(input, config).then((result) => {
  680. return expect(result.css).toMatchFormattedCss(css`
  681. @media (min-width: 640px) {
  682. .sm\:b {
  683. color: red;
  684. }
  685. }
  686. `)
  687. })
  688. })
  689. it('variants only picks the used selectors in a group (apply)', () => {
  690. let config = {
  691. content: [{ raw: html`<div class="baz"></div>` }],
  692. plugins: [],
  693. corePlugins: { preflight: false },
  694. }
  695. let input = css`
  696. @tailwind utilities;
  697. @layer utilities {
  698. .a,
  699. .b {
  700. color: red;
  701. }
  702. }
  703. .baz {
  704. @apply sm:b;
  705. }
  706. `
  707. return run(input, config).then((result) => {
  708. return expect(result.css).toMatchFormattedCss(css`
  709. @media (min-width: 640px) {
  710. .baz {
  711. color: red;
  712. }
  713. }
  714. `)
  715. })
  716. })
  717. test('hoverOnlyWhenSupported adds hover and pointer media features by default', () => {
  718. let config = {
  719. future: {
  720. hoverOnlyWhenSupported: true,
  721. },
  722. content: [
  723. {
  724. raw: html`<div class="hover:underline group-hover:underline peer-hover:underline"></div>`,
  725. },
  726. ],
  727. corePlugins: { preflight: false },
  728. }
  729. let input = css`
  730. @tailwind base;
  731. @tailwind components;
  732. @tailwind utilities;
  733. `
  734. return run(input, config).then((result) => {
  735. expect(result.css).toMatchFormattedCss(css`
  736. ${defaults}
  737. @media (hover: hover) and (pointer: fine) {
  738. .hover\:underline:hover,
  739. .group:hover .group-hover\:underline,
  740. .peer:hover ~ .peer-hover\:underline {
  741. text-decoration-line: underline;
  742. }
  743. }
  744. `)
  745. })
  746. })
  747. test('multi-class utilities handle selector-mutating variants correctly', () => {
  748. let config = {
  749. content: [
  750. {
  751. raw: html`<div
  752. class="after:foo after:bar after:baz hover:foo hover:bar hover:baz group-hover:foo group-hover:bar group-hover:baz peer-checked:foo peer-checked:bar peer-checked:baz"
  753. ></div>`,
  754. },
  755. {
  756. raw: html`<div
  757. class="after:foo1 after:bar1 after:baz1 hover:foo1 hover:bar1 hover:baz1 group-hover:foo1 group-hover:bar1 group-hover:baz1 peer-checked:foo1 peer-checked:bar1 peer-checked:baz1"
  758. ></div>`,
  759. },
  760. ],
  761. corePlugins: { preflight: false },
  762. }
  763. let input = css`
  764. @tailwind utilities;
  765. @layer utilities {
  766. .foo.bar.baz {
  767. color: red;
  768. }
  769. .foo1 .bar1 .baz1 {
  770. color: red;
  771. }
  772. }
  773. `
  774. // The second set of ::after cases (w/ descendant selectors)
  775. // are clearly "wrong" BUT you can't have a descendant of a
  776. // pseudo - element so the utilities `after:foo1` and
  777. // `after:bar1` are non-sensical so this is still
  778. // perfectly fine behavior
  779. return run(input, config).then((result) => {
  780. expect(result.css).toMatchFormattedCss(css`
  781. .after\:foo.bar.baz:after,
  782. .after\:bar.foo.baz:after,
  783. .after\:baz.foo.bar:after,
  784. .after\:foo1 .bar1 .baz1:after,
  785. .foo1 .after\:bar1 .baz1:after,
  786. .foo1 .bar1 .after\:baz1:after {
  787. content: var(--tw-content);
  788. color: red;
  789. }
  790. .hover\:foo:hover.bar.baz,
  791. .hover\:bar:hover.foo.baz,
  792. .hover\:baz:hover.foo.bar,
  793. .hover\:foo1:hover .bar1 .baz1,
  794. .foo1 .hover\:bar1:hover .baz1,
  795. .foo1 .bar1 .hover\:baz1:hover,
  796. .group:hover .group-hover\:foo.bar.baz,
  797. .group:hover .group-hover\:bar.foo.baz,
  798. .group:hover .group-hover\:baz.foo.bar,
  799. .group:hover .group-hover\:foo1 .bar1 .baz1,
  800. .foo1 .group:hover .group-hover\:bar1 .baz1,
  801. .foo1 .bar1 .group:hover .group-hover\:baz1,
  802. .peer:checked ~ .peer-checked\:foo.bar.baz,
  803. .peer:checked ~ .peer-checked\:bar.foo.baz,
  804. .peer:checked ~ .peer-checked\:baz.foo.bar,
  805. .peer:checked ~ .peer-checked\:foo1 .bar1 .baz1,
  806. .foo1 .peer:checked ~ .peer-checked\:bar1 .baz1,
  807. .foo1 .bar1 .peer:checked ~ .peer-checked\:baz1 {
  808. color: red;
  809. }
  810. `)
  811. })
  812. })
  813. test('class inside pseudo-class function :has', () => {
  814. let config = {
  815. content: [
  816. { raw: html`<div class="foo hover:foo sm:foo"></div>` },
  817. { raw: html`<div class="bar hover:bar sm:bar"></div>` },
  818. { raw: html`<div class="baz hover:baz sm:baz"></div>` },
  819. ],
  820. corePlugins: { preflight: false },
  821. }
  822. let input = css`
  823. @tailwind utilities;
  824. @layer utilities {
  825. :where(.foo) {
  826. color: red;
  827. }
  828. :is(.foo, .bar, .baz) {
  829. color: orange;
  830. }
  831. :is(.foo) {
  832. color: yellow;
  833. }
  834. html:has(.foo) {
  835. color: green;
  836. }
  837. }
  838. `
  839. return run(input, config).then((result) => {
  840. expect(result.css).toMatchFormattedCss(css`
  841. :where(.foo) {
  842. color: red;
  843. }
  844. :is(.foo, .bar, .baz) {
  845. color: orange;
  846. }
  847. .foo {
  848. color: #ff0;
  849. }
  850. html:has(.foo) {
  851. color: green;
  852. }
  853. :where(.hover\:foo:hover) {
  854. color: red;
  855. }
  856. :is(.hover\:foo:hover, .bar, .baz),
  857. :is(.foo, .hover\:bar:hover, .baz),
  858. :is(.foo, .bar, .hover\:baz:hover) {
  859. color: orange;
  860. }
  861. .hover\:foo:hover {
  862. color: #ff0;
  863. }
  864. html:has(.hover\:foo:hover) {
  865. color: green;
  866. }
  867. @media (min-width: 640px) {
  868. :where(.sm\:foo) {
  869. color: red;
  870. }
  871. :is(.sm\:foo, .bar, .baz),
  872. :is(.foo, .sm\:bar, .baz),
  873. :is(.foo, .bar, .sm\:baz) {
  874. color: orange;
  875. }
  876. .sm\:foo {
  877. color: #ff0;
  878. }
  879. html:has(.sm\:foo) {
  880. color: green;
  881. }
  882. }
  883. `)
  884. })
  885. })
  886. test('variant functions returning arrays should output correct results when nesting', async () => {
  887. let config = {
  888. content: [{ raw: html`<div class="test:foo" />` }],
  889. corePlugins: { preflight: false },
  890. plugins: [
  891. function ({ addUtilities, addVariant }) {
  892. addVariant('test', () => ['@media (test)'])
  893. addUtilities({
  894. '.foo': {
  895. display: 'grid',
  896. '> *': {
  897. 'grid-column': 'span 2',
  898. },
  899. },
  900. })
  901. },
  902. ],
  903. }
  904. let input = css`
  905. @tailwind utilities;
  906. `
  907. let result = await run(input, config)
  908. expect(result.css).toMatchFormattedCss(css`
  909. @media (test) {
  910. .test\:foo {
  911. display: grid;
  912. }
  913. .test\:foo > * {
  914. grid-column: span 2;
  915. }
  916. }
  917. `)
  918. })
  919. test('variants with slashes in them work', () => {
  920. let config = {
  921. content: [
  922. {
  923. raw: html` <div class="ar-1/10:flex">ar-1/10</div> `,
  924. },
  925. ],
  926. theme: {
  927. extend: {
  928. screens: {
  929. 'ar-1/10': { raw: '(min-aspect-ratio: 1/10)' },
  930. },
  931. },
  932. },
  933. corePlugins: { preflight: false },
  934. }
  935. let input = css`
  936. @tailwind utilities;
  937. `
  938. return run(input, config).then((result) => {
  939. expect(result.css).toMatchFormattedCss(css`
  940. @media (min-aspect-ratio: 1 / 10) {
  941. .ar-1\/10\:flex {
  942. display: flex;
  943. }
  944. }
  945. `)
  946. })
  947. })
  948. test('variants with slashes support modifiers', () => {
  949. let config = {
  950. content: [
  951. {
  952. raw: html` <div class="ar-1/10/20:flex">ar-1/10</div> `,
  953. },
  954. ],
  955. corePlugins: { preflight: false },
  956. plugins: [
  957. function ({ matchVariant }) {
  958. matchVariant(
  959. 'ar',
  960. (value, { modifier }) => {
  961. return [`@media (min-aspect-ratio: ${value}) and (foo: ${modifier})`]
  962. },
  963. { values: { '1/10': '1/10' } }
  964. )
  965. },
  966. ],
  967. }
  968. let input = css`
  969. @tailwind utilities;
  970. `
  971. return run(input, config).then((result) => {
  972. expect(result.css).toMatchFormattedCss(css`
  973. @media (min-aspect-ratio: 1 / 10) and (foo: 20) {
  974. .ar-1\/10\/20\:flex {
  975. display: flex;
  976. }
  977. }
  978. `)
  979. })
  980. })
  981. test('arbitrary variant selectors should not re-order scrollbar pseudo classes', async () => {
  982. let config = {
  983. content: [
  984. {
  985. raw: html`
  986. <div class="[&::-webkit-scrollbar:hover]:underline" />
  987. <div class="[&::-webkit-scrollbar-button:hover]:underline" />
  988. <div class="[&::-webkit-scrollbar-thumb:hover]:underline" />
  989. <div class="[&::-webkit-scrollbar-track:hover]:underline" />
  990. <div class="[&::-webkit-scrollbar-track-piece:hover]:underline" />
  991. <div class="[&::-webkit-scrollbar-corner:hover]:underline" />
  992. <div class="[&::-webkit-resizer:hover]:underline" />
  993. `,
  994. },
  995. ],
  996. corePlugins: { preflight: false },
  997. }
  998. let input = css`
  999. @tailwind utilities;
  1000. `
  1001. let result = await run(input, config)
  1002. expect(result.css).toMatchFormattedCss(css`
  1003. .\[\&\:\:-webkit-resizer\:hover\]\:underline::-webkit-resizer:hover {
  1004. text-decoration-line: underline;
  1005. }
  1006. .\[\&\:\:-webkit-scrollbar-button\:hover\]\:underline::-webkit-scrollbar-button:hover {
  1007. text-decoration-line: underline;
  1008. }
  1009. .\[\&\:\:-webkit-scrollbar-corner\:hover\]\:underline::-webkit-scrollbar-corner:hover {
  1010. text-decoration-line: underline;
  1011. }
  1012. .\[\&\:\:-webkit-scrollbar-thumb\:hover\]\:underline::-webkit-scrollbar-thumb:hover {
  1013. text-decoration-line: underline;
  1014. }
  1015. .\[\&\:\:-webkit-scrollbar-track-piece\:hover\]\:underline::-webkit-scrollbar-track-piece:hover {
  1016. text-decoration-line: underline;
  1017. }
  1018. .\[\&\:\:-webkit-scrollbar-track\:hover\]\:underline::-webkit-scrollbar-track:hover {
  1019. text-decoration-line: underline;
  1020. }
  1021. .\[\&\:\:-webkit-scrollbar\:hover\]\:underline::-webkit-scrollbar:hover {
  1022. text-decoration-line: underline;
  1023. }
  1024. `)
  1025. })
  1026. test('stacking dark and rtl variants', async () => {
  1027. let config = {
  1028. darkMode: 'class',
  1029. content: [
  1030. {
  1031. raw: html`<div class="dark:rtl:italic" />`,
  1032. },
  1033. ],
  1034. corePlugins: { preflight: false },
  1035. }
  1036. let input = css`
  1037. @tailwind utilities;
  1038. `
  1039. let result = await run(input, config)
  1040. expect(result.css).toMatchFormattedCss(css`
  1041. :is(.dark :is([dir='rtl'] .dark\:rtl\:italic)) {
  1042. font-style: italic;
  1043. }
  1044. `)
  1045. })
  1046. test('stacking dark and rtl variants with pseudo elements', async () => {
  1047. let config = {
  1048. darkMode: 'class',
  1049. content: [
  1050. {
  1051. raw: html`<div class="dark:rtl:placeholder:italic" />`,
  1052. },
  1053. ],
  1054. corePlugins: { preflight: false },
  1055. }
  1056. let input = css`
  1057. @tailwind utilities;
  1058. `
  1059. let result = await run(input, config)
  1060. expect(result.css).toMatchFormattedCss(css`
  1061. :is(.dark :is([dir='rtl'] .dark\:rtl\:placeholder\:italic))::placeholder {
  1062. font-style: italic;
  1063. }
  1064. `)
  1065. })