prefix.test.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. import { run, html, css, defaults } from './util/run'
  2. test('prefix', () => {
  3. let config = {
  4. prefix: 'tw-',
  5. darkMode: 'class',
  6. content: [
  7. {
  8. raw: html`
  9. <div class="tw--ml-4"></div>
  10. <div class="md:tw--ml-5"></div>
  11. <div class="md:hover:tw--ml-6"></div>
  12. <div class="tw-container"></div>
  13. <div class="btn-no-prefix"></div>
  14. <div class="tw-btn-prefix"></div>
  15. <div class="tw-custom-util-prefix"></div>
  16. <div class="custom-util-no-prefix"></div>
  17. <div class="custom-component"></div>
  18. <div class="tw-custom-component-prefix"></div>
  19. <div class="custom-component-no-prefix"></div>
  20. <div class="tw-font-bold"></div>
  21. <div class="md:hover:tw-text-right"></div>
  22. <div class="motion-safe:hover:tw-text-center"></div>
  23. <div class="dark:focus:tw-text-left"></div>
  24. <div class="dark:tw-bg-[rgb(255,0,0)]"></div>
  25. <div class="group-hover:focus-within:tw-text-left"></div>
  26. <div class="rtl:active:tw-text-center"></div>
  27. <div class="tw-animate-ping"></div>
  28. <div class="tw-animate-spin"></div>
  29. `,
  30. },
  31. ],
  32. corePlugins: { preflight: false },
  33. theme: {
  34. animation: {
  35. spin: 'spin 1s linear infinite',
  36. ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
  37. },
  38. keyframes: {
  39. spin: { to: { transform: 'rotate(360deg)' } },
  40. },
  41. },
  42. plugins: [
  43. function ({ addComponents, addUtilities }) {
  44. addComponents({
  45. '.btn-prefix': {
  46. button: 'yes',
  47. },
  48. })
  49. addComponents(
  50. {
  51. '.btn-no-prefix': {
  52. button: 'yes',
  53. },
  54. },
  55. { respectPrefix: false }
  56. )
  57. addUtilities({
  58. '.custom-util-prefix': {
  59. button: 'no',
  60. },
  61. })
  62. addUtilities(
  63. {
  64. '.custom-util-no-prefix': {
  65. button: 'no',
  66. },
  67. },
  68. { respectPrefix: false }
  69. )
  70. },
  71. ],
  72. }
  73. let input = css`
  74. @tailwind base;
  75. @tailwind components;
  76. @layer components {
  77. .custom-component {
  78. @apply tw-font-bold dark:group-hover:tw-font-normal;
  79. }
  80. }
  81. @tailwind utilities;
  82. @layer utilities {
  83. .custom-utility {
  84. foo: bar;
  85. }
  86. }
  87. `
  88. return run(input, config).then((result) => {
  89. expect(result.css).toMatchFormattedCss(css`
  90. ${defaults}
  91. .tw-container {
  92. width: 100%;
  93. }
  94. @media (min-width: 640px) {
  95. .tw-container {
  96. max-width: 640px;
  97. }
  98. }
  99. @media (min-width: 768px) {
  100. .tw-container {
  101. max-width: 768px;
  102. }
  103. }
  104. @media (min-width: 1024px) {
  105. .tw-container {
  106. max-width: 1024px;
  107. }
  108. }
  109. @media (min-width: 1280px) {
  110. .tw-container {
  111. max-width: 1280px;
  112. }
  113. }
  114. @media (min-width: 1536px) {
  115. .tw-container {
  116. max-width: 1536px;
  117. }
  118. }
  119. .tw-btn-prefix,
  120. .btn-no-prefix {
  121. button: yes;
  122. }
  123. .custom-component {
  124. font-weight: 700;
  125. }
  126. :is(.tw-dark .tw-group:hover .custom-component) {
  127. font-weight: 400;
  128. }
  129. .tw--ml-4 {
  130. margin-left: -1rem;
  131. }
  132. .tw-animate-ping {
  133. animation: 1s cubic-bezier(0, 0, 0.2, 1) infinite ping;
  134. }
  135. @keyframes tw-spin {
  136. to {
  137. transform: rotate(360deg);
  138. }
  139. }
  140. .tw-animate-spin {
  141. animation: 1s linear infinite tw-spin;
  142. }
  143. .tw-font-bold {
  144. font-weight: 700;
  145. }
  146. .tw-custom-util-prefix,
  147. .custom-util-no-prefix {
  148. button: no;
  149. }
  150. .tw-group:hover .group-hover\:focus-within\:tw-text-left:focus-within {
  151. text-align: left;
  152. }
  153. :is([dir='rtl'] .rtl\:active\:tw-text-center:active) {
  154. text-align: center;
  155. }
  156. @media (prefers-reduced-motion: no-preference) {
  157. .motion-safe\:hover\:tw-text-center:hover {
  158. text-align: center;
  159. }
  160. }
  161. :is(.tw-dark .dark\:tw-bg-\[rgb\(255\,0\,0\)\]) {
  162. --tw-bg-opacity: 1;
  163. background-color: rgb(255 0 0 / var(--tw-bg-opacity));
  164. }
  165. :is(.tw-dark .dark\:focus\:tw-text-left:focus) {
  166. text-align: left;
  167. }
  168. @media (min-width: 768px) {
  169. .md\:tw--ml-5 {
  170. margin-left: -1.25rem;
  171. }
  172. .md\:hover\:tw--ml-6:hover {
  173. margin-left: -1.5rem;
  174. }
  175. .md\:hover\:tw-text-right:hover {
  176. text-align: right;
  177. }
  178. }
  179. `)
  180. })
  181. })
  182. test('negative values: marker before prefix', async () => {
  183. let config = {
  184. prefix: 'tw-',
  185. content: [{ raw: html`<div class="-tw-top-1"></div>` }],
  186. corePlugins: { preflight: false },
  187. }
  188. let input = css`
  189. @tailwind utilities;
  190. `
  191. await run(input, config)
  192. const result = await run(input, config)
  193. expect(result.css).toMatchFormattedCss(css`
  194. .-tw-top-1 {
  195. top: -0.25rem;
  196. }
  197. `)
  198. })
  199. test('negative values: marker after prefix', async () => {
  200. let config = {
  201. prefix: 'tw-',
  202. content: [{ raw: html`<div class="tw--top-1"></div>` }],
  203. corePlugins: { preflight: false },
  204. }
  205. let input = css`
  206. @tailwind utilities;
  207. `
  208. await run(input, config)
  209. const result = await run(input, config)
  210. expect(result.css).toMatchFormattedCss(css`
  211. .tw--top-1 {
  212. top: -0.25rem;
  213. }
  214. `)
  215. })
  216. test('negative values: marker before prefix and arbitrary value', async () => {
  217. let config = {
  218. prefix: 'tw-',
  219. content: [{ raw: html`<div class="-tw-top-[1px]"></div>` }],
  220. corePlugins: { preflight: false },
  221. }
  222. let input = css`
  223. @tailwind utilities;
  224. `
  225. await run(input, config)
  226. const result = await run(input, config)
  227. expect(result.css).toMatchFormattedCss(css`
  228. .-tw-top-\[1px\] {
  229. top: -1px;
  230. }
  231. `)
  232. })
  233. test('negative values: marker after prefix and arbitrary value', async () => {
  234. let config = {
  235. prefix: 'tw-',
  236. content: [{ raw: html`<div class="tw--top-[1px]"></div>` }],
  237. corePlugins: { preflight: false },
  238. }
  239. let input = css`
  240. @tailwind utilities;
  241. `
  242. await run(input, config)
  243. const result = await run(input, config)
  244. expect(result.css).toMatchFormattedCss(css`
  245. .tw--top-\[1px\] {
  246. top: -1px;
  247. }
  248. `)
  249. })
  250. test('negative values: no marker and arbitrary value', async () => {
  251. let config = {
  252. prefix: 'tw-',
  253. content: [{ raw: html`<div class="tw-top-[-1px]"></div>` }],
  254. corePlugins: { preflight: false },
  255. }
  256. let input = css`
  257. @tailwind utilities;
  258. `
  259. await run(input, config)
  260. const result = await run(input, config)
  261. expect(result.css).toMatchFormattedCss(css`
  262. .tw-top-\[-1px\] {
  263. top: -1px;
  264. }
  265. `)
  266. })
  267. test('negative values: variant versions', async () => {
  268. let config = {
  269. prefix: 'tw-',
  270. content: [
  271. {
  272. raw: html`
  273. <div class="hover:-tw-top-1 hover:tw--top-1"></div>
  274. <div class="hover:-tw-top-[1px] hover:tw--top-[1px]"></div>
  275. <div class="hover:tw-top-[-1px]"></div>
  276. <!-- this one should not generate anything -->
  277. <div class="-hover:tw-top-1"></div>
  278. `,
  279. },
  280. ],
  281. corePlugins: { preflight: false },
  282. }
  283. let input = css`
  284. @tailwind utilities;
  285. `
  286. await run(input, config)
  287. const result = await run(input, config)
  288. expect(result.css).toMatchFormattedCss(css`
  289. .hover\:-tw-top-1:hover {
  290. top: -0.25rem;
  291. }
  292. .hover\:-tw-top-\[1px\]:hover {
  293. top: -1px;
  294. }
  295. .hover\:tw--top-1:hover {
  296. top: -0.25rem;
  297. }
  298. .hover\:tw--top-\[1px\]:hover,
  299. .hover\:tw-top-\[-1px\]:hover {
  300. top: -1px;
  301. }
  302. `)
  303. })
  304. test('negative values: prefix and apply', async () => {
  305. let config = {
  306. prefix: 'tw-',
  307. content: [{ raw: html`` }],
  308. corePlugins: { preflight: false },
  309. }
  310. let input = css`
  311. @tailwind utilities;
  312. .a {
  313. @apply hover:tw--top-1;
  314. }
  315. .b {
  316. @apply hover:-tw-top-1;
  317. }
  318. .c {
  319. @apply hover:-tw-top-[1px];
  320. }
  321. .d {
  322. @apply hover:tw--top-[1px];
  323. }
  324. .e {
  325. @apply hover:tw-top-[-1px];
  326. }
  327. `
  328. await run(input, config)
  329. const result = await run(input, config)
  330. expect(result.css).toMatchFormattedCss(css`
  331. .a:hover,
  332. .b:hover {
  333. top: -0.25rem;
  334. }
  335. .c:hover,
  336. .d:hover,
  337. .e:hover {
  338. top: -1px;
  339. }
  340. `)
  341. })
  342. test('negative values: prefix in the safelist', async () => {
  343. let config = {
  344. prefix: 'tw-',
  345. safelist: [{ pattern: /-tw-top-1/g }, { pattern: /tw--top-1/g }],
  346. theme: {
  347. inset: {
  348. 1: '0.25rem',
  349. },
  350. },
  351. content: [{ raw: html`` }],
  352. corePlugins: { preflight: false },
  353. }
  354. let input = css`
  355. @tailwind utilities;
  356. `
  357. await run(input, config)
  358. const result = await run(input, config)
  359. expect(result.css).toMatchFormattedCss(css`
  360. .-tw-top-1,
  361. .tw--top-1 {
  362. top: -0.25rem;
  363. }
  364. `)
  365. })
  366. test('prefix with negative values and variants in the safelist', async () => {
  367. let config = {
  368. prefix: 'tw-',
  369. safelist: [
  370. { pattern: /-tw-top-1/, variants: ['hover', 'sm:hover'] },
  371. { pattern: /tw--top-1/, variants: ['hover', 'sm:hover'] },
  372. ],
  373. theme: {
  374. inset: {
  375. 1: '0.25rem',
  376. },
  377. },
  378. content: [{ raw: html`` }],
  379. corePlugins: { preflight: false },
  380. }
  381. let input = css`
  382. @tailwind utilities;
  383. `
  384. await run(input, config)
  385. const result = await run(input, config)
  386. expect(result.css).toMatchFormattedCss(css`
  387. .-tw-top-1,
  388. .tw--top-1,
  389. .hover\:-tw-top-1:hover,
  390. .hover\:tw--top-1:hover {
  391. top: -0.25rem;
  392. }
  393. @media (min-width: 640px) {
  394. .sm\:hover\:-tw-top-1:hover,
  395. .sm\:hover\:tw--top-1:hover {
  396. top: -0.25rem;
  397. }
  398. }
  399. `)
  400. })
  401. test('prefix does not detect and generate unnecessary classes', async () => {
  402. let config = {
  403. prefix: 'tw-_',
  404. content: [{ raw: html`-aaa-filter aaaa-table aaaa-hidden` }],
  405. corePlugins: { preflight: false },
  406. }
  407. let input = css`
  408. @tailwind utilities;
  409. `
  410. const result = await run(input, config)
  411. expect(result.css).toMatchFormattedCss(css``)
  412. })
  413. test('supports prefixed utilities using arbitrary values', async () => {
  414. let config = {
  415. prefix: 'tw-',
  416. content: [{ raw: html`foo` }],
  417. corePlugins: { preflight: false },
  418. }
  419. let input = css`
  420. .foo {
  421. @apply tw-text-[color:rgb(var(--button-background,var(--primary-button-background)))];
  422. @apply tw-ease-[cubic-bezier(0.77,0,0.175,1)];
  423. @apply tw-rounded-[min(4px,var(--input-border-radius))];
  424. }
  425. `
  426. const result = await run(input, config)
  427. expect(result.css).toMatchFormattedCss(css`
  428. .foo {
  429. color: rgb(var(--button-background, var(--primary-button-background)));
  430. border-radius: min(4px, var(--input-border-radius));
  431. transition-timing-function: cubic-bezier(0.77, 0, 0.175, 1);
  432. }
  433. `)
  434. })
  435. test('supports non-word prefixes (1)', async () => {
  436. let config = {
  437. prefix: '@',
  438. content: [
  439. {
  440. raw: html`
  441. <div class="@underline"></div>
  442. <div class="@bg-black"></div>
  443. <div class="@[color:red]"></div>
  444. <div class="hover:before:@content-['Hovering']"></div>
  445. <div class="my-utility"></div>
  446. <div class="foo"></div>
  447. <!-- these won't be detected -->
  448. <div class="overline"></div>
  449. `,
  450. },
  451. ],
  452. corePlugins: { preflight: false },
  453. }
  454. let input = css`
  455. @tailwind utilities;
  456. @layer utilities {
  457. .my-utility {
  458. color: orange;
  459. }
  460. }
  461. .foo {
  462. @apply @text-white;
  463. @apply [background-color:red];
  464. }
  465. `
  466. const result = await run(input, config)
  467. expect(result.css).toMatchFormattedCss(css`
  468. .\@bg-black {
  469. --tw-bg-opacity: 1;
  470. background-color: rgb(0 0 0 / var(--tw-bg-opacity));
  471. }
  472. .\@underline {
  473. text-decoration-line: underline;
  474. }
  475. .my-utility {
  476. color: orange;
  477. }
  478. .foo {
  479. --tw-text-opacity: 1;
  480. color: rgb(255 255 255 / var(--tw-text-opacity));
  481. background-color: red;
  482. }
  483. .hover\:before\:\@content-\[\'Hovering\'\]:hover:before {
  484. --tw-content: 'Hovering';
  485. content: var(--tw-content);
  486. }
  487. `)
  488. })
  489. test('supports non-word prefixes (2)', async () => {
  490. let config = {
  491. prefix: '@]$',
  492. content: [
  493. {
  494. raw: html`
  495. <div class="@]$underline"></div>
  496. <div class="@]$bg-black"></div>
  497. <div class="@]$[color:red]"></div>
  498. <div class="hover:before:@]$content-['Hovering']"></div>
  499. <div class="my-utility"></div>
  500. <div class="foo"></div>
  501. <!-- these won't be detected -->
  502. <div class="overline"></div>
  503. `,
  504. },
  505. ],
  506. corePlugins: { preflight: false },
  507. }
  508. let input = css`
  509. @tailwind utilities;
  510. @layer utilities {
  511. .my-utility {
  512. color: orange;
  513. }
  514. }
  515. .foo {
  516. @apply @]$text-white;
  517. @apply [background-color:red];
  518. }
  519. `
  520. const result = await run(input, config)
  521. // TODO: The class `.hover\:before\:\@\]\$content-\[\'Hovering\'\]:hover::before` is not generated
  522. // This happens because of the parenthesis/brace/bracket clipping performed on candidates
  523. expect(result.css).toMatchFormattedCss(css`
  524. .\@\]\$bg-black {
  525. --tw-bg-opacity: 1;
  526. background-color: rgb(0 0 0 / var(--tw-bg-opacity));
  527. }
  528. .\@\]\$underline {
  529. text-decoration-line: underline;
  530. }
  531. .my-utility {
  532. color: orange;
  533. }
  534. .foo {
  535. --tw-text-opacity: 1;
  536. color: rgb(255 255 255 / var(--tw-text-opacity));
  537. background-color: red;
  538. }
  539. `)
  540. })
  541. test('does not prefix arbitrary group/peer classes', async () => {
  542. let config = {
  543. prefix: 'tw-',
  544. content: [
  545. {
  546. raw: html`
  547. <div class="tw-group tw-peer foo">
  548. <div class="group-[&.foo]:tw-flex"></div>
  549. </div>
  550. <div class="peer-[&.foo]:tw-flex"></div>
  551. `,
  552. },
  553. ],
  554. corePlugins: { preflight: false },
  555. }
  556. let input = css`
  557. @tailwind utilities;
  558. `
  559. const result = await run(input, config)
  560. expect(result.css).toMatchFormattedCss(css`
  561. .tw-group.foo .group-\[\&\.foo\]\:tw-flex,
  562. .tw-peer.foo ~ .peer-\[\&\.foo\]\:tw-flex {
  563. display: flex;
  564. }
  565. `)
  566. })