compileStyle.spec.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /**
  2. * @jest-environment node
  3. */
  4. import {
  5. compileStyle,
  6. compileStyleAsync,
  7. SFCStyleCompileOptions
  8. } from '../src/compileStyle'
  9. import path from 'path'
  10. describe('SFC scoped CSS', () => {
  11. function compileScoped(
  12. source: string,
  13. options?: Partial<SFCStyleCompileOptions>
  14. ): string {
  15. const res = compileStyle({
  16. source,
  17. filename: 'test.css',
  18. id: 'test',
  19. scoped: true,
  20. ...options
  21. })
  22. if (res.errors.length) {
  23. res.errors.forEach(err => {
  24. console.error(err)
  25. })
  26. expect(res.errors.length).toBe(0)
  27. }
  28. return res.code
  29. }
  30. test('simple selectors', () => {
  31. expect(compileScoped(`h1 { color: red; }`)).toMatch(
  32. `h1[test] { color: red;`
  33. )
  34. expect(compileScoped(`.foo { color: red; }`)).toMatch(
  35. `.foo[test] { color: red;`
  36. )
  37. })
  38. test('descendent selector', () => {
  39. expect(compileScoped(`h1 .foo { color: red; }`)).toMatch(
  40. `h1 .foo[test] { color: red;`
  41. )
  42. })
  43. test('multiple selectors', () => {
  44. expect(compileScoped(`h1 .foo, .bar, .baz { color: red; }`)).toMatch(
  45. `h1 .foo[test], .bar[test], .baz[test] { color: red;`
  46. )
  47. })
  48. test('pseudo class', () => {
  49. expect(compileScoped(`.foo:after { color: red; }`)).toMatch(
  50. `.foo[test]:after { color: red;`
  51. )
  52. })
  53. test('pseudo element', () => {
  54. expect(compileScoped(`::selection { display: none; }`)).toMatch(
  55. '[test]::selection {'
  56. )
  57. })
  58. test('spaces before pseudo element', () => {
  59. const code = compileScoped(`.abc, ::selection { color: red; }`)
  60. expect(code).toMatch('.abc[test],')
  61. expect(code).toMatch('[test]::selection {')
  62. })
  63. test('::v-deep', () => {
  64. expect(compileScoped(`:deep(.foo) { color: red; }`)).toMatchInlineSnapshot(`
  65. "[test] .foo { color: red;
  66. }"
  67. `)
  68. expect(compileScoped(`::v-deep(.foo) { color: red; }`))
  69. .toMatchInlineSnapshot(`
  70. "[test] .foo { color: red;
  71. }"
  72. `)
  73. expect(compileScoped(`::v-deep(.foo .bar) { color: red; }`))
  74. .toMatchInlineSnapshot(`
  75. "[test] .foo .bar { color: red;
  76. }"
  77. `)
  78. expect(compileScoped(`.baz .qux ::v-deep(.foo .bar) { color: red; }`))
  79. .toMatchInlineSnapshot(`
  80. ".baz .qux[test] .foo .bar { color: red;
  81. }"
  82. `)
  83. })
  84. test('::v-slotted', () => {
  85. expect(compileScoped(`:slotted(.foo) { color: red; }`))
  86. .toMatchInlineSnapshot(`
  87. ".foo[test-s] { color: red;
  88. }"
  89. `)
  90. expect(compileScoped(`::v-slotted(.foo) { color: red; }`))
  91. .toMatchInlineSnapshot(`
  92. ".foo[test-s] { color: red;
  93. }"
  94. `)
  95. expect(compileScoped(`::v-slotted(.foo .bar) { color: red; }`))
  96. .toMatchInlineSnapshot(`
  97. ".foo .bar[test-s] { color: red;
  98. }"
  99. `)
  100. expect(compileScoped(`.baz .qux ::v-slotted(.foo .bar) { color: red; }`))
  101. .toMatchInlineSnapshot(`
  102. ".baz .qux .foo .bar[test-s] { color: red;
  103. }"
  104. `)
  105. })
  106. test('::v-global', () => {
  107. expect(compileScoped(`:global(.foo) { color: red; }`))
  108. .toMatchInlineSnapshot(`
  109. ".foo { color: red;
  110. }"
  111. `)
  112. expect(compileScoped(`::v-global(.foo) { color: red; }`))
  113. .toMatchInlineSnapshot(`
  114. ".foo { color: red;
  115. }"
  116. `)
  117. expect(compileScoped(`::v-global(.foo .bar) { color: red; }`))
  118. .toMatchInlineSnapshot(`
  119. ".foo .bar { color: red;
  120. }"
  121. `)
  122. // global ignores anything before it
  123. expect(compileScoped(`.baz .qux ::v-global(.foo .bar) { color: red; }`))
  124. .toMatchInlineSnapshot(`
  125. ".foo .bar { color: red;
  126. }"
  127. `)
  128. })
  129. test('media query', () => {
  130. expect(compileScoped(`@media print { .foo { color: red }}`))
  131. .toMatchInlineSnapshot(`
  132. "@media print {
  133. .foo[test] { color: red
  134. }}"
  135. `)
  136. })
  137. test('supports query', () => {
  138. expect(compileScoped(`@supports(display: grid) { .foo { display: grid }}`))
  139. .toMatchInlineSnapshot(`
  140. "@supports(display: grid) {
  141. .foo[test] { display: grid
  142. }}"
  143. `)
  144. })
  145. test('scoped keyframes', () => {
  146. const style = compileScoped(
  147. `
  148. .anim {
  149. animation: color 5s infinite, other 5s;
  150. }
  151. .anim-2 {
  152. animation-name: color;
  153. animation-duration: 5s;
  154. }
  155. .anim-3 {
  156. animation: 5s color infinite, 5s other;
  157. }
  158. .anim-multiple {
  159. animation: color 5s infinite, opacity 2s;
  160. }
  161. .anim-multiple-2 {
  162. animation-name: color, opacity;
  163. animation-duration: 5s, 2s;
  164. }
  165. @keyframes color {
  166. from { color: red; }
  167. to { color: green; }
  168. }
  169. @-webkit-keyframes color {
  170. from { color: red; }
  171. to { color: green; }
  172. }
  173. @keyframes opacity {
  174. from { opacity: 0; }
  175. to { opacity: 1; }
  176. }
  177. @-webkit-keyframes opacity {
  178. from { opacity: 0; }
  179. to { opacity: 1; }
  180. }
  181. `,
  182. { id: 'data-v-test' }
  183. )
  184. expect(style).toContain(
  185. `.anim[data-v-test] {\n animation: color-test 5s infinite, other 5s;`
  186. )
  187. expect(style).toContain(
  188. `.anim-2[data-v-test] {\n animation-name: color-test`
  189. )
  190. expect(style).toContain(
  191. `.anim-3[data-v-test] {\n animation: 5s color-test infinite, 5s other;`
  192. )
  193. expect(style).toContain(`@keyframes color-test {`)
  194. expect(style).toContain(`@-webkit-keyframes color-test {`)
  195. expect(style).toContain(
  196. `.anim-multiple[data-v-test] {\n animation: color-test 5s infinite,opacity-test 2s;`
  197. )
  198. expect(style).toContain(
  199. `.anim-multiple-2[data-v-test] {\n animation-name: color-test,opacity-test;`
  200. )
  201. expect(style).toContain(`@keyframes opacity-test {`)
  202. expect(style).toContain(`@-webkit-keyframes opacity-test {`)
  203. })
  204. // vue-loader/#1370
  205. test('spaces after selector', () => {
  206. expect(compileScoped(`.foo , .bar { color: red; }`)).toMatchInlineSnapshot(`
  207. ".foo[test], .bar[test] { color: red;
  208. }"
  209. `)
  210. })
  211. describe('deprecated syntax', () => {
  212. test('::v-deep as combinator', () => {
  213. expect(compileScoped(`::v-deep .foo { color: red; }`))
  214. .toMatchInlineSnapshot(`
  215. "[test] .foo { color: red;
  216. }"
  217. `)
  218. expect(compileScoped(`.bar ::v-deep .foo { color: red; }`))
  219. .toMatchInlineSnapshot(`
  220. ".bar[test] .foo { color: red;
  221. }"
  222. `)
  223. expect(
  224. `::v-deep usage as a combinator has been deprecated.`
  225. ).toHaveBeenWarned()
  226. })
  227. test('>>> (deprecated syntax)', () => {
  228. const code = compileScoped(`>>> .foo { color: red; }`)
  229. expect(code).toMatchInlineSnapshot(`
  230. "[test] .foo { color: red;
  231. }"
  232. `)
  233. expect(
  234. `the >>> and /deep/ combinators have been deprecated.`
  235. ).toHaveBeenWarned()
  236. })
  237. test('/deep/ (deprecated syntax)', () => {
  238. const code = compileScoped(`/deep/ .foo { color: red; }`)
  239. expect(code).toMatchInlineSnapshot(`
  240. "[test] .foo { color: red;
  241. }"
  242. `)
  243. expect(
  244. `the >>> and /deep/ combinators have been deprecated.`
  245. ).toHaveBeenWarned()
  246. })
  247. })
  248. describe('<style vars>', () => {
  249. test('should rewrite CSS vars in scoped mode', () => {
  250. const code = compileScoped(
  251. `.foo {
  252. color: var(--color);
  253. font-size: var(--global:font);
  254. }`,
  255. {
  256. id: 'data-v-test',
  257. vars: true
  258. }
  259. )
  260. expect(code).toMatchInlineSnapshot(`
  261. ".foo[data-v-test] {
  262. color: var(--test-color);
  263. font-size: var(--font);
  264. }"
  265. `)
  266. })
  267. })
  268. })
  269. describe('SFC CSS modules', () => {
  270. test('should include resulting classes object in result', async () => {
  271. const result = await compileStyleAsync({
  272. source: `.red { color: red }\n.green { color: green }\n:global(.blue) { color: blue }`,
  273. filename: `test.css`,
  274. id: 'test',
  275. modules: true
  276. })
  277. expect(result.modules).toBeDefined()
  278. expect(result.modules!.red).toMatch('_red_')
  279. expect(result.modules!.green).toMatch('_green_')
  280. expect(result.modules!.blue).toBeUndefined()
  281. })
  282. test('postcss-modules options', async () => {
  283. const result = await compileStyleAsync({
  284. source: `:local(.foo-bar) { color: red }\n.baz-qux { color: green }`,
  285. filename: `test.css`,
  286. id: 'test',
  287. modules: true,
  288. modulesOptions: {
  289. scopeBehaviour: 'global',
  290. generateScopedName: `[name]__[local]__[hash:base64:5]`,
  291. localsConvention: 'camelCaseOnly'
  292. }
  293. })
  294. expect(result.modules).toBeDefined()
  295. expect(result.modules!.fooBar).toMatch('__foo-bar__')
  296. expect(result.modules!.bazQux).toBeUndefined()
  297. })
  298. })
  299. describe('SFC style preprocessors', () => {
  300. test('scss @import', () => {
  301. const res = compileStyle({
  302. source: `
  303. @import "./import.scss";
  304. `,
  305. filename: path.resolve(__dirname, './fixture/test.scss'),
  306. id: '',
  307. preprocessLang: 'scss'
  308. })
  309. expect([...res.dependencies]).toStrictEqual([
  310. path.join(__dirname, './fixture/import.scss')
  311. ])
  312. })
  313. test('scss respect user-defined string options.additionalData', () => {
  314. const res = compileStyle({
  315. preprocessOptions: {
  316. additionalData: `
  317. @mixin square($size) {
  318. width: $size;
  319. height: $size;
  320. }`
  321. },
  322. source: `
  323. .square {
  324. @include square(100px);
  325. }
  326. `,
  327. filename: path.resolve(__dirname, './fixture/test.scss'),
  328. id: '',
  329. preprocessLang: 'scss'
  330. })
  331. expect(res.errors.length).toBe(0)
  332. })
  333. test('scss respect user-defined function options.additionalData', () => {
  334. const source = `
  335. .square {
  336. @include square(100px);
  337. }
  338. `
  339. const filename = path.resolve(__dirname, './fixture/test.scss')
  340. const res = compileStyle({
  341. preprocessOptions: {
  342. additionalData: (s: string, f: string) => {
  343. expect(s).toBe(source)
  344. expect(f).toBe(filename)
  345. return `
  346. @mixin square($size) {
  347. width: $size;
  348. height: $size;
  349. }`
  350. }
  351. },
  352. source,
  353. filename,
  354. id: '',
  355. preprocessLang: 'scss'
  356. })
  357. expect(res.errors.length).toBe(0)
  358. })
  359. })