variants.test.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858
  1. import fs from 'fs'
  2. import path from 'path'
  3. import postcss from 'postcss'
  4. import { run, css, html, 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. let expectedPath = path.resolve(__dirname, './variants.test.css')
  18. let expected = fs.readFileSync(expectedPath, 'utf8')
  19. expect(result.css).toMatchFormattedCss(expected)
  20. })
  21. })
  22. test('order matters and produces different behaviour', () => {
  23. let config = {
  24. content: [
  25. {
  26. raw: html`
  27. <div class="hover:file:bg-pink-600"></div>
  28. <div class="file:hover:bg-pink-600"></div>
  29. `,
  30. },
  31. ],
  32. }
  33. return run('@tailwind utilities', config).then((result) => {
  34. return expect(result.css).toMatchFormattedCss(css`
  35. .hover\:file\:bg-pink-600::file-selector-button:hover {
  36. --tw-bg-opacity: 1;
  37. background-color: rgb(219 39 119 / var(--tw-bg-opacity));
  38. }
  39. .file\:hover\:bg-pink-600:hover::file-selector-button {
  40. --tw-bg-opacity: 1;
  41. background-color: rgb(219 39 119 / var(--tw-bg-opacity));
  42. }
  43. `)
  44. })
  45. })
  46. describe('custom advanced variants', () => {
  47. test('prose-headings usage on its own', () => {
  48. let config = {
  49. content: [
  50. {
  51. raw: html` <div class="prose-headings:text-center"></div> `,
  52. },
  53. ],
  54. plugins: [
  55. function ({ addVariant }) {
  56. addVariant('prose-headings', ':where(&) :is(h1, h2, h3, h4)')
  57. },
  58. ],
  59. }
  60. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  61. return expect(result.css).toMatchFormattedCss(css`
  62. :where(.prose-headings\:text-center) :is(h1, h2, h3, h4) {
  63. text-align: center;
  64. }
  65. `)
  66. })
  67. })
  68. test('prose-headings with another "simple" variant', () => {
  69. let config = {
  70. content: [
  71. {
  72. raw: html`
  73. <div class="hover:prose-headings:text-center"></div>
  74. <div class="prose-headings:hover:text-center"></div>
  75. `,
  76. },
  77. ],
  78. plugins: [
  79. function ({ addVariant }) {
  80. addVariant('prose-headings', ':where(&) :is(h1, h2, h3, h4)')
  81. },
  82. ],
  83. }
  84. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  85. return expect(result.css).toMatchFormattedCss(css`
  86. :where(.hover\:prose-headings\:text-center) :is(h1, h2, h3, h4):hover {
  87. text-align: center;
  88. }
  89. :where(.prose-headings\:hover\:text-center:hover) :is(h1, h2, h3, h4) {
  90. text-align: center;
  91. }
  92. `)
  93. })
  94. })
  95. test('prose-headings with another "complex" variant', () => {
  96. let config = {
  97. content: [
  98. {
  99. raw: html`
  100. <div class="group-hover:prose-headings:text-center"></div>
  101. <div class="prose-headings:group-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 utilities', config).then((result) => {
  112. return expect(result.css).toMatchFormattedCss(css`
  113. .group:hover :where(.group-hover\:prose-headings\:text-center) :is(h1, h2, h3, h4) {
  114. text-align: center;
  115. }
  116. :where(.group:hover .prose-headings\:group-hover\:text-center) :is(h1, h2, h3, h4) {
  117. text-align: center;
  118. }
  119. `)
  120. })
  121. })
  122. test('using variants with multi-class selectors', () => {
  123. let config = {
  124. content: [
  125. {
  126. raw: html` <div class="screen:parent screen:child"></div> `,
  127. },
  128. ],
  129. plugins: [
  130. function ({ addVariant, addComponents }) {
  131. addComponents({
  132. '.parent .child': {
  133. foo: 'bar',
  134. },
  135. })
  136. addVariant('screen', '@media screen')
  137. },
  138. ],
  139. }
  140. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  141. return expect(result.css).toMatchFormattedCss(css`
  142. @media screen {
  143. .screen\:parent .child {
  144. foo: bar;
  145. }
  146. .parent .screen\:child {
  147. foo: bar;
  148. }
  149. }
  150. `)
  151. })
  152. })
  153. test('using multiple classNames in your custom variant', () => {
  154. let config = {
  155. content: [
  156. {
  157. raw: html` <div class="my-variant:underline test"></div> `,
  158. },
  159. ],
  160. plugins: [
  161. function ({ addVariant }) {
  162. addVariant('my-variant', '&:where(.one, .two, .three)')
  163. },
  164. ],
  165. }
  166. let input = css`
  167. @tailwind components;
  168. @tailwind utilities;
  169. @layer components {
  170. .test {
  171. @apply my-variant:italic;
  172. }
  173. }
  174. `
  175. return run(input, config).then((result) => {
  176. return expect(result.css).toMatchFormattedCss(css`
  177. .test:where(.one, .two, .three) {
  178. font-style: italic;
  179. }
  180. .my-variant\:underline:where(.one, .two, .three) {
  181. text-decoration-line: underline;
  182. }
  183. `)
  184. })
  185. })
  186. test('variant format string must include at-rule or & (1)', async () => {
  187. let config = {
  188. content: [
  189. {
  190. raw: html` <div class="wtf-bbq:text-center"></div> `,
  191. },
  192. ],
  193. plugins: [
  194. function ({ addVariant }) {
  195. addVariant('wtf-bbq', 'lol')
  196. },
  197. ],
  198. }
  199. await expect(run('@tailwind components;@tailwind utilities', config)).rejects.toThrowError(
  200. "Your custom variant `wtf-bbq` has an invalid format string. Make sure it's an at-rule or contains a `&` placeholder."
  201. )
  202. })
  203. test('variant format string must include at-rule or & (2)', async () => {
  204. let config = {
  205. content: [
  206. {
  207. raw: html` <div class="wtf-bbq:text-center"></div> `,
  208. },
  209. ],
  210. plugins: [
  211. function ({ addVariant }) {
  212. addVariant('wtf-bbq', () => 'lol')
  213. },
  214. ],
  215. }
  216. await expect(run('@tailwind components;@tailwind utilities', config)).rejects.toThrowError(
  217. "Your custom variant `wtf-bbq` has an invalid format string. Make sure it's an at-rule or contains a `&` placeholder."
  218. )
  219. })
  220. })
  221. test('stacked peer variants', async () => {
  222. let config = {
  223. content: [{ raw: 'peer-disabled:peer-focus:peer-hover:border-blue-500' }],
  224. corePlugins: { preflight: false },
  225. }
  226. let input = css`
  227. @tailwind base;
  228. @tailwind components;
  229. @tailwind utilities;
  230. `
  231. let expected = css`
  232. .peer:disabled:focus:hover ~ .peer-disabled\:peer-focus\:peer-hover\:border-blue-500 {
  233. --tw-border-opacity: 1;
  234. border-color: rgb(59 130 246 / var(--tw-border-opacity));
  235. }
  236. `
  237. let result = await run(input, config)
  238. expect(result.css).toIncludeCss(expected)
  239. })
  240. it('should properly handle keyframes with multiple variants', async () => {
  241. let config = {
  242. content: [
  243. {
  244. raw: 'animate-spin hover:animate-spin focus:animate-spin hover:animate-bounce focus:animate-bounce',
  245. },
  246. ],
  247. }
  248. let input = css`
  249. @tailwind components;
  250. @tailwind utilities;
  251. `
  252. let result = await run(input, config)
  253. expect(result.css).toMatchFormattedCss(css`
  254. @keyframes spin {
  255. to {
  256. transform: rotate(360deg);
  257. }
  258. }
  259. .animate-spin {
  260. animation: spin 1s linear infinite;
  261. }
  262. @keyframes spin {
  263. to {
  264. transform: rotate(360deg);
  265. }
  266. }
  267. .hover\:animate-spin:hover {
  268. animation: spin 1s linear infinite;
  269. }
  270. @keyframes bounce {
  271. 0%,
  272. 100% {
  273. transform: translateY(-25%);
  274. animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
  275. }
  276. 50% {
  277. transform: none;
  278. animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
  279. }
  280. }
  281. .hover\:animate-bounce:hover {
  282. animation: bounce 1s infinite;
  283. }
  284. @keyframes spin {
  285. to {
  286. transform: rotate(360deg);
  287. }
  288. }
  289. .focus\:animate-spin:focus {
  290. animation: spin 1s linear infinite;
  291. }
  292. @keyframes bounce {
  293. 0%,
  294. 100% {
  295. transform: translateY(-25%);
  296. animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
  297. }
  298. 50% {
  299. transform: none;
  300. animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
  301. }
  302. }
  303. .focus\:animate-bounce:focus {
  304. animation: bounce 1s infinite;
  305. }
  306. `)
  307. })
  308. test('custom addVariant with more complex media query params', () => {
  309. let config = {
  310. content: [
  311. {
  312. raw: html` <div class="magic:text-center"></div> `,
  313. },
  314. ],
  315. plugins: [
  316. function ({ addVariant }) {
  317. addVariant('magic', '@media screen and (max-wdith: 600px)')
  318. },
  319. ],
  320. }
  321. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  322. return expect(result.css).toMatchFormattedCss(css`
  323. @media screen and (max-wdith: 600px) {
  324. .magic\:text-center {
  325. text-align: center;
  326. }
  327. }
  328. `)
  329. })
  330. })
  331. test('custom addVariant with nested media & format shorthand', () => {
  332. let config = {
  333. content: [
  334. {
  335. raw: html` <div class="magic:text-center"></div> `,
  336. },
  337. ],
  338. plugins: [
  339. function ({ addVariant }) {
  340. addVariant('magic', '@supports (hover: hover) { @media print { &:disabled } }')
  341. },
  342. ],
  343. }
  344. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  345. return expect(result.css).toMatchFormattedCss(css`
  346. @supports (hover: hover) {
  347. @media print {
  348. .magic\:text-center:disabled {
  349. text-align: center;
  350. }
  351. }
  352. }
  353. `)
  354. })
  355. })
  356. test('before and after variants are a bit special, and forced to the end', () => {
  357. let config = {
  358. content: [
  359. {
  360. raw: html`
  361. <div class="before:hover:text-center"></div>
  362. <div class="hover:before:text-center"></div>
  363. `,
  364. },
  365. ],
  366. plugins: [],
  367. }
  368. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  369. return expect(result.css).toMatchFormattedCss(css`
  370. .before\:hover\:text-center:hover::before {
  371. content: var(--tw-content);
  372. text-align: center;
  373. }
  374. .hover\:before\:text-center:hover::before {
  375. content: var(--tw-content);
  376. text-align: center;
  377. }
  378. `)
  379. })
  380. })
  381. test('before and after variants are a bit special, and forced to the end (2)', () => {
  382. let config = {
  383. content: [
  384. {
  385. raw: html`
  386. <div class="before:prose-headings:text-center"></div>
  387. <div class="prose-headings:before:text-center"></div>
  388. `,
  389. },
  390. ],
  391. plugins: [
  392. function ({ addVariant }) {
  393. addVariant('prose-headings', ':where(&) :is(h1, h2, h3, h4)')
  394. },
  395. ],
  396. }
  397. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  398. return expect(result.css).toMatchFormattedCss(css`
  399. :where(.before\:prose-headings\:text-center) :is(h1, h2, h3, h4)::before {
  400. content: var(--tw-content);
  401. text-align: center;
  402. }
  403. :where(.prose-headings\:before\:text-center) :is(h1, h2, h3, h4)::before {
  404. content: var(--tw-content);
  405. text-align: center;
  406. }
  407. `)
  408. })
  409. })
  410. test('returning non-strings and non-selectors in addVariant', () => {
  411. /** @type {import('../types/config').Config} */
  412. let config = {
  413. content: [
  414. {
  415. raw: html`
  416. <div class="peer-aria-expanded:text-center"></div>
  417. <div class="peer-aria-expanded-2:text-center"></div>
  418. `,
  419. },
  420. ],
  421. plugins: [
  422. function ({ addVariant, e }) {
  423. addVariant('peer-aria-expanded', ({ modifySelectors, separator }) =>
  424. // Returning anything other string | string[] | undefined here is not supported
  425. // But we're trying to be lenient here and just throw it out
  426. modifySelectors(
  427. ({ className }) =>
  428. `.peer[aria-expanded="true"] ~ .${e(`peer-aria-expanded${separator}${className}`)}`
  429. )
  430. )
  431. addVariant('peer-aria-expanded-2', ({ modifySelectors, separator }) => {
  432. let nodes = modifySelectors(
  433. ({ className }) => `.${e(`peer-aria-expanded-2${separator}${className}`)}`
  434. )
  435. return [
  436. // Returning anything other than strings here is not supported
  437. // But we're trying to be lenient here and just throw it out
  438. nodes,
  439. '.peer[aria-expanded="false"] ~ &',
  440. ]
  441. })
  442. },
  443. ],
  444. }
  445. return run('@tailwind components;@tailwind utilities', config).then((result) => {
  446. return expect(result.css).toMatchFormattedCss(css`
  447. .peer[aria-expanded='true'] ~ .peer-aria-expanded\:text-center {
  448. text-align: center;
  449. }
  450. .peer[aria-expanded='false'] ~ .peer-aria-expanded-2\:text-center {
  451. text-align: center;
  452. }
  453. `)
  454. })
  455. })
  456. it('should not generate variants of user css if it is not inside a layer', () => {
  457. let config = {
  458. content: [{ raw: html`<div class="hover:foo"></div>` }],
  459. plugins: [],
  460. }
  461. let input = css`
  462. @tailwind components;
  463. @tailwind utilities;
  464. .foo {
  465. color: red;
  466. }
  467. `
  468. return run(input, config).then((result) => {
  469. return expect(result.css).toMatchFormattedCss(css`
  470. .foo {
  471. color: red;
  472. }
  473. `)
  474. })
  475. })
  476. it('should be possible to use responsive modifiers that are defined with special characters', () => {
  477. let config = {
  478. content: [{ raw: html`<div class="<sm:underline"></div>` }],
  479. theme: {
  480. screens: {
  481. '<sm': { max: '399px' },
  482. },
  483. },
  484. plugins: [],
  485. }
  486. return run('@tailwind utilities', config).then((result) => {
  487. return expect(result.css).toMatchFormattedCss(css`
  488. @media (max-width: 399px) {
  489. .\<sm\:underline {
  490. text-decoration-line: underline;
  491. }
  492. }
  493. `)
  494. })
  495. })
  496. it('including just the base layer should not produce variants', () => {
  497. let config = {
  498. content: [{ raw: html`<div class="sm:container sm:underline"></div>` }],
  499. corePlugins: { preflight: false },
  500. }
  501. return run('@tailwind base', config).then((result) => {
  502. return expect(result.css).toMatchFormattedCss(
  503. css`
  504. ${defaults}
  505. `
  506. )
  507. })
  508. })
  509. it('variants for components should not be produced in a file without a components layer', () => {
  510. let config = {
  511. content: [{ raw: html`<div class="sm:container sm:underline"></div>` }],
  512. }
  513. return run('@tailwind utilities', config).then((result) => {
  514. return expect(result.css).toMatchFormattedCss(css`
  515. @media (min-width: 640px) {
  516. .sm\:underline {
  517. text-decoration-line: underline;
  518. }
  519. }
  520. `)
  521. })
  522. })
  523. it('variants for utilities should not be produced in a file without a utilities layer', () => {
  524. let config = {
  525. content: [{ raw: html`<div class="sm:container sm:underline"></div>` }],
  526. }
  527. return run('@tailwind components', config).then((result) => {
  528. return expect(result.css).toMatchFormattedCss(css`
  529. @media (min-width: 640px) {
  530. .sm\:container {
  531. width: 100%;
  532. }
  533. @media (min-width: 640px) {
  534. .sm\:container {
  535. max-width: 640px;
  536. }
  537. }
  538. @media (min-width: 768px) {
  539. .sm\:container {
  540. max-width: 768px;
  541. }
  542. }
  543. @media (min-width: 1024px) {
  544. .sm\:container {
  545. max-width: 1024px;
  546. }
  547. }
  548. @media (min-width: 1280px) {
  549. .sm\:container {
  550. max-width: 1280px;
  551. }
  552. }
  553. @media (min-width: 1536px) {
  554. .sm\:container {
  555. max-width: 1536px;
  556. }
  557. }
  558. }
  559. `)
  560. })
  561. })
  562. test('The visited variant removes opacity support', () => {
  563. let config = {
  564. content: [
  565. {
  566. raw: html`
  567. <a class="visited:border-red-500 visited:bg-red-500 visited:text-red-500"
  568. >Look, it's a link!</a
  569. >
  570. `,
  571. },
  572. ],
  573. plugins: [],
  574. }
  575. return run('@tailwind utilities', config).then((result) => {
  576. return expect(result.css).toMatchFormattedCss(css`
  577. .visited\:border-red-500:visited {
  578. border-color: rgb(239 68 68);
  579. }
  580. .visited\:bg-red-500:visited {
  581. background-color: rgb(239 68 68);
  582. }
  583. .visited\:text-red-500:visited {
  584. color: rgb(239 68 68);
  585. }
  586. `)
  587. })
  588. })
  589. it('appends variants to the correct place when using postcss documents', () => {
  590. let config = {
  591. content: [{ raw: html`<div class="underline sm:underline"></div>` }],
  592. plugins: [],
  593. corePlugins: { preflight: false },
  594. }
  595. const doc = postcss.document()
  596. doc.append(postcss.parse(`a {}`))
  597. doc.append(postcss.parse(`@tailwind base`))
  598. doc.append(postcss.parse(`@tailwind utilities`))
  599. doc.append(postcss.parse(`b {}`))
  600. const result = doc.toResult()
  601. return run(result, config).then((result) => {
  602. return expect(result.css).toMatchFormattedCss(css`
  603. a {
  604. }
  605. ${defaults}
  606. .underline {
  607. text-decoration-line: underline;
  608. }
  609. @media (min-width: 640px) {
  610. .sm\:underline {
  611. text-decoration-line: underline;
  612. }
  613. }
  614. b {
  615. }
  616. `)
  617. })
  618. })
  619. it('variants support multiple, grouped selectors (html)', () => {
  620. let config = {
  621. content: [{ raw: html`<div class="sm:base1 sm:base2"></div>` }],
  622. plugins: [],
  623. corePlugins: { preflight: false },
  624. }
  625. let input = css`
  626. @tailwind utilities;
  627. @layer utilities {
  628. .base1 .foo,
  629. .base1 .bar {
  630. color: red;
  631. }
  632. .base2 .bar .base2-foo {
  633. color: red;
  634. }
  635. }
  636. `
  637. return run(input, config).then((result) => {
  638. return expect(result.css).toMatchFormattedCss(css`
  639. @media (min-width: 640px) {
  640. .sm\:base1 .foo,
  641. .sm\:base1 .bar {
  642. color: red;
  643. }
  644. .sm\:base2 .bar .base2-foo {
  645. color: red;
  646. }
  647. }
  648. `)
  649. })
  650. })
  651. it('variants support multiple, grouped selectors (apply)', () => {
  652. let config = {
  653. content: [{ raw: html`<div class="baz"></div>` }],
  654. plugins: [],
  655. corePlugins: { preflight: false },
  656. }
  657. let input = css`
  658. @tailwind utilities;
  659. @layer utilities {
  660. .base .foo,
  661. .base .bar {
  662. color: red;
  663. }
  664. }
  665. .baz {
  666. @apply sm:base;
  667. }
  668. `
  669. return run(input, config).then((result) => {
  670. return expect(result.css).toMatchFormattedCss(css`
  671. @media (min-width: 640px) {
  672. .baz .foo,
  673. .baz .bar {
  674. color: red;
  675. }
  676. }
  677. `)
  678. })
  679. })
  680. it('variants only picks the used selectors in a group (html)', () => {
  681. let config = {
  682. content: [{ raw: html`<div class="sm:b"></div>` }],
  683. plugins: [],
  684. corePlugins: { preflight: false },
  685. }
  686. let input = css`
  687. @tailwind utilities;
  688. @layer utilities {
  689. .a,
  690. .b {
  691. color: red;
  692. }
  693. }
  694. `
  695. return run(input, config).then((result) => {
  696. return expect(result.css).toMatchFormattedCss(css`
  697. @media (min-width: 640px) {
  698. .sm\:b {
  699. color: red;
  700. }
  701. }
  702. `)
  703. })
  704. })
  705. it('variants only picks the used selectors in a group (apply)', () => {
  706. let config = {
  707. content: [{ raw: html`<div class="baz"></div>` }],
  708. plugins: [],
  709. corePlugins: { preflight: false },
  710. }
  711. let input = css`
  712. @tailwind utilities;
  713. @layer utilities {
  714. .a,
  715. .b {
  716. color: red;
  717. }
  718. }
  719. .baz {
  720. @apply sm:b;
  721. }
  722. `
  723. return run(input, config).then((result) => {
  724. return expect(result.css).toMatchFormattedCss(css`
  725. @media (min-width: 640px) {
  726. .baz {
  727. color: red;
  728. }
  729. }
  730. `)
  731. })
  732. })
  733. test('hoverOnlyWhenSupported adds hover and pointer media features by default', () => {
  734. let config = {
  735. future: {
  736. hoverOnlyWhenSupported: true,
  737. },
  738. content: [
  739. { raw: html`<div class="hover:underline group-hover:underline peer-hover:underline"></div>` },
  740. ],
  741. corePlugins: { preflight: false },
  742. }
  743. let input = css`
  744. @tailwind base;
  745. @tailwind components;
  746. @tailwind utilities;
  747. `
  748. return run(input, config).then((result) => {
  749. expect(result.css).toMatchFormattedCss(css`
  750. ${defaults}
  751. @media (hover: hover) and (pointer: fine) {
  752. .hover\:underline:hover {
  753. text-decoration-line: underline;
  754. }
  755. .group:hover .group-hover\:underline {
  756. text-decoration-line: underline;
  757. }
  758. .peer:hover ~ .peer-hover\:underline {
  759. text-decoration-line: underline;
  760. }
  761. }
  762. `)
  763. })
  764. })