theme-manager-spec.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. const path = require('path');
  2. const fs = require('fs-plus');
  3. const temp = require('temp').track();
  4. describe('atom.themes', function() {
  5. beforeEach(function() {
  6. spyOn(atom, 'inSpecMode').andReturn(false);
  7. spyOn(console, 'warn');
  8. });
  9. afterEach(function() {
  10. waitsForPromise(() => atom.themes.deactivateThemes());
  11. runs(function() {
  12. try {
  13. temp.cleanupSync();
  14. } catch (error) {}
  15. });
  16. });
  17. describe('theme getters and setters', function() {
  18. beforeEach(function() {
  19. jasmine.snapshotDeprecations();
  20. atom.packages.loadPackages();
  21. });
  22. afterEach(() => jasmine.restoreDeprecationsSnapshot());
  23. describe('getLoadedThemes', () =>
  24. it('gets all the loaded themes', function() {
  25. const themes = atom.themes.getLoadedThemes();
  26. expect(themes.length).toBeGreaterThan(2);
  27. }));
  28. describe('getActiveThemes', () =>
  29. it('gets all the active themes', function() {
  30. waitsForPromise(() => atom.themes.activateThemes());
  31. runs(function() {
  32. const names = atom.config.get('core.themes');
  33. expect(names.length).toBeGreaterThan(0);
  34. const themes = atom.themes.getActiveThemes();
  35. expect(themes).toHaveLength(names.length);
  36. });
  37. }));
  38. });
  39. describe('when the core.themes config value contains invalid entry', () =>
  40. it('ignores theme', function() {
  41. atom.config.set('core.themes', [
  42. 'atom-light-ui',
  43. null,
  44. undefined,
  45. '',
  46. false,
  47. 4,
  48. {},
  49. [],
  50. 'atom-dark-ui'
  51. ]);
  52. expect(atom.themes.getEnabledThemeNames()).toEqual([
  53. 'atom-dark-ui',
  54. 'atom-light-ui'
  55. ]);
  56. }));
  57. describe('::getImportPaths()', function() {
  58. it('returns the theme directories before the themes are loaded', function() {
  59. atom.config.set('core.themes', [
  60. 'theme-with-index-less',
  61. 'atom-dark-ui',
  62. 'atom-light-ui'
  63. ]);
  64. const paths = atom.themes.getImportPaths();
  65. // syntax theme is not a dir at this time, so only two.
  66. expect(paths.length).toBe(2);
  67. expect(paths[0]).toContain('atom-light-ui');
  68. expect(paths[1]).toContain('atom-dark-ui');
  69. });
  70. it('ignores themes that cannot be resolved to a directory', function() {
  71. atom.config.set('core.themes', ['definitely-not-a-theme']);
  72. expect(() => atom.themes.getImportPaths()).not.toThrow();
  73. });
  74. });
  75. describe('when the core.themes config value changes', function() {
  76. it('add/removes stylesheets to reflect the new config value', function() {
  77. let didChangeActiveThemesHandler;
  78. atom.themes.onDidChangeActiveThemes(
  79. (didChangeActiveThemesHandler = jasmine.createSpy())
  80. );
  81. spyOn(atom.styles, 'getUserStyleSheetPath').andCallFake(() => null);
  82. waitsForPromise(() => atom.themes.activateThemes());
  83. runs(function() {
  84. didChangeActiveThemesHandler.reset();
  85. atom.config.set('core.themes', []);
  86. });
  87. waitsFor('a', () => didChangeActiveThemesHandler.callCount === 1);
  88. runs(function() {
  89. didChangeActiveThemesHandler.reset();
  90. expect(document.querySelectorAll('style.theme')).toHaveLength(0);
  91. atom.config.set('core.themes', ['atom-dark-ui']);
  92. });
  93. waitsFor('b', () => didChangeActiveThemesHandler.callCount === 1);
  94. runs(function() {
  95. didChangeActiveThemesHandler.reset();
  96. expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(
  97. 2
  98. );
  99. expect(
  100. document
  101. .querySelector('style[priority="1"]')
  102. .getAttribute('source-path')
  103. ).toMatch(/atom-dark-ui/);
  104. atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-ui']);
  105. });
  106. waitsFor('c', () => didChangeActiveThemesHandler.callCount === 1);
  107. runs(function() {
  108. didChangeActiveThemesHandler.reset();
  109. expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(
  110. 2
  111. );
  112. expect(
  113. document
  114. .querySelectorAll('style[priority="1"]')[0]
  115. .getAttribute('source-path')
  116. ).toMatch(/atom-dark-ui/);
  117. expect(
  118. document
  119. .querySelectorAll('style[priority="1"]')[1]
  120. .getAttribute('source-path')
  121. ).toMatch(/atom-light-ui/);
  122. atom.config.set('core.themes', []);
  123. });
  124. waitsFor(() => didChangeActiveThemesHandler.callCount === 1);
  125. runs(function() {
  126. didChangeActiveThemesHandler.reset();
  127. expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(
  128. 2
  129. );
  130. // atom-dark-ui has a directory path, the syntax one doesn't
  131. atom.config.set('core.themes', [
  132. 'theme-with-index-less',
  133. 'atom-dark-ui'
  134. ]);
  135. });
  136. waitsFor(() => didChangeActiveThemesHandler.callCount === 1);
  137. runs(function() {
  138. expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(
  139. 2
  140. );
  141. const importPaths = atom.themes.getImportPaths();
  142. expect(importPaths.length).toBe(1);
  143. expect(importPaths[0]).toContain('atom-dark-ui');
  144. });
  145. });
  146. it('adds theme-* classes to the workspace for each active theme', function() {
  147. atom.config.set('core.themes', ['atom-dark-ui', 'atom-dark-syntax']);
  148. let didChangeActiveThemesHandler;
  149. atom.themes.onDidChangeActiveThemes(
  150. (didChangeActiveThemesHandler = jasmine.createSpy())
  151. );
  152. waitsForPromise(() => atom.themes.activateThemes());
  153. const workspaceElement = atom.workspace.getElement();
  154. runs(function() {
  155. expect(workspaceElement).toHaveClass('theme-atom-dark-ui');
  156. atom.themes.onDidChangeActiveThemes(
  157. (didChangeActiveThemesHandler = jasmine.createSpy())
  158. );
  159. atom.config.set('core.themes', [
  160. 'theme-with-ui-variables',
  161. 'theme-with-syntax-variables'
  162. ]);
  163. });
  164. waitsFor(() => didChangeActiveThemesHandler.callCount > 0);
  165. runs(function() {
  166. // `theme-` twice as it prefixes the name with `theme-`
  167. expect(workspaceElement).toHaveClass('theme-theme-with-ui-variables');
  168. expect(workspaceElement).toHaveClass(
  169. 'theme-theme-with-syntax-variables'
  170. );
  171. expect(workspaceElement).not.toHaveClass('theme-atom-dark-ui');
  172. expect(workspaceElement).not.toHaveClass('theme-atom-dark-syntax');
  173. });
  174. });
  175. });
  176. describe('when a theme fails to load', () =>
  177. it('logs a warning', function() {
  178. console.warn.reset();
  179. atom.packages
  180. .activatePackage('a-theme-that-will-not-be-found')
  181. .then(function() {}, function() {});
  182. expect(console.warn.callCount).toBe(1);
  183. expect(console.warn.argsForCall[0][0]).toContain(
  184. "Could not resolve 'a-theme-that-will-not-be-found'"
  185. );
  186. }));
  187. describe('::requireStylesheet(path)', function() {
  188. beforeEach(() => jasmine.snapshotDeprecations());
  189. afterEach(() => jasmine.restoreDeprecationsSnapshot());
  190. it('synchronously loads css at the given path and installs a style tag for it in the head', function() {
  191. let styleElementAddedHandler;
  192. atom.styles.onDidAddStyleElement(
  193. (styleElementAddedHandler = jasmine.createSpy(
  194. 'styleElementAddedHandler'
  195. ))
  196. );
  197. const cssPath = getAbsolutePath(
  198. atom.project.getDirectories()[0],
  199. 'css.css'
  200. );
  201. const lengthBefore = document.querySelectorAll('head style').length;
  202. atom.themes.requireStylesheet(cssPath);
  203. expect(document.querySelectorAll('head style').length).toBe(
  204. lengthBefore + 1
  205. );
  206. expect(styleElementAddedHandler).toHaveBeenCalled();
  207. const element = document.querySelector(
  208. 'head style[source-path*="css.css"]'
  209. );
  210. expect(element.getAttribute('source-path')).toEqualPath(cssPath);
  211. expect(element.textContent).toBe(fs.readFileSync(cssPath, 'utf8'));
  212. // doesn't append twice
  213. styleElementAddedHandler.reset();
  214. atom.themes.requireStylesheet(cssPath);
  215. expect(document.querySelectorAll('head style').length).toBe(
  216. lengthBefore + 1
  217. );
  218. expect(styleElementAddedHandler).not.toHaveBeenCalled();
  219. document
  220. .querySelectorAll('head style[id*="css.css"]')
  221. .forEach(styleElement => {
  222. styleElement.remove();
  223. });
  224. });
  225. it('synchronously loads and parses less files at the given path and installs a style tag for it in the head', function() {
  226. const lessPath = getAbsolutePath(
  227. atom.project.getDirectories()[0],
  228. 'sample.less'
  229. );
  230. const lengthBefore = document.querySelectorAll('head style').length;
  231. atom.themes.requireStylesheet(lessPath);
  232. expect(document.querySelectorAll('head style').length).toBe(
  233. lengthBefore + 1
  234. );
  235. const element = document.querySelector(
  236. 'head style[source-path*="sample.less"]'
  237. );
  238. expect(element.getAttribute('source-path')).toEqualPath(lessPath);
  239. expect(element.textContent.toLowerCase()).toBe(`\
  240. #header {
  241. color: #4d926f;
  242. }
  243. h2 {
  244. color: #4d926f;
  245. }
  246. \
  247. `);
  248. // doesn't append twice
  249. atom.themes.requireStylesheet(lessPath);
  250. expect(document.querySelectorAll('head style').length).toBe(
  251. lengthBefore + 1
  252. );
  253. document
  254. .querySelectorAll('head style[id*="sample.less"]')
  255. .forEach(styleElement => {
  256. styleElement.remove();
  257. });
  258. });
  259. it('supports requiring css and less stylesheets without an explicit extension', function() {
  260. atom.themes.requireStylesheet(path.join(__dirname, 'fixtures', 'css'));
  261. expect(
  262. document
  263. .querySelector('head style[source-path*="css.css"]')
  264. .getAttribute('source-path')
  265. ).toEqualPath(
  266. getAbsolutePath(atom.project.getDirectories()[0], 'css.css')
  267. );
  268. atom.themes.requireStylesheet(path.join(__dirname, 'fixtures', 'sample'));
  269. expect(
  270. document
  271. .querySelector('head style[source-path*="sample.less"]')
  272. .getAttribute('source-path')
  273. ).toEqualPath(
  274. getAbsolutePath(atom.project.getDirectories()[0], 'sample.less')
  275. );
  276. document.querySelector('head style[source-path*="css.css"]').remove();
  277. document.querySelector('head style[source-path*="sample.less"]').remove();
  278. });
  279. it('returns a disposable allowing styles applied by the given path to be removed', function() {
  280. const cssPath = require.resolve('./fixtures/css.css');
  281. expect(getComputedStyle(document.body).fontWeight).not.toBe('700');
  282. const disposable = atom.themes.requireStylesheet(cssPath);
  283. expect(getComputedStyle(document.body).fontWeight).toBe('700');
  284. let styleElementRemovedHandler;
  285. atom.styles.onDidRemoveStyleElement(
  286. (styleElementRemovedHandler = jasmine.createSpy(
  287. 'styleElementRemovedHandler'
  288. ))
  289. );
  290. disposable.dispose();
  291. expect(getComputedStyle(document.body).fontWeight).not.toBe('bold');
  292. expect(styleElementRemovedHandler).toHaveBeenCalled();
  293. });
  294. });
  295. describe('base style sheet loading', function() {
  296. beforeEach(function() {
  297. const workspaceElement = atom.workspace.getElement();
  298. jasmine.attachToDOM(atom.workspace.getElement());
  299. workspaceElement.appendChild(document.createElement('atom-text-editor'));
  300. waitsForPromise(() => atom.themes.activateThemes());
  301. });
  302. it("loads the correct values from the theme's ui-variables file", function() {
  303. let didChangeActiveThemesHandler;
  304. atom.themes.onDidChangeActiveThemes(
  305. (didChangeActiveThemesHandler = jasmine.createSpy())
  306. );
  307. atom.config.set('core.themes', [
  308. 'theme-with-ui-variables',
  309. 'theme-with-syntax-variables'
  310. ]);
  311. waitsFor(() => didChangeActiveThemesHandler.callCount > 0);
  312. runs(function() {
  313. // an override loaded in the base css
  314. expect(
  315. getComputedStyle(atom.workspace.getElement())['background-color']
  316. ).toBe('rgb(0, 0, 255)');
  317. // from within the theme itself
  318. expect(
  319. getComputedStyle(document.querySelector('atom-text-editor'))
  320. .paddingTop
  321. ).toBe('150px');
  322. expect(
  323. getComputedStyle(document.querySelector('atom-text-editor'))
  324. .paddingRight
  325. ).toBe('150px');
  326. expect(
  327. getComputedStyle(document.querySelector('atom-text-editor'))
  328. .paddingBottom
  329. ).toBe('150px');
  330. });
  331. });
  332. describe('when there is a theme with incomplete variables', () =>
  333. it('loads the correct values from the fallback ui-variables', function() {
  334. let didChangeActiveThemesHandler;
  335. atom.themes.onDidChangeActiveThemes(
  336. (didChangeActiveThemesHandler = jasmine.createSpy())
  337. );
  338. atom.config.set('core.themes', [
  339. 'theme-with-incomplete-ui-variables',
  340. 'theme-with-syntax-variables'
  341. ]);
  342. waitsFor(() => didChangeActiveThemesHandler.callCount > 0);
  343. runs(function() {
  344. // an override loaded in the base css
  345. expect(
  346. getComputedStyle(atom.workspace.getElement())['background-color']
  347. ).toBe('rgb(0, 0, 255)');
  348. // from within the theme itself
  349. expect(
  350. getComputedStyle(document.querySelector('atom-text-editor'))
  351. .backgroundColor
  352. ).toBe('rgb(0, 152, 255)');
  353. });
  354. }));
  355. });
  356. describe('user stylesheet', function() {
  357. let userStylesheetPath;
  358. beforeEach(function() {
  359. userStylesheetPath = path.join(temp.mkdirSync('atom'), 'styles.less');
  360. fs.writeFileSync(
  361. userStylesheetPath,
  362. 'body {border-style: dotted !important;}'
  363. );
  364. spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(userStylesheetPath);
  365. });
  366. describe('when the user stylesheet changes', function() {
  367. beforeEach(() => jasmine.snapshotDeprecations());
  368. afterEach(() => jasmine.restoreDeprecationsSnapshot());
  369. it('reloads it', function() {
  370. let styleElementAddedHandler, styleElementRemovedHandler;
  371. waitsForPromise(() => atom.themes.activateThemes());
  372. runs(function() {
  373. atom.styles.onDidRemoveStyleElement(
  374. (styleElementRemovedHandler = jasmine.createSpy(
  375. 'styleElementRemovedHandler'
  376. ))
  377. );
  378. atom.styles.onDidAddStyleElement(
  379. (styleElementAddedHandler = jasmine.createSpy(
  380. 'styleElementAddedHandler'
  381. ))
  382. );
  383. spyOn(atom.themes, 'loadUserStylesheet').andCallThrough();
  384. expect(getComputedStyle(document.body).borderStyle).toBe('dotted');
  385. fs.writeFileSync(userStylesheetPath, 'body {border-style: dashed}');
  386. });
  387. waitsFor(() => atom.themes.loadUserStylesheet.callCount === 1);
  388. runs(function() {
  389. expect(getComputedStyle(document.body).borderStyle).toBe('dashed');
  390. expect(styleElementRemovedHandler).toHaveBeenCalled();
  391. expect(
  392. styleElementRemovedHandler.argsForCall[0][0].textContent
  393. ).toContain('dotted');
  394. expect(styleElementAddedHandler).toHaveBeenCalled();
  395. expect(
  396. styleElementAddedHandler.argsForCall[0][0].textContent
  397. ).toContain('dashed');
  398. styleElementRemovedHandler.reset();
  399. fs.removeSync(userStylesheetPath);
  400. });
  401. waitsFor(() => atom.themes.loadUserStylesheet.callCount === 2);
  402. runs(function() {
  403. expect(styleElementRemovedHandler).toHaveBeenCalled();
  404. expect(
  405. styleElementRemovedHandler.argsForCall[0][0].textContent
  406. ).toContain('dashed');
  407. expect(getComputedStyle(document.body).borderStyle).toBe('none');
  408. });
  409. });
  410. });
  411. describe('when there is an error reading the stylesheet', function() {
  412. let addErrorHandler = null;
  413. beforeEach(function() {
  414. atom.themes.loadUserStylesheet();
  415. spyOn(atom.themes.lessCache, 'cssForFile').andCallFake(function() {
  416. throw new Error('EACCES permission denied "styles.less"');
  417. });
  418. atom.notifications.onDidAddNotification(
  419. (addErrorHandler = jasmine.createSpy())
  420. );
  421. });
  422. it('creates an error notification and does not add the stylesheet', function() {
  423. atom.themes.loadUserStylesheet();
  424. expect(addErrorHandler).toHaveBeenCalled();
  425. const note = addErrorHandler.mostRecentCall.args[0];
  426. expect(note.getType()).toBe('error');
  427. expect(note.getMessage()).toContain('Error loading');
  428. expect(
  429. atom.styles.styleElementsBySourcePath[
  430. atom.styles.getUserStyleSheetPath()
  431. ]
  432. ).toBeUndefined();
  433. });
  434. });
  435. describe('when there is an error watching the user stylesheet', function() {
  436. let addErrorHandler = null;
  437. beforeEach(function() {
  438. const { File } = require('pathwatcher');
  439. spyOn(File.prototype, 'on').andCallFake(function(event) {
  440. if (event.indexOf('contents-changed') > -1) {
  441. throw new Error('Unable to watch path');
  442. }
  443. });
  444. spyOn(atom.themes, 'loadStylesheet').andReturn('');
  445. atom.notifications.onDidAddNotification(
  446. (addErrorHandler = jasmine.createSpy())
  447. );
  448. });
  449. it('creates an error notification', function() {
  450. atom.themes.loadUserStylesheet();
  451. expect(addErrorHandler).toHaveBeenCalled();
  452. const note = addErrorHandler.mostRecentCall.args[0];
  453. expect(note.getType()).toBe('error');
  454. expect(note.getMessage()).toContain('Unable to watch path');
  455. });
  456. });
  457. it("adds a notification when a theme's stylesheet is invalid", function() {
  458. const addErrorHandler = jasmine.createSpy();
  459. atom.notifications.onDidAddNotification(addErrorHandler);
  460. expect(() =>
  461. atom.packages
  462. .activatePackage('theme-with-invalid-styles')
  463. .then(function() {}, function() {})
  464. ).not.toThrow();
  465. expect(addErrorHandler.callCount).toBe(2);
  466. expect(addErrorHandler.argsForCall[1][0].message).toContain(
  467. 'Failed to activate the theme-with-invalid-styles theme'
  468. );
  469. });
  470. });
  471. describe('when a non-existent theme is present in the config', function() {
  472. beforeEach(function() {
  473. console.warn.reset();
  474. atom.config.set('core.themes', [
  475. 'non-existent-dark-ui',
  476. 'non-existent-dark-syntax'
  477. ]);
  478. waitsForPromise(() => atom.themes.activateThemes());
  479. });
  480. it('uses the default one-dark UI and syntax themes and logs a warning', function() {
  481. const activeThemeNames = atom.themes.getActiveThemeNames();
  482. expect(console.warn.callCount).toBe(2);
  483. expect(activeThemeNames.length).toBe(2);
  484. expect(activeThemeNames).toContain('one-dark-ui');
  485. expect(activeThemeNames).toContain('one-dark-syntax');
  486. });
  487. });
  488. describe('when in safe mode', function() {
  489. describe('when the enabled UI and syntax themes are bundled with Atom', function() {
  490. beforeEach(function() {
  491. atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-syntax']);
  492. waitsForPromise(() => atom.themes.activateThemes());
  493. });
  494. it('uses the enabled themes', function() {
  495. const activeThemeNames = atom.themes.getActiveThemeNames();
  496. expect(activeThemeNames.length).toBe(2);
  497. expect(activeThemeNames).toContain('atom-light-ui');
  498. expect(activeThemeNames).toContain('atom-dark-syntax');
  499. });
  500. });
  501. describe('when the enabled UI and syntax themes are not bundled with Atom', function() {
  502. beforeEach(function() {
  503. atom.config.set('core.themes', [
  504. 'installed-dark-ui',
  505. 'installed-dark-syntax'
  506. ]);
  507. waitsForPromise(() => atom.themes.activateThemes());
  508. });
  509. it('uses the default dark UI and syntax themes', function() {
  510. const activeThemeNames = atom.themes.getActiveThemeNames();
  511. expect(activeThemeNames.length).toBe(2);
  512. expect(activeThemeNames).toContain('one-dark-ui');
  513. expect(activeThemeNames).toContain('one-dark-syntax');
  514. });
  515. });
  516. describe('when the enabled UI theme is not bundled with Atom', function() {
  517. beforeEach(function() {
  518. atom.config.set('core.themes', [
  519. 'installed-dark-ui',
  520. 'atom-light-syntax'
  521. ]);
  522. waitsForPromise(() => atom.themes.activateThemes());
  523. });
  524. it('uses the default one-dark UI theme', function() {
  525. const activeThemeNames = atom.themes.getActiveThemeNames();
  526. expect(activeThemeNames.length).toBe(2);
  527. expect(activeThemeNames).toContain('one-dark-ui');
  528. expect(activeThemeNames).toContain('atom-light-syntax');
  529. });
  530. });
  531. describe('when the enabled syntax theme is not bundled with Atom', function() {
  532. beforeEach(function() {
  533. atom.config.set('core.themes', [
  534. 'atom-light-ui',
  535. 'installed-dark-syntax'
  536. ]);
  537. waitsForPromise(() => atom.themes.activateThemes());
  538. });
  539. it('uses the default one-dark syntax theme', function() {
  540. const activeThemeNames = atom.themes.getActiveThemeNames();
  541. expect(activeThemeNames.length).toBe(2);
  542. expect(activeThemeNames).toContain('atom-light-ui');
  543. expect(activeThemeNames).toContain('one-dark-syntax');
  544. });
  545. });
  546. });
  547. });
  548. function getAbsolutePath(directory, relativePath) {
  549. if (directory) {
  550. return directory.resolve(relativePath);
  551. }
  552. }