const path = require('path'); const fs = require('fs-plus'); const temp = require('temp').track(); describe('atom.themes', function() { beforeEach(function() { spyOn(atom, 'inSpecMode').andReturn(false); spyOn(console, 'warn'); }); afterEach(function() { waitsForPromise(() => atom.themes.deactivateThemes()); runs(function() { try { temp.cleanupSync(); } catch (error) {} }); }); describe('theme getters and setters', function() { beforeEach(function() { jasmine.snapshotDeprecations(); atom.packages.loadPackages(); }); afterEach(() => jasmine.restoreDeprecationsSnapshot()); describe('getLoadedThemes', () => it('gets all the loaded themes', function() { const themes = atom.themes.getLoadedThemes(); expect(themes.length).toBeGreaterThan(2); })); describe('getActiveThemes', () => it('gets all the active themes', function() { waitsForPromise(() => atom.themes.activateThemes()); runs(function() { const names = atom.config.get('core.themes'); expect(names.length).toBeGreaterThan(0); const themes = atom.themes.getActiveThemes(); expect(themes).toHaveLength(names.length); }); })); }); describe('when the core.themes config value contains invalid entry', () => it('ignores theme', function() { atom.config.set('core.themes', [ 'atom-light-ui', null, undefined, '', false, 4, {}, [], 'atom-dark-ui' ]); expect(atom.themes.getEnabledThemeNames()).toEqual([ 'atom-dark-ui', 'atom-light-ui' ]); })); describe('::getImportPaths()', function() { it('returns the theme directories before the themes are loaded', function() { atom.config.set('core.themes', [ 'theme-with-index-less', 'atom-dark-ui', 'atom-light-ui' ]); const paths = atom.themes.getImportPaths(); // syntax theme is not a dir at this time, so only two. expect(paths.length).toBe(2); expect(paths[0]).toContain('atom-light-ui'); expect(paths[1]).toContain('atom-dark-ui'); }); it('ignores themes that cannot be resolved to a directory', function() { atom.config.set('core.themes', ['definitely-not-a-theme']); expect(() => atom.themes.getImportPaths()).not.toThrow(); }); }); describe('when the core.themes config value changes', function() { it('add/removes stylesheets to reflect the new config value', function() { let didChangeActiveThemesHandler; atom.themes.onDidChangeActiveThemes( (didChangeActiveThemesHandler = jasmine.createSpy()) ); spyOn(atom.styles, 'getUserStyleSheetPath').andCallFake(() => null); waitsForPromise(() => atom.themes.activateThemes()); runs(function() { didChangeActiveThemesHandler.reset(); atom.config.set('core.themes', []); }); waitsFor('a', () => didChangeActiveThemesHandler.callCount === 1); runs(function() { didChangeActiveThemesHandler.reset(); expect(document.querySelectorAll('style.theme')).toHaveLength(0); atom.config.set('core.themes', ['atom-dark-ui']); }); waitsFor('b', () => didChangeActiveThemesHandler.callCount === 1); runs(function() { didChangeActiveThemesHandler.reset(); expect(document.querySelectorAll('style[priority="1"]')).toHaveLength( 2 ); expect( document .querySelector('style[priority="1"]') .getAttribute('source-path') ).toMatch(/atom-dark-ui/); atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-ui']); }); waitsFor('c', () => didChangeActiveThemesHandler.callCount === 1); runs(function() { didChangeActiveThemesHandler.reset(); expect(document.querySelectorAll('style[priority="1"]')).toHaveLength( 2 ); expect( document .querySelectorAll('style[priority="1"]')[0] .getAttribute('source-path') ).toMatch(/atom-dark-ui/); expect( document .querySelectorAll('style[priority="1"]')[1] .getAttribute('source-path') ).toMatch(/atom-light-ui/); atom.config.set('core.themes', []); }); waitsFor(() => didChangeActiveThemesHandler.callCount === 1); runs(function() { didChangeActiveThemesHandler.reset(); expect(document.querySelectorAll('style[priority="1"]')).toHaveLength( 2 ); // atom-dark-ui has a directory path, the syntax one doesn't atom.config.set('core.themes', [ 'theme-with-index-less', 'atom-dark-ui' ]); }); waitsFor(() => didChangeActiveThemesHandler.callCount === 1); runs(function() { expect(document.querySelectorAll('style[priority="1"]')).toHaveLength( 2 ); const importPaths = atom.themes.getImportPaths(); expect(importPaths.length).toBe(1); expect(importPaths[0]).toContain('atom-dark-ui'); }); }); it('adds theme-* classes to the workspace for each active theme', function() { atom.config.set('core.themes', ['atom-dark-ui', 'atom-dark-syntax']); let didChangeActiveThemesHandler; atom.themes.onDidChangeActiveThemes( (didChangeActiveThemesHandler = jasmine.createSpy()) ); waitsForPromise(() => atom.themes.activateThemes()); const workspaceElement = atom.workspace.getElement(); runs(function() { expect(workspaceElement).toHaveClass('theme-atom-dark-ui'); atom.themes.onDidChangeActiveThemes( (didChangeActiveThemesHandler = jasmine.createSpy()) ); atom.config.set('core.themes', [ 'theme-with-ui-variables', 'theme-with-syntax-variables' ]); }); waitsFor(() => didChangeActiveThemesHandler.callCount > 0); runs(function() { // `theme-` twice as it prefixes the name with `theme-` expect(workspaceElement).toHaveClass('theme-theme-with-ui-variables'); expect(workspaceElement).toHaveClass( 'theme-theme-with-syntax-variables' ); expect(workspaceElement).not.toHaveClass('theme-atom-dark-ui'); expect(workspaceElement).not.toHaveClass('theme-atom-dark-syntax'); }); }); }); describe('when a theme fails to load', () => it('logs a warning', function() { console.warn.reset(); atom.packages .activatePackage('a-theme-that-will-not-be-found') .then(function() {}, function() {}); expect(console.warn.callCount).toBe(1); expect(console.warn.argsForCall[0][0]).toContain( "Could not resolve 'a-theme-that-will-not-be-found'" ); })); describe('::requireStylesheet(path)', function() { beforeEach(() => jasmine.snapshotDeprecations()); afterEach(() => jasmine.restoreDeprecationsSnapshot()); it('synchronously loads css at the given path and installs a style tag for it in the head', function() { let styleElementAddedHandler; atom.styles.onDidAddStyleElement( (styleElementAddedHandler = jasmine.createSpy( 'styleElementAddedHandler' )) ); const cssPath = getAbsolutePath( atom.project.getDirectories()[0], 'css.css' ); const lengthBefore = document.querySelectorAll('head style').length; atom.themes.requireStylesheet(cssPath); expect(document.querySelectorAll('head style').length).toBe( lengthBefore + 1 ); expect(styleElementAddedHandler).toHaveBeenCalled(); const element = document.querySelector( 'head style[source-path*="css.css"]' ); expect(element.getAttribute('source-path')).toEqualPath(cssPath); expect(element.textContent).toBe(fs.readFileSync(cssPath, 'utf8')); // doesn't append twice styleElementAddedHandler.reset(); atom.themes.requireStylesheet(cssPath); expect(document.querySelectorAll('head style').length).toBe( lengthBefore + 1 ); expect(styleElementAddedHandler).not.toHaveBeenCalled(); document .querySelectorAll('head style[id*="css.css"]') .forEach(styleElement => { styleElement.remove(); }); }); it('synchronously loads and parses less files at the given path and installs a style tag for it in the head', function() { const lessPath = getAbsolutePath( atom.project.getDirectories()[0], 'sample.less' ); const lengthBefore = document.querySelectorAll('head style').length; atom.themes.requireStylesheet(lessPath); expect(document.querySelectorAll('head style').length).toBe( lengthBefore + 1 ); const element = document.querySelector( 'head style[source-path*="sample.less"]' ); expect(element.getAttribute('source-path')).toEqualPath(lessPath); expect(element.textContent.toLowerCase()).toBe(`\ #header { color: #4d926f; } h2 { color: #4d926f; } \ `); // doesn't append twice atom.themes.requireStylesheet(lessPath); expect(document.querySelectorAll('head style').length).toBe( lengthBefore + 1 ); document .querySelectorAll('head style[id*="sample.less"]') .forEach(styleElement => { styleElement.remove(); }); }); it('supports requiring css and less stylesheets without an explicit extension', function() { atom.themes.requireStylesheet(path.join(__dirname, 'fixtures', 'css')); expect( document .querySelector('head style[source-path*="css.css"]') .getAttribute('source-path') ).toEqualPath( getAbsolutePath(atom.project.getDirectories()[0], 'css.css') ); atom.themes.requireStylesheet(path.join(__dirname, 'fixtures', 'sample')); expect( document .querySelector('head style[source-path*="sample.less"]') .getAttribute('source-path') ).toEqualPath( getAbsolutePath(atom.project.getDirectories()[0], 'sample.less') ); document.querySelector('head style[source-path*="css.css"]').remove(); document.querySelector('head style[source-path*="sample.less"]').remove(); }); it('returns a disposable allowing styles applied by the given path to be removed', function() { const cssPath = require.resolve('./fixtures/css.css'); expect(getComputedStyle(document.body).fontWeight).not.toBe('700'); const disposable = atom.themes.requireStylesheet(cssPath); expect(getComputedStyle(document.body).fontWeight).toBe('700'); let styleElementRemovedHandler; atom.styles.onDidRemoveStyleElement( (styleElementRemovedHandler = jasmine.createSpy( 'styleElementRemovedHandler' )) ); disposable.dispose(); expect(getComputedStyle(document.body).fontWeight).not.toBe('bold'); expect(styleElementRemovedHandler).toHaveBeenCalled(); }); }); describe('base style sheet loading', function() { beforeEach(function() { const workspaceElement = atom.workspace.getElement(); jasmine.attachToDOM(atom.workspace.getElement()); workspaceElement.appendChild(document.createElement('atom-text-editor')); waitsForPromise(() => atom.themes.activateThemes()); }); it("loads the correct values from the theme's ui-variables file", function() { let didChangeActiveThemesHandler; atom.themes.onDidChangeActiveThemes( (didChangeActiveThemesHandler = jasmine.createSpy()) ); atom.config.set('core.themes', [ 'theme-with-ui-variables', 'theme-with-syntax-variables' ]); waitsFor(() => didChangeActiveThemesHandler.callCount > 0); runs(function() { // an override loaded in the base css expect( getComputedStyle(atom.workspace.getElement())['background-color'] ).toBe('rgb(0, 0, 255)'); // from within the theme itself expect( getComputedStyle(document.querySelector('atom-text-editor')) .paddingTop ).toBe('150px'); expect( getComputedStyle(document.querySelector('atom-text-editor')) .paddingRight ).toBe('150px'); expect( getComputedStyle(document.querySelector('atom-text-editor')) .paddingBottom ).toBe('150px'); }); }); describe('when there is a theme with incomplete variables', () => it('loads the correct values from the fallback ui-variables', function() { let didChangeActiveThemesHandler; atom.themes.onDidChangeActiveThemes( (didChangeActiveThemesHandler = jasmine.createSpy()) ); atom.config.set('core.themes', [ 'theme-with-incomplete-ui-variables', 'theme-with-syntax-variables' ]); waitsFor(() => didChangeActiveThemesHandler.callCount > 0); runs(function() { // an override loaded in the base css expect( getComputedStyle(atom.workspace.getElement())['background-color'] ).toBe('rgb(0, 0, 255)'); // from within the theme itself expect( getComputedStyle(document.querySelector('atom-text-editor')) .backgroundColor ).toBe('rgb(0, 152, 255)'); }); })); }); describe('user stylesheet', function() { let userStylesheetPath; beforeEach(function() { userStylesheetPath = path.join(temp.mkdirSync('atom'), 'styles.less'); fs.writeFileSync( userStylesheetPath, 'body {border-style: dotted !important;}' ); spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(userStylesheetPath); }); describe('when the user stylesheet changes', function() { beforeEach(() => jasmine.snapshotDeprecations()); afterEach(() => jasmine.restoreDeprecationsSnapshot()); it('reloads it', function() { let styleElementAddedHandler, styleElementRemovedHandler; waitsForPromise(() => atom.themes.activateThemes()); runs(function() { atom.styles.onDidRemoveStyleElement( (styleElementRemovedHandler = jasmine.createSpy( 'styleElementRemovedHandler' )) ); atom.styles.onDidAddStyleElement( (styleElementAddedHandler = jasmine.createSpy( 'styleElementAddedHandler' )) ); spyOn(atom.themes, 'loadUserStylesheet').andCallThrough(); expect(getComputedStyle(document.body).borderStyle).toBe('dotted'); fs.writeFileSync(userStylesheetPath, 'body {border-style: dashed}'); }); waitsFor(() => atom.themes.loadUserStylesheet.callCount === 1); runs(function() { expect(getComputedStyle(document.body).borderStyle).toBe('dashed'); expect(styleElementRemovedHandler).toHaveBeenCalled(); expect( styleElementRemovedHandler.argsForCall[0][0].textContent ).toContain('dotted'); expect(styleElementAddedHandler).toHaveBeenCalled(); expect( styleElementAddedHandler.argsForCall[0][0].textContent ).toContain('dashed'); styleElementRemovedHandler.reset(); fs.removeSync(userStylesheetPath); }); waitsFor(() => atom.themes.loadUserStylesheet.callCount === 2); runs(function() { expect(styleElementRemovedHandler).toHaveBeenCalled(); expect( styleElementRemovedHandler.argsForCall[0][0].textContent ).toContain('dashed'); expect(getComputedStyle(document.body).borderStyle).toBe('none'); }); }); }); describe('when there is an error reading the stylesheet', function() { let addErrorHandler = null; beforeEach(function() { atom.themes.loadUserStylesheet(); spyOn(atom.themes.lessCache, 'cssForFile').andCallFake(function() { throw new Error('EACCES permission denied "styles.less"'); }); atom.notifications.onDidAddNotification( (addErrorHandler = jasmine.createSpy()) ); }); it('creates an error notification and does not add the stylesheet', function() { atom.themes.loadUserStylesheet(); expect(addErrorHandler).toHaveBeenCalled(); const note = addErrorHandler.mostRecentCall.args[0]; expect(note.getType()).toBe('error'); expect(note.getMessage()).toContain('Error loading'); expect( atom.styles.styleElementsBySourcePath[ atom.styles.getUserStyleSheetPath() ] ).toBeUndefined(); }); }); describe('when there is an error watching the user stylesheet', function() { let addErrorHandler = null; beforeEach(function() { const { File } = require('pathwatcher'); spyOn(File.prototype, 'on').andCallFake(function(event) { if (event.indexOf('contents-changed') > -1) { throw new Error('Unable to watch path'); } }); spyOn(atom.themes, 'loadStylesheet').andReturn(''); atom.notifications.onDidAddNotification( (addErrorHandler = jasmine.createSpy()) ); }); it('creates an error notification', function() { atom.themes.loadUserStylesheet(); expect(addErrorHandler).toHaveBeenCalled(); const note = addErrorHandler.mostRecentCall.args[0]; expect(note.getType()).toBe('error'); expect(note.getMessage()).toContain('Unable to watch path'); }); }); it("adds a notification when a theme's stylesheet is invalid", function() { const addErrorHandler = jasmine.createSpy(); atom.notifications.onDidAddNotification(addErrorHandler); expect(() => atom.packages .activatePackage('theme-with-invalid-styles') .then(function() {}, function() {}) ).not.toThrow(); expect(addErrorHandler.callCount).toBe(2); expect(addErrorHandler.argsForCall[1][0].message).toContain( 'Failed to activate the theme-with-invalid-styles theme' ); }); }); describe('when a non-existent theme is present in the config', function() { beforeEach(function() { console.warn.reset(); atom.config.set('core.themes', [ 'non-existent-dark-ui', 'non-existent-dark-syntax' ]); waitsForPromise(() => atom.themes.activateThemes()); }); it('uses the default one-dark UI and syntax themes and logs a warning', function() { const activeThemeNames = atom.themes.getActiveThemeNames(); expect(console.warn.callCount).toBe(2); expect(activeThemeNames.length).toBe(2); expect(activeThemeNames).toContain('one-dark-ui'); expect(activeThemeNames).toContain('one-dark-syntax'); }); }); describe('when in safe mode', function() { describe('when the enabled UI and syntax themes are bundled with Atom', function() { beforeEach(function() { atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-syntax']); waitsForPromise(() => atom.themes.activateThemes()); }); it('uses the enabled themes', function() { const activeThemeNames = atom.themes.getActiveThemeNames(); expect(activeThemeNames.length).toBe(2); expect(activeThemeNames).toContain('atom-light-ui'); expect(activeThemeNames).toContain('atom-dark-syntax'); }); }); describe('when the enabled UI and syntax themes are not bundled with Atom', function() { beforeEach(function() { atom.config.set('core.themes', [ 'installed-dark-ui', 'installed-dark-syntax' ]); waitsForPromise(() => atom.themes.activateThemes()); }); it('uses the default dark UI and syntax themes', function() { const activeThemeNames = atom.themes.getActiveThemeNames(); expect(activeThemeNames.length).toBe(2); expect(activeThemeNames).toContain('one-dark-ui'); expect(activeThemeNames).toContain('one-dark-syntax'); }); }); describe('when the enabled UI theme is not bundled with Atom', function() { beforeEach(function() { atom.config.set('core.themes', [ 'installed-dark-ui', 'atom-light-syntax' ]); waitsForPromise(() => atom.themes.activateThemes()); }); it('uses the default one-dark UI theme', function() { const activeThemeNames = atom.themes.getActiveThemeNames(); expect(activeThemeNames.length).toBe(2); expect(activeThemeNames).toContain('one-dark-ui'); expect(activeThemeNames).toContain('atom-light-syntax'); }); }); describe('when the enabled syntax theme is not bundled with Atom', function() { beforeEach(function() { atom.config.set('core.themes', [ 'atom-light-ui', 'installed-dark-syntax' ]); waitsForPromise(() => atom.themes.activateThemes()); }); it('uses the default one-dark syntax theme', function() { const activeThemeNames = atom.themes.getActiveThemeNames(); expect(activeThemeNames.length).toBe(2); expect(activeThemeNames).toContain('atom-light-ui'); expect(activeThemeNames).toContain('one-dark-syntax'); }); }); }); }); function getAbsolutePath(directory, relativePath) { if (directory) { return directory.resolve(relativePath); } }