style-manager-spec.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. const temp = require('temp').track();
  2. const StyleManager = require('../src/style-manager');
  3. describe('StyleManager', () => {
  4. let [styleManager, addEvents, removeEvents, updateEvents] = [];
  5. beforeEach(() => {
  6. styleManager = new StyleManager({
  7. configDirPath: temp.mkdirSync('atom-config')
  8. });
  9. addEvents = [];
  10. removeEvents = [];
  11. updateEvents = [];
  12. styleManager.onDidAddStyleElement(event => {
  13. addEvents.push(event);
  14. });
  15. styleManager.onDidRemoveStyleElement(event => {
  16. removeEvents.push(event);
  17. });
  18. styleManager.onDidUpdateStyleElement(event => {
  19. updateEvents.push(event);
  20. });
  21. });
  22. afterEach(() => {
  23. try {
  24. temp.cleanupSync();
  25. } catch (e) {
  26. // Do nothing
  27. }
  28. });
  29. describe('::addStyleSheet(source, params)', () => {
  30. it('adds a style sheet based on the given source and returns a disposable allowing it to be removed', () => {
  31. const disposable = styleManager.addStyleSheet('a {color: red}');
  32. expect(addEvents.length).toBe(1);
  33. expect(addEvents[0].textContent).toBe('a {color: red}');
  34. const styleElements = styleManager.getStyleElements();
  35. expect(styleElements.length).toBe(1);
  36. expect(styleElements[0].textContent).toBe('a {color: red}');
  37. disposable.dispose();
  38. expect(removeEvents.length).toBe(1);
  39. expect(removeEvents[0].textContent).toBe('a {color: red}');
  40. expect(styleManager.getStyleElements().length).toBe(0);
  41. });
  42. describe('atom-text-editor shadow DOM selectors upgrades', () => {
  43. beforeEach(() => {
  44. // attach styles element to the DOM to parse CSS rules
  45. styleManager.onDidAddStyleElement(styleElement => {
  46. jasmine.attachToDOM(styleElement);
  47. });
  48. });
  49. it('removes the ::shadow pseudo-element from atom-text-editor selectors', () => {
  50. styleManager.addStyleSheet(`
  51. atom-text-editor::shadow .class-1, atom-text-editor::shadow .class-2 { color: red }
  52. atom-text-editor::shadow > .class-3 { color: yellow }
  53. atom-text-editor .class-4 { color: blue }
  54. atom-text-editor[data-grammar*="js"]::shadow .class-6 { color: green; }
  55. atom-text-editor[mini].is-focused::shadow .class-7 { color: green; }
  56. `);
  57. expect(
  58. Array.from(styleManager.getStyleElements()[0].sheet.cssRules).map(
  59. r => r.selectorText
  60. )
  61. ).toEqual([
  62. 'atom-text-editor.editor .class-1, atom-text-editor.editor .class-2',
  63. 'atom-text-editor.editor > .class-3',
  64. 'atom-text-editor .class-4',
  65. 'atom-text-editor[data-grammar*="js"].editor .class-6',
  66. 'atom-text-editor[mini].is-focused.editor .class-7'
  67. ]);
  68. });
  69. describe('when a selector targets the atom-text-editor shadow DOM', () => {
  70. it('prepends "--syntax" to class selectors matching a grammar scope name and not already starting with "syntax--"', () => {
  71. styleManager.addStyleSheet(
  72. `
  73. .class-1 { color: red }
  74. .source > .js, .source.coffee { color: green }
  75. .syntax--source { color: gray }
  76. #id-1 { color: blue }
  77. `,
  78. { context: 'atom-text-editor' }
  79. );
  80. expect(
  81. Array.from(styleManager.getStyleElements()[0].sheet.cssRules).map(
  82. r => r.selectorText
  83. )
  84. ).toEqual([
  85. '.class-1',
  86. '.syntax--source > .syntax--js, .syntax--source.syntax--coffee',
  87. '.syntax--source',
  88. '#id-1'
  89. ]);
  90. styleManager.addStyleSheet(`
  91. .source > .js, .source.coffee { color: green }
  92. atom-text-editor::shadow .source > .js { color: yellow }
  93. atom-text-editor[mini].is-focused::shadow .source > .js { color: gray }
  94. atom-text-editor .source > .js { color: red }
  95. `);
  96. expect(
  97. Array.from(styleManager.getStyleElements()[1].sheet.cssRules).map(
  98. r => r.selectorText
  99. )
  100. ).toEqual([
  101. '.source > .js, .source.coffee',
  102. 'atom-text-editor.editor .syntax--source > .syntax--js',
  103. 'atom-text-editor[mini].is-focused.editor .syntax--source > .syntax--js',
  104. 'atom-text-editor .source > .js'
  105. ]);
  106. });
  107. });
  108. it('replaces ":host" with "atom-text-editor" only when the context of a style sheet is "atom-text-editor"', () => {
  109. styleManager.addStyleSheet(
  110. ':host .class-1, :host .class-2 { color: red; }'
  111. );
  112. expect(
  113. Array.from(styleManager.getStyleElements()[0].sheet.cssRules).map(
  114. r => r.selectorText
  115. )
  116. ).toEqual([':host .class-1, :host .class-2']);
  117. styleManager.addStyleSheet(
  118. ':host .class-1, :host .class-2 { color: red; }',
  119. { context: 'atom-text-editor' }
  120. );
  121. expect(
  122. Array.from(styleManager.getStyleElements()[1].sheet.cssRules).map(
  123. r => r.selectorText
  124. )
  125. ).toEqual(['atom-text-editor .class-1, atom-text-editor .class-2']);
  126. });
  127. it('does not throw exceptions on rules with no selectors', () => {
  128. styleManager.addStyleSheet('@media screen {font-size: 10px}', {
  129. context: 'atom-text-editor'
  130. });
  131. });
  132. });
  133. describe('css mathematical expression calc() wrap upgrades', () => {
  134. const mathStyleManager = new StyleManager();
  135. mathStyleManager.configDirPath = null; // Ensures for testing that we never
  136. // go looking for cached files, and will always use the css provided
  137. it('does not upgrade already wrapped math', () => {
  138. let upgradedSheet = mathStyleManager.upgradeStyleSheet(
  139. "p { padding: calc(10px/2); }",
  140. {},
  141. "math"
  142. );
  143. expect(upgradedSheet.source).toEqual("p { padding: calc(10px/2); }");
  144. });
  145. it('does not upgrade negative numbers', () => {
  146. let upgradedSheet = mathStyleManager.upgradeStyleSheet(
  147. "p { padding: 0 -1px; }",
  148. {},
  149. "math"
  150. );
  151. expect(upgradedSheet.source).toEqual("p { padding: 0 -1px; }");
  152. });
  153. it('upgrades simple division', () => {
  154. let upgradedSheet = mathStyleManager.upgradeStyleSheet(
  155. "p { padding: 10px/2; }",
  156. {},
  157. "math"
  158. );
  159. expect(upgradedSheet.source).toEqual("p { padding: calc(10px/2); }");
  160. });
  161. it('upgrades multi parameter math', () => {
  162. let upgradedSheet = mathStyleManager.upgradeStyleSheet(
  163. "p { padding: 0 10px/2 5em; }",
  164. {},
  165. "math"
  166. );
  167. expect(upgradedSheet.source).toEqual("p { padding: 0 calc(10px/2) 5em; }");
  168. });
  169. it('upgrades math with spaces', () => {
  170. let upgradedSheet = mathStyleManager.upgradeStyleSheet(
  171. "p { padding: 10px / 2; }",
  172. {},
  173. "math"
  174. );
  175. expect(upgradedSheet.source).toEqual("p { padding: calc(10px / 2); }");
  176. });
  177. it('upgrades multiple math expressions in a single line', () => {
  178. let upgradedSheet = mathStyleManager.upgradeStyleSheet(
  179. "p { padding: 10px/2 10px/3; }",
  180. {},
  181. "math"
  182. );
  183. expect(upgradedSheet.source).toEqual("p { padding: calc(10px/2) calc(10px/3); }");
  184. });
  185. it('does not upgrade base64 strings', () => {
  186. // Regression Check
  187. let upgradedSheet = mathStyleManager.upgradeStyleSheet(
  188. "p { cursor: -webkit-image-set(url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAL0lEQVQoz2NgCD3x//9/BhBYBWdhgFVAiVW4JBFKGIa4AqD0//9D3pt4I4tAdAMAHTQ/j5Zom30AAAAASUVORK5CYII=')); }",
  189. {},
  190. "math"
  191. );
  192. expect(upgradedSheet.source).toEqual(
  193. "p { cursor: -webkit-image-set(url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAL0lEQVQoz2NgCD3x//9/BhBYBWdhgFVAiVW4JBFKGIa4AqD0//9D3pt4I4tAdAMAHTQ/j5Zom30AAAAASUVORK5CYII=')); }"
  194. );
  195. });
  196. it('does not modify hsl function where `/` is valid', () => {
  197. let upgradedSheet = mathStyleManager.upgradeStyleSheet(
  198. "p { caret-color: hsl(228deg 4% 24% / 0.8); }",
  199. {},
  200. "math"
  201. );
  202. expect(upgradedSheet.source).toEqual(
  203. "p { caret-color: hsl(228deg 4% 24% / 0.8); }"
  204. );
  205. });
  206. it('does not modify acos function, where math is valid', () => {
  207. let upgradedSheet = mathStyleManager.upgradeStyleSheet(
  208. "p { transform: rotate(acos(2 * 0.125)); }",
  209. {},
  210. "math"
  211. );
  212. expect(upgradedSheet.source).toEqual(
  213. "p { transform: rotate(acos(2 * 0.125)); }"
  214. );
  215. });
  216. it('recognizes valid less variables: right side', () => {
  217. let upgradedSheet = mathStyleManager.upgradeStyleSheet(
  218. "p { padding: @size + 12px; }",
  219. {},
  220. "math"
  221. );
  222. expect(upgradedSheet.source).toEqual(
  223. "p { padding: calc(@size + 12px); }"
  224. );
  225. });
  226. it('recognizes valid less variables: left side', () => {
  227. let upgradedSheet = mathStyleManager.upgradeStyleSheet(
  228. "p { padding: 12px + @size; }",
  229. {},
  230. "math"
  231. );
  232. expect(upgradedSheet.source).toEqual(
  233. "p { padding: calc(12px + @size); }"
  234. );
  235. });
  236. });
  237. describe('when a sourcePath parameter is specified', () => {
  238. it('ensures a maximum of one style element for the given source path, updating a previous if it exists', () => {
  239. styleManager.addStyleSheet('a {color: red}', {
  240. sourcePath: '/foo/bar'
  241. });
  242. expect(addEvents.length).toBe(1);
  243. expect(addEvents[0].getAttribute('source-path')).toBe('/foo/bar');
  244. const disposable2 = styleManager.addStyleSheet('a {color: blue}', {
  245. sourcePath: '/foo/bar'
  246. });
  247. expect(addEvents.length).toBe(1);
  248. expect(updateEvents.length).toBe(1);
  249. expect(updateEvents[0].getAttribute('source-path')).toBe('/foo/bar');
  250. expect(updateEvents[0].textContent).toBe('a {color: blue}');
  251. disposable2.dispose();
  252. addEvents = [];
  253. styleManager.addStyleSheet('a {color: yellow}', {
  254. sourcePath: '/foo/bar'
  255. });
  256. expect(addEvents.length).toBe(1);
  257. expect(addEvents[0].getAttribute('source-path')).toBe('/foo/bar');
  258. expect(addEvents[0].textContent).toBe('a {color: yellow}');
  259. });
  260. });
  261. describe('when a priority parameter is specified', () => {
  262. it('inserts the style sheet based on the priority', () => {
  263. styleManager.addStyleSheet('a {color: red}', { priority: 1 });
  264. styleManager.addStyleSheet('a {color: blue}', { priority: 0 });
  265. styleManager.addStyleSheet('a {color: green}', { priority: 2 });
  266. styleManager.addStyleSheet('a {color: yellow}', { priority: 1 });
  267. expect(
  268. styleManager.getStyleElements().map(elt => elt.textContent)
  269. ).toEqual([
  270. 'a {color: blue}',
  271. 'a {color: red}',
  272. 'a {color: yellow}',
  273. 'a {color: green}'
  274. ]);
  275. });
  276. });
  277. });
  278. });