arbitrary-values.test.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. import fs from 'fs'
  2. import path from 'path'
  3. import { run, html, css } from './util/run'
  4. test('arbitrary values', () => {
  5. let config = {
  6. content: [path.resolve(__dirname, './arbitrary-values.test.html')],
  7. }
  8. return run('@tailwind utilities', config).then((result) => {
  9. let expectedPath = path.resolve(__dirname, './arbitrary-values.test.css')
  10. let expected = fs.readFileSync(expectedPath, 'utf8')
  11. expect(result.css).toMatchFormattedCss(expected)
  12. })
  13. })
  14. it('should be possible to differentiate between decoration utilities', () => {
  15. let config = {
  16. content: [
  17. {
  18. raw: html` <div class="decoration-[#ccc] decoration-[3px]"></div> `,
  19. },
  20. ],
  21. }
  22. return run('@tailwind utilities', config).then((result) => {
  23. return expect(result.css).toMatchFormattedCss(css`
  24. .decoration-\[\#ccc\] {
  25. text-decoration-color: #ccc;
  26. }
  27. .decoration-\[3px\] {
  28. text-decoration-thickness: 3px;
  29. }
  30. `)
  31. })
  32. })
  33. it('should support modifiers for arbitrary values that contain the separator', () => {
  34. let config = {
  35. content: [
  36. {
  37. raw: html` <div class="hover:bg-[url('https://github.com/tailwindlabs.png')]"></div> `,
  38. },
  39. ],
  40. }
  41. return run('@tailwind utilities', config).then((result) => {
  42. return expect(result.css).toMatchFormattedCss(css`
  43. .hover\:bg-\[url\(\'https\:\/\/github\.com\/tailwindlabs\.png\'\)\]:hover {
  44. background-image: url('https://github.com/tailwindlabs.png');
  45. }
  46. `)
  47. })
  48. })
  49. it('should support arbitrary values for various background utilities', () => {
  50. let config = {
  51. content: [
  52. {
  53. raw: html`
  54. <!-- Lookup -->
  55. <div class="bg-gradient-to-r"></div>
  56. <div class="bg-red-500"></div>
  57. <!-- By implicit type -->
  58. <div class="bg-[url('/image-1-0.png')]"></div>
  59. <div class="bg-[#ff0000]"></div>
  60. <div class="bg-[rgb(var(--bg-color))]"></div>
  61. <div class="bg-[hsl(var(--bg-color))]"></div>
  62. <!-- By explicit type -->
  63. <div class="bg-[url:var(--image-url)]"></div>
  64. <div class="bg-[color:var(--bg-color)]"></div>
  65. `,
  66. },
  67. ],
  68. }
  69. return run('@tailwind utilities', config).then((result) => {
  70. return expect(result.css).toMatchFormattedCss(css`
  71. .bg-red-500 {
  72. --tw-bg-opacity: 1;
  73. background-color: rgb(239 68 68 / var(--tw-bg-opacity));
  74. }
  75. .bg-\[\#ff0000\] {
  76. --tw-bg-opacity: 1;
  77. background-color: rgb(255 0 0 / var(--tw-bg-opacity));
  78. }
  79. .bg-\[rgb\(var\(--bg-color\)\)\] {
  80. background-color: rgb(var(--bg-color));
  81. }
  82. .bg-\[hsl\(var\(--bg-color\)\)\] {
  83. background-color: hsl(var(--bg-color));
  84. }
  85. .bg-\[color\:var\(--bg-color\)\] {
  86. background-color: var(--bg-color);
  87. }
  88. .bg-gradient-to-r {
  89. background-image: linear-gradient(to right, var(--tw-gradient-stops));
  90. }
  91. .bg-\[url\(\'\/image-1-0\.png\'\)\] {
  92. background-image: url('/image-1-0.png');
  93. }
  94. .bg-\[url\:var\(--image-url\)\] {
  95. background-image: var(--image-url);
  96. }
  97. `)
  98. })
  99. })
  100. it('should not generate any css if an unknown typehint is used', () => {
  101. let config = {
  102. content: [
  103. {
  104. raw: html`<div class="inset-[hmm:12px]"></div>`,
  105. },
  106. ],
  107. }
  108. return run('@tailwind utilities', config).then((result) => {
  109. return expect(result.css).toMatchFormattedCss(css``)
  110. })
  111. })
  112. it('should handle unknown typehints', () => {
  113. let config = { content: [{ raw: html`<div class="w-[length:12px]"></div>` }] }
  114. return run('@tailwind utilities', config).then((result) => {
  115. return expect(result.css).toMatchFormattedCss(css`
  116. .w-\[length\:12px\] {
  117. width: 12px;
  118. }
  119. `)
  120. })
  121. })
  122. it('should convert _ to spaces', () => {
  123. // Using custom css function here, because otherwise with String.raw, we run
  124. // into an issue with `\2c ` escapes. If we use `\2c ` then JS complains
  125. // about strict mode. But `\\2c ` is not what it expected.
  126. function css(templates) {
  127. return templates.join('')
  128. }
  129. let config = {
  130. content: [
  131. {
  132. raw: html`
  133. <div class="grid-cols-[200px_repeat(auto-fill,minmax(15%,100px))_300px]"></div>
  134. <div class="grid-rows-[200px_repeat(auto-fill,minmax(15%,100px))_300px]"></div>
  135. <div class="shadow-[0px_0px_4px_black]"></div>
  136. <div class="rounded-[0px_4px_4px_0px]"></div>
  137. <div class="m-[8px_4px]"></div>
  138. <div class="p-[8px_4px]"></div>
  139. <div class="flex-[1_1_100%]"></div>
  140. <div class="col-[span_3_/_span_8]"></div>
  141. <div class="row-[span_3_/_span_8]"></div>
  142. <div class="auto-cols-[minmax(0,_1fr)]"></div>
  143. <div class="drop-shadow-[0px_1px_3px_black]"></div>
  144. <div class="content-[_hello_world_]"></div>
  145. <div class="content-[___abc____]"></div>
  146. <div class="content-['__hello__world__']"></div>
  147. `,
  148. },
  149. ],
  150. corePlugins: { preflight: false },
  151. }
  152. return run('@tailwind utilities', config).then((result) => {
  153. return expect(result.css).toMatchFormattedCss(css`
  154. .col-\\[span_3_\\/_span_8\\] {
  155. grid-column: span 3 / span 8;
  156. }
  157. .row-\\[span_3_\\/_span_8\\] {
  158. grid-row: span 3 / span 8;
  159. }
  160. .m-\\[8px_4px\\] {
  161. margin: 8px 4px;
  162. }
  163. .flex-\\[1_1_100\\%\\] {
  164. flex: 1 1 100%;
  165. }
  166. .auto-cols-\\[minmax\\(0\\2c _1fr\\)\\] {
  167. grid-auto-columns: minmax(0, 1fr);
  168. }
  169. .grid-cols-\\[200px_repeat\\(auto-fill\\2c minmax\\(15\\%\\2c 100px\\)\\)_300px\\] {
  170. grid-template-columns: 200px repeat(auto-fill, minmax(15%, 100px)) 300px;
  171. }
  172. .grid-rows-\\[200px_repeat\\(auto-fill\\2c minmax\\(15\\%\\2c 100px\\)\\)_300px\\] {
  173. grid-template-rows: 200px repeat(auto-fill, minmax(15%, 100px)) 300px;
  174. }
  175. .rounded-\\[0px_4px_4px_0px\\] {
  176. border-radius: 0px 4px 4px 0px;
  177. }
  178. .p-\\[8px_4px\\] {
  179. padding: 8px 4px;
  180. }
  181. .shadow-\\[0px_0px_4px_black\\] {
  182. --tw-shadow: 0px 0px 4px black;
  183. --tw-shadow-colored: 0px 0px 4px var(--tw-shadow-color);
  184. box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
  185. var(--tw-shadow);
  186. }
  187. .drop-shadow-\\[0px_1px_3px_black\\] {
  188. --tw-drop-shadow: drop-shadow(0px 1px 3px black);
  189. filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale)
  190. var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia)
  191. var(--tw-drop-shadow);
  192. }
  193. .content-\\[_hello_world_\\] {
  194. --tw-content: hello world;
  195. content: var(--tw-content);
  196. }
  197. .content-\\[___abc____\\] {
  198. --tw-content: abc;
  199. content: var(--tw-content);
  200. }
  201. .content-\\[\\'__hello__world__\\'\\] {
  202. --tw-content: ' hello world ';
  203. content: var(--tw-content);
  204. }
  205. `)
  206. })
  207. })
  208. it('should not convert escaped underscores with spaces', () => {
  209. let config = {
  210. content: [{ raw: `<div class="content-['snake\\_case']"></div>` }],
  211. corePlugins: { preflight: false },
  212. }
  213. return run('@tailwind utilities', config).then((result) => {
  214. return expect(result.css).toMatchFormattedCss(css`
  215. .content-\[\'snake\\_case\'\] {
  216. --tw-content: 'snake_case';
  217. content: var(--tw-content);
  218. }
  219. `)
  220. })
  221. })
  222. it('should warn and not generate if arbitrary values are ambiguous', () => {
  223. // If we don't protect against this, then `bg-[200px_100px]` would both
  224. // generate the background-size as well as the background-position utilities.
  225. let config = {
  226. content: [{ raw: html`<div class="bg-[200px_100px]"></div>` }],
  227. }
  228. return run('@tailwind utilities', config).then((result) => {
  229. return expect(result.css).toMatchFormattedCss(css``)
  230. })
  231. })
  232. it('should support colons in URLs', () => {
  233. let config = {
  234. content: [
  235. { raw: html`<div class="bg-[url('https://www.spacejam.com/1996/img/bg_stars.gif')]"></div>` },
  236. ],
  237. }
  238. return run('@tailwind utilities', config).then((result) => {
  239. return expect(result.css).toMatchFormattedCss(css`
  240. .bg-\[url\(\'https\:\/\/www\.spacejam\.com\/1996\/img\/bg_stars\.gif\'\)\] {
  241. background-image: url('https://www.spacejam.com/1996/img/bg_stars.gif');
  242. }
  243. `)
  244. })
  245. })
  246. it('should support unescaped underscores in URLs', () => {
  247. let config = {
  248. content: [
  249. { raw: html`<div class="bg-[url('brown_potato.jpg'),_url('red_tomato.png')]"></div>` },
  250. ],
  251. }
  252. return run('@tailwind utilities', config).then((result) => {
  253. return expect(result.css).toMatchFormattedCss(`
  254. .bg-\\[url\\(\\'brown_potato\\.jpg\\'\\)\\2c _url\\(\\'red_tomato\\.png\\'\\)\\] {
  255. background-image: url('brown_potato.jpg'), url('red_tomato.png');
  256. }
  257. `)
  258. })
  259. })
  260. it('should be possible to read theme values in arbitrary values (without quotes)', () => {
  261. let config = {
  262. content: [{ raw: html`<div class="w-[theme(spacing.1)] w-[theme(spacing[0.5])]"></div>` }],
  263. theme: {
  264. spacing: {
  265. 0.5: 'calc(.5 * .25rem)',
  266. 1: 'calc(1 * .25rem)',
  267. },
  268. },
  269. }
  270. return run('@tailwind utilities', config).then((result) => {
  271. return expect(result.css).toMatchFormattedCss(css`
  272. .w-\[theme\(spacing\.1\)\] {
  273. width: calc(1 * 0.25rem);
  274. }
  275. .w-\[theme\(spacing\[0\.5\]\)\] {
  276. width: calc(0.5 * 0.25rem);
  277. }
  278. `)
  279. })
  280. })
  281. it('should be possible to read theme values in arbitrary values (with quotes)', () => {
  282. let config = {
  283. content: [{ raw: html`<div class="w-[theme('spacing.1')] w-[theme('spacing[0.5]')]"></div>` }],
  284. theme: {
  285. spacing: {
  286. 0.5: 'calc(.5 * .25rem)',
  287. 1: 'calc(1 * .25rem)',
  288. },
  289. },
  290. }
  291. return run('@tailwind utilities', config).then((result) => {
  292. return expect(result.css).toMatchFormattedCss(css`
  293. .w-\[theme\(\'spacing\.1\'\)\] {
  294. width: calc(1 * 0.25rem);
  295. }
  296. .w-\[theme\(\'spacing\[0\.5\]\'\)\] {
  297. width: calc(0.5 * 0.25rem);
  298. }
  299. `)
  300. })
  301. })
  302. it('should be possible to read theme values in arbitrary values (with quotes) when inside calc or similar functions', () => {
  303. let config = {
  304. content: [
  305. {
  306. raw: html`<div
  307. class="w-[calc(100%-theme('spacing.1'))] w-[calc(100%-theme('spacing[0.5]'))]"
  308. ></div>`,
  309. },
  310. ],
  311. theme: {
  312. spacing: {
  313. 0.5: 'calc(.5 * .25rem)',
  314. 1: 'calc(1 * .25rem)',
  315. },
  316. },
  317. }
  318. return run('@tailwind utilities', config).then((result) => {
  319. return expect(result.css).toMatchFormattedCss(css`
  320. .w-\[calc\(100\%-theme\(\'spacing\.1\'\)\)\] {
  321. width: calc(100% - calc(1 * 0.25rem));
  322. }
  323. .w-\[calc\(100\%-theme\(\'spacing\[0\.5\]\'\)\)\] {
  324. width: calc(100% - calc(0.5 * 0.25rem));
  325. }
  326. `)
  327. })
  328. })
  329. it('should not output unparsable arbitrary CSS values', () => {
  330. let config = {
  331. content: [
  332. {
  333. raw: 'let classes = `w-[${sizes.width}]`',
  334. },
  335. ],
  336. }
  337. return run('@tailwind utilities', config).then((result) => {
  338. return expect(result.css).toMatchFormattedCss(``)
  339. })
  340. })
  341. // Issue: https://github.com/tailwindlabs/tailwindcss/issues/7997
  342. // `top_right_50%` was a valid percentage before introducing this change
  343. it('should correctly validate each part when checking for `percentage` data types', () => {
  344. let config = {
  345. content: [{ raw: html`<div class="bg-[top_right_50%]"></div>` }],
  346. corePlugins: { preflight: false },
  347. plugins: [],
  348. }
  349. let input = css`
  350. @tailwind utilities;
  351. `
  352. return run(input, config).then((result) => {
  353. expect(result.css).toMatchFormattedCss(css`
  354. .bg-\[top_right_50\%\] {
  355. background-position: top right 50%;
  356. }
  357. `)
  358. })
  359. })