/** @babel */ /* global advanceClock, HTMLElement, waits */ const path = require('path') const temp = require('temp').track() const TextEditor = require('../src/text-editor') const Workspace = require('../src/workspace') const Project = require('../src/project') const platform = require('./spec-helper-platform') const _ = require('underscore-plus') const fstream = require('fstream') const fs = require('fs-plus') const AtomEnvironment = require('../src/atom-environment') const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') describe('Workspace', () => { let workspace let setDocumentEdited beforeEach(() => { workspace = atom.workspace workspace.resetFontSize() spyOn(atom.applicationDelegate, 'confirm') setDocumentEdited = spyOn(atom.applicationDelegate, 'setWindowDocumentEdited') atom.project.setPaths([atom.project.getDirectories()[0].resolve('dir')]) waits(1) waitsForPromise(() => atom.workspace.itemLocationStore.clear()) }) afterEach(() => temp.cleanupSync()) describe('serialization', () => { const simulateReload = () => { const workspaceState = atom.workspace.serialize() const projectState = atom.project.serialize({isUnloading: true}) atom.workspace.destroy() atom.project.destroy() atom.project = new Project({ notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom), applicationDelegate: atom.applicationDelegate }) atom.project.deserialize(projectState) atom.workspace = new Workspace({ config: atom.config, project: atom.project, packageManager: atom.packages, grammarRegistry: atom.grammars, styleManager: atom.styles, deserializerManager: atom.deserializers, notificationManager: atom.notifications, applicationDelegate: atom.applicationDelegate, viewRegistry: atom.views, assert: atom.assert.bind(atom), textEditorRegistry: atom.textEditors }) return atom.workspace.deserialize(workspaceState, atom.deserializers) } describe('when the workspace contains text editors', () => { it('constructs the view with the same panes', () => { const pane1 = atom.workspace.getActivePane() const pane2 = pane1.splitRight({copyActiveItem: true}) const pane3 = pane2.splitRight({copyActiveItem: true}) let pane4 = null waitsForPromise(() => atom.workspace.open(null).then(editor => editor.setText('An untitled editor.'))) waitsForPromise(() => atom.workspace.open('b').then(editor => pane2.activateItem(editor.copy())) ) waitsForPromise(() => atom.workspace.open('../sample.js').then(editor => pane3.activateItem(editor)) ) runs(() => { pane3.activeItem.setCursorScreenPosition([2, 4]) pane4 = pane2.splitDown() }) waitsForPromise(() => atom.workspace.open('../sample.txt').then(editor => pane4.activateItem(editor)) ) runs(() => { pane4.getActiveItem().setCursorScreenPosition([0, 2]) pane2.activate() simulateReload() expect(atom.workspace.getTextEditors().length).toBe(5) const [editor1, editor2, untitledEditor, editor3, editor4] = atom.workspace.getTextEditors() const firstDirectory = atom.project.getDirectories()[0] expect(firstDirectory).toBeDefined() expect(editor1.getPath()).toBe(firstDirectory.resolve('b')) expect(editor2.getPath()).toBe(firstDirectory.resolve('../sample.txt')) expect(editor2.getCursorScreenPosition()).toEqual([0, 2]) expect(editor3.getPath()).toBe(firstDirectory.resolve('b')) expect(editor4.getPath()).toBe(firstDirectory.resolve('../sample.js')) expect(editor4.getCursorScreenPosition()).toEqual([2, 4]) expect(untitledEditor.getPath()).toBeUndefined() expect(untitledEditor.getText()).toBe('An untitled editor.') expect(atom.workspace.getActiveTextEditor().getPath()).toBe(editor3.getPath()) const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) expect(document.title).toMatch(new RegExp(`^${path.basename(editor3.getLongTitle())} \\u2014 ${pathEscaped}`)) }) }) }) describe('where there are no open panes or editors', () => { it('constructs the view with no open editors', () => { atom.workspace.getActivePane().destroy() expect(atom.workspace.getTextEditors().length).toBe(0) simulateReload() expect(atom.workspace.getTextEditors().length).toBe(0) }) }) describe('where a dock contains an editor', () => { afterEach(() => { atom.workspace.getRightDock().paneContainer.destroy() }) it('constructs the view with the same panes', () => { const pane1 = atom.workspace.getRightDock().getActivePane() const pane2 = pane1.splitRight({copyActiveItem: true}) const pane3 = pane2.splitRight({copyActiveItem: true}) let pane4 = null waitsForPromise(() => atom.workspace.open(null, {location: 'right'}).then(editor => editor.setText('An untitled editor.')) ) waitsForPromise(() => atom.workspace.open('b', {location: 'right'}).then(editor => pane2.activateItem(editor.copy())) ) waitsForPromise(() => atom.workspace.open('../sample.js', {location: 'right'}).then(editor => pane3.activateItem(editor)) ) runs(() => { pane3.activeItem.setCursorScreenPosition([2, 4]) pane4 = pane2.splitDown() }) waitsForPromise(() => atom.workspace.open('../sample.txt', {location: 'right'}).then(editor => pane4.activateItem(editor)) ) runs(() => { pane4.getActiveItem().setCursorScreenPosition([0, 2]) pane2.activate() simulateReload() expect(atom.workspace.getTextEditors().length).toBe(5) const [editor1, editor2, untitledEditor, editor3, editor4] = atom.workspace.getTextEditors() const firstDirectory = atom.project.getDirectories()[0] expect(firstDirectory).toBeDefined() expect(editor1.getPath()).toBe(firstDirectory.resolve('b')) expect(editor2.getPath()).toBe(firstDirectory.resolve('../sample.txt')) expect(editor2.getCursorScreenPosition()).toEqual([0, 2]) expect(editor3.getPath()).toBe(firstDirectory.resolve('b')) expect(editor4.getPath()).toBe(firstDirectory.resolve('../sample.js')) expect(editor4.getCursorScreenPosition()).toEqual([2, 4]) expect(untitledEditor.getPath()).toBeUndefined() expect(untitledEditor.getText()).toBe('An untitled editor.') expect(atom.workspace.getRightDock().getActiveTextEditor().getPath()).toBe(editor3.getPath()) }) }) }) }) describe('::open(itemOrURI, options)', () => { let openEvents = null beforeEach(() => { openEvents = [] workspace.onDidOpen(event => openEvents.push(event)) spyOn(workspace.getActivePane(), 'activate').andCallThrough() }) describe("when the 'searchAllPanes' option is false (default)", () => { describe('when called without a uri or item', () => { it('adds and activates an empty editor on the active pane', () => { let editor1 let editor2 waitsForPromise(() => workspace.open().then(editor => { editor1 = editor })) runs(() => { expect(editor1.getPath()).toBeUndefined() expect(workspace.getActivePane().items).toEqual([editor1]) expect(workspace.getActivePaneItem()).toBe(editor1) expect(workspace.getActivePane().activate).toHaveBeenCalled() expect(openEvents).toEqual([{uri: undefined, pane: workspace.getActivePane(), item: editor1, index: 0}]) openEvents = [] }) waitsForPromise(() => workspace.open().then(editor => { editor2 = editor })) runs(() => { expect(editor2.getPath()).toBeUndefined() expect(workspace.getActivePane().items).toEqual([editor1, editor2]) expect(workspace.getActivePaneItem()).toBe(editor2) expect(workspace.getActivePane().activate).toHaveBeenCalled() expect(openEvents).toEqual([{uri: undefined, pane: workspace.getActivePane(), item: editor2, index: 1}]) }) }) }) describe('when called with a uri', () => { describe('when the active pane already has an editor for the given uri', () => { it('activates the existing editor on the active pane', () => { let editor = null let editor1 = null let editor2 = null waitsForPromise(() => workspace.open('a').then(o => { editor1 = o return workspace.open('b').then(o => { editor2 = o return workspace.open('a').then(o => { editor = o }) }) }) ) runs(() => { expect(editor).toBe(editor1) expect(workspace.getActivePaneItem()).toBe(editor) expect(workspace.getActivePane().activate).toHaveBeenCalled() const firstDirectory = atom.project.getDirectories()[0] expect(firstDirectory).toBeDefined() expect(openEvents).toEqual([ { uri: firstDirectory.resolve('a'), item: editor1, pane: atom.workspace.getActivePane(), index: 0 }, { uri: firstDirectory.resolve('b'), item: editor2, pane: atom.workspace.getActivePane(), index: 1 }, { uri: firstDirectory.resolve('a'), item: editor1, pane: atom.workspace.getActivePane(), index: 0 } ]) }) }) it('finds items in docks', () => { const dock = atom.workspace.getRightDock() const ITEM_URI = 'atom://test' const item = { getURI: () => ITEM_URI, getDefaultLocation: () => 'left', getElement: () => document.createElement('div') } dock.getActivePane().addItem(item) expect(dock.getPaneItems()).toHaveLength(1) waitsForPromise(() => atom.workspace.open(ITEM_URI, {searchAllPanes: true})) runs(() => { expect(atom.workspace.getPaneItems()).toHaveLength(1) expect(dock.getPaneItems()).toHaveLength(1) expect(dock.getPaneItems()[0]).toBe(item) }) }) }) describe("when the 'activateItem' option is false", () => { it('adds the item to the workspace', () => { let editor waitsForPromise(() => workspace.open('a')) waitsForPromise(() => workspace.open('b', {activateItem: false}).then(o => { editor = o })) runs(() => { expect(workspace.getPaneItems()).toContain(editor) expect(workspace.getActivePaneItem()).not.toBe(editor) }) }) }) describe('when the active pane does not have an editor for the given uri', () => { beforeEach(() => { atom.workspace.enablePersistence = true }) afterEach(async () => { await atom.workspace.itemLocationStore.clear() atom.workspace.enablePersistence = false }) it('adds and activates a new editor for the given path on the active pane', () => { let editor = null waitsForPromise(() => workspace.open('a').then(o => { editor = o })) runs(() => { const firstDirectory = atom.project.getDirectories()[0] expect(firstDirectory).toBeDefined() expect(editor.getURI()).toBe(firstDirectory.resolve('a')) expect(workspace.getActivePaneItem()).toBe(editor) expect(workspace.getActivePane().items).toEqual([editor]) expect(workspace.getActivePane().activate).toHaveBeenCalled() }) }) it("uses the location specified by the model's `getDefaultLocation()` method", () => { const item = { getDefaultLocation: jasmine.createSpy().andReturn('right'), getElement: () => document.createElement('div') } const opener = jasmine.createSpy().andReturn(item) const dock = atom.workspace.getRightDock() spyOn(atom.workspace.itemLocationStore, 'load').andReturn(Promise.resolve()) spyOn(atom.workspace, 'getOpeners').andReturn([opener]) expect(dock.getPaneItems()).toHaveLength(0) waitsForPromise(() => atom.workspace.open('a')) runs(() => { expect(dock.getPaneItems()).toHaveLength(1) expect(opener).toHaveBeenCalled() expect(item.getDefaultLocation).toHaveBeenCalled() }) }) it('prefers the last location the user used for that item', () => { const ITEM_URI = 'atom://test' const item = { getURI: () => ITEM_URI, getDefaultLocation: () => 'left', getElement: () => document.createElement('div') } const opener = uri => uri === ITEM_URI ? item : null const dock = atom.workspace.getRightDock() spyOn(atom.workspace.itemLocationStore, 'load').andCallFake(uri => uri === 'atom://test' ? Promise.resolve('right') : Promise.resolve() ) spyOn(atom.workspace, 'getOpeners').andReturn([opener]) expect(dock.getPaneItems()).toHaveLength(0) waitsForPromise(() => atom.workspace.open(ITEM_URI)) runs(() => { expect(dock.getPaneItems()).toHaveLength(1) expect(dock.getPaneItems()[0]).toBe(item) }) }) }) }) }) describe("when the 'searchAllPanes' option is true", () => { describe('when an editor for the given uri is already open on an inactive pane', () => { it('activates the existing editor on the inactive pane, then activates that pane', () => { let editor1 = null let editor2 = null const pane1 = workspace.getActivePane() const pane2 = workspace.getActivePane().splitRight() waitsForPromise(() => { pane1.activate() return workspace.open('a').then(o => { editor1 = o }) }) waitsForPromise(() => { pane2.activate() return workspace.open('b').then(o => { editor2 = o }) }) runs(() => expect(workspace.getActivePaneItem()).toBe(editor2)) waitsForPromise(() => workspace.open('a', {searchAllPanes: true})) runs(() => { expect(workspace.getActivePane()).toBe(pane1) expect(workspace.getActivePaneItem()).toBe(editor1) }) }) it('activates the pane in the dock with the matching item', () => { const dock = atom.workspace.getRightDock() const ITEM_URI = 'atom://test' const item = { getURI: () => ITEM_URI, getDefaultLocation: jasmine.createSpy().andReturn('left'), getElement: () => document.createElement('div') } dock.getActivePane().addItem(item) spyOn(dock.paneForItem(item), 'activate') waitsForPromise(() => atom.workspace.open(ITEM_URI, {searchAllPanes: true})) runs(() => expect(dock.paneForItem(item).activate).toHaveBeenCalled()) }) }) describe('when no editor for the given uri is open in any pane', () => { it('opens an editor for the given uri in the active pane', () => { let editor = null waitsForPromise(() => workspace.open('a', {searchAllPanes: true}).then(o => { editor = o })) runs(() => expect(workspace.getActivePaneItem()).toBe(editor)) }) }) }) describe('when called with an item rather than a URI', () => { it('adds the item itself to the workspace', async () => { const item = document.createElement('div') await atom.workspace.open(item) expect(atom.workspace.getActivePaneItem()).toBe(item) }) describe('when the active pane already contains the item', () => { it('activates the item', async () => { const item = document.createElement('div') await atom.workspace.open(item) await atom.workspace.open() expect(atom.workspace.getActivePaneItem()).not.toBe(item) expect(atom.workspace.getActivePane().getItems().length).toBe(2) await atom.workspace.open(item) expect(atom.workspace.getActivePaneItem()).toBe(item) expect(atom.workspace.getActivePane().getItems().length).toBe(2) }) }) describe('when the item already exists in another pane', () => { it('rejects the promise', async () => { const item = document.createElement('div') await atom.workspace.open(item) await atom.workspace.open(null, {split: 'right'}) expect(atom.workspace.getActivePaneItem()).not.toBe(item) expect(atom.workspace.getActivePane().getItems().length).toBe(1) let rejection try { await atom.workspace.open(item) } catch (error) { rejection = error } expect(rejection.message).toMatch(/The workspace can only contain one instance of item/) }) }) }) describe("when the 'split' option is set", () => { describe("when the 'split' option is 'left'", () => { it('opens the editor in the leftmost pane of the current pane axis', () => { const pane1 = workspace.getActivePane() const pane2 = pane1.splitRight() expect(workspace.getActivePane()).toBe(pane2) let editor = null waitsForPromise(() => workspace.open('a', {split: 'left'}).then(o => { editor = o })) runs(() => { expect(workspace.getActivePane()).toBe(pane1) expect(pane1.items).toEqual([editor]) expect(pane2.items).toEqual([]) }) // Focus right pane and reopen the file on the left waitsForPromise(() => { pane2.focus() return workspace.open('a', {split: 'left'}).then(o => { editor = o }) }) runs(() => { expect(workspace.getActivePane()).toBe(pane1) expect(pane1.items).toEqual([editor]) expect(pane2.items).toEqual([]) }) }) }) describe('when a pane axis is the leftmost sibling of the current pane', () => { it('opens the new item in the current pane', () => { let editor = null const pane1 = workspace.getActivePane() const pane2 = pane1.splitLeft() pane2.splitDown() pane1.activate() expect(workspace.getActivePane()).toBe(pane1) waitsForPromise(() => workspace.open('a', {split: 'left'}).then(o => { editor = o })) runs(() => { expect(workspace.getActivePane()).toBe(pane1) expect(pane1.items).toEqual([editor]) }) }) }) describe("when the 'split' option is 'right'", () => { it('opens the editor in the rightmost pane of the current pane axis', () => { let editor = null const pane1 = workspace.getActivePane() let pane2 = null waitsForPromise(() => workspace.open('a', {split: 'right'}).then(o => { editor = o })) runs(() => { pane2 = workspace.getPanes().filter(p => p !== pane1)[0] expect(workspace.getActivePane()).toBe(pane2) expect(pane1.items).toEqual([]) expect(pane2.items).toEqual([editor]) }) // Focus right pane and reopen the file on the right waitsForPromise(() => { pane1.focus() return workspace.open('a', {split: 'right'}).then(o => { editor = o }) }) runs(() => { expect(workspace.getActivePane()).toBe(pane2) expect(pane1.items).toEqual([]) expect(pane2.items).toEqual([editor]) }) }) describe('when a pane axis is the rightmost sibling of the current pane', () => { it('opens the new item in a new pane split to the right of the current pane', () => { let editor = null const pane1 = workspace.getActivePane() const pane2 = pane1.splitRight() pane2.splitDown() pane1.activate() expect(workspace.getActivePane()).toBe(pane1) let pane4 = null waitsForPromise(() => workspace.open('a', {split: 'right'}).then(o => { editor = o })) runs(() => { pane4 = workspace.getPanes().filter(p => p !== pane1)[0] expect(workspace.getActivePane()).toBe(pane4) expect(pane4.items).toEqual([editor]) expect(workspace.getCenter().paneContainer.root.children[0]).toBe(pane1) expect(workspace.getCenter().paneContainer.root.children[1]).toBe(pane4) }) }) }) }) describe("when the 'split' option is 'up'", () => { it('opens the editor in the topmost pane of the current pane axis', () => { const pane1 = workspace.getActivePane() const pane2 = pane1.splitDown() expect(workspace.getActivePane()).toBe(pane2) let editor = null waitsForPromise(() => workspace.open('a', {split: 'up'}).then(o => { editor = o })) runs(() => { expect(workspace.getActivePane()).toBe(pane1) expect(pane1.items).toEqual([editor]) expect(pane2.items).toEqual([]) }) // Focus bottom pane and reopen the file on the top waitsForPromise(() => { pane2.focus() return workspace.open('a', {split: 'up'}).then(o => { editor = o }) }) runs(() => { expect(workspace.getActivePane()).toBe(pane1) expect(pane1.items).toEqual([editor]) expect(pane2.items).toEqual([]) }) }) }) describe('when a pane axis is the topmost sibling of the current pane', () => { it('opens the new item in the current pane', () => { let editor = null const pane1 = workspace.getActivePane() const pane2 = pane1.splitUp() pane2.splitRight() pane1.activate() expect(workspace.getActivePane()).toBe(pane1) waitsForPromise(() => workspace.open('a', {split: 'up'}).then(o => { editor = o })) runs(() => { expect(workspace.getActivePane()).toBe(pane1) expect(pane1.items).toEqual([editor]) }) }) }) describe("when the 'split' option is 'down'", () => { it('opens the editor in the bottommost pane of the current pane axis', () => { let editor = null const pane1 = workspace.getActivePane() let pane2 = null waitsForPromise(() => workspace.open('a', {split: 'down'}).then(o => { editor = o })) runs(() => { pane2 = workspace.getPanes().filter(p => p !== pane1)[0] expect(workspace.getActivePane()).toBe(pane2) expect(pane1.items).toEqual([]) expect(pane2.items).toEqual([editor]) }) // Focus bottom pane and reopen the file on the right waitsForPromise(() => { pane1.focus() return workspace.open('a', {split: 'down'}).then(o => { editor = o }) }) runs(() => { expect(workspace.getActivePane()).toBe(pane2) expect(pane1.items).toEqual([]) expect(pane2.items).toEqual([editor]) }) }) describe('when a pane axis is the bottommost sibling of the current pane', () => { it('opens the new item in a new pane split to the bottom of the current pane', () => { let editor = null const pane1 = workspace.getActivePane() const pane2 = pane1.splitDown() pane1.activate() expect(workspace.getActivePane()).toBe(pane1) let pane4 = null waitsForPromise(() => workspace.open('a', {split: 'down'}).then(o => { editor = o })) runs(() => { pane4 = workspace.getPanes().filter(p => p !== pane1)[0] expect(workspace.getActivePane()).toBe(pane4) expect(pane4.items).toEqual([editor]) expect(workspace.getCenter().paneContainer.root.children[0]).toBe(pane1) expect(workspace.getCenter().paneContainer.root.children[1]).toBe(pane2) }) }) }) }) }) describe('when an initialLine and initialColumn are specified', () => { it('moves the cursor to the indicated location', () => { waitsForPromise(() => workspace.open('a', {initialLine: 1, initialColumn: 5})) runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([1, 5])) waitsForPromise(() => workspace.open('a', {initialLine: 2, initialColumn: 4})) runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 4])) waitsForPromise(() => workspace.open('a', {initialLine: 0, initialColumn: 0})) runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([0, 0])) waitsForPromise(() => workspace.open('a', {initialLine: NaN, initialColumn: 4})) runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([0, 4])) waitsForPromise(() => workspace.open('a', {initialLine: 2, initialColumn: NaN})) runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 0])) waitsForPromise(() => workspace.open('a', {initialLine: Infinity, initialColumn: Infinity})) runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 11])) }) }) describe('when the file is over 2MB', () => { it('opens the editor with largeFileMode: true', () => { spyOn(fs, 'getSizeSync').andReturn(2 * 1048577) // 2MB let editor = null waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e })) runs(() => expect(editor.largeFileMode).toBe(true)) }) }) describe('when the file is over user-defined limit', () => { const shouldPromptForFileOfSize = (size, shouldPrompt) => { spyOn(fs, 'getSizeSync').andReturn(size * 1048577) atom.applicationDelegate.confirm.andCallFake(() => selectedButtonIndex) atom.applicationDelegate.confirm() var selectedButtonIndex = 1 // cancel let editor = null waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e })) if (shouldPrompt) { runs(() => { expect(editor).toBeUndefined() expect(atom.applicationDelegate.confirm).toHaveBeenCalled() atom.applicationDelegate.confirm.reset() selectedButtonIndex = 0 }) // open the file waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e })) runs(() => { expect(atom.applicationDelegate.confirm).toHaveBeenCalled() expect(editor.largeFileMode).toBe(true) }) } else { runs(() => expect(editor).not.toBeUndefined()) } } it('prompts the user to make sure they want to open a file this big', () => { atom.config.set('core.warnOnLargeFileLimit', 20) shouldPromptForFileOfSize(20, true) }) it("doesn't prompt on files below the limit", () => { atom.config.set('core.warnOnLargeFileLimit', 30) shouldPromptForFileOfSize(20, false) }) it('prompts for smaller files with a lower limit', () => { atom.config.set('core.warnOnLargeFileLimit', 5) shouldPromptForFileOfSize(10, true) }) }) describe('when passed a path that matches a custom opener', () => { it('returns the resource returned by the custom opener', () => { const fooOpener = (pathToOpen, options) => { if (pathToOpen != null ? pathToOpen.match(/\.foo/) : undefined) { return {foo: pathToOpen, options} } } const barOpener = (pathToOpen) => { if (pathToOpen != null ? pathToOpen.match(/^bar:\/\//) : undefined) { return {bar: pathToOpen} } } workspace.addOpener(fooOpener) workspace.addOpener(barOpener) waitsForPromise(() => { const pathToOpen = atom.project.getDirectories()[0].resolve('a.foo') return workspace.open(pathToOpen, {hey: 'there'}).then(item => expect(item).toEqual({foo: pathToOpen, options: {hey: 'there'}})) }) waitsForPromise(() => workspace.open('bar://baz').then(item => expect(item).toEqual({bar: 'bar://baz'}))) }) }) it("adds the file to the application's recent documents list", () => { if (process.platform !== 'darwin') { return } // Feature only supported on macOS spyOn(atom.applicationDelegate, 'addRecentDocument') waitsForPromise(() => workspace.open()) runs(() => expect(atom.applicationDelegate.addRecentDocument).not.toHaveBeenCalled()) waitsForPromise(() => workspace.open('something://a/url')) runs(() => expect(atom.applicationDelegate.addRecentDocument).not.toHaveBeenCalled()) waitsForPromise(() => workspace.open(__filename)) runs(() => expect(atom.applicationDelegate.addRecentDocument).toHaveBeenCalledWith(__filename)) }) it('notifies ::onDidAddTextEditor observers', () => { const absolutePath = require.resolve('./fixtures/dir/a') const newEditorHandler = jasmine.createSpy('newEditorHandler') workspace.onDidAddTextEditor(newEditorHandler) let editor = null waitsForPromise(() => workspace.open(absolutePath).then(e => { editor = e })) runs(() => expect(newEditorHandler.argsForCall[0][0].textEditor).toBe(editor)) }) describe('when there is an error opening the file', () => { let notificationSpy = null beforeEach(() => atom.notifications.onDidAddNotification(notificationSpy = jasmine.createSpy())) describe('when a file does not exist', () => { it('creates an empty buffer for the specified path', () => { waitsForPromise(() => workspace.open('not-a-file.md')) runs(() => { const editor = workspace.getActiveTextEditor() expect(notificationSpy).not.toHaveBeenCalled() expect(editor.getPath()).toContain('not-a-file.md') }) }) }) describe('when the user does not have access to the file', () => { beforeEach(() => spyOn(fs, 'openSync').andCallFake(path => { const error = new Error(`EACCES, permission denied '${path}'`) error.path = path error.code = 'EACCES' throw error }) ) it('creates a notification', () => { waitsForPromise(() => workspace.open('file1')) runs(() => { expect(notificationSpy).toHaveBeenCalled() const notification = notificationSpy.mostRecentCall.args[0] expect(notification.getType()).toBe('warning') expect(notification.getMessage()).toContain('Permission denied') expect(notification.getMessage()).toContain('file1') }) }) }) describe('when the the operation is not permitted', () => { beforeEach(() => spyOn(fs, 'openSync').andCallFake(path => { const error = new Error(`EPERM, operation not permitted '${path}'`) error.path = path error.code = 'EPERM' throw error }) ) it('creates a notification', () => { waitsForPromise(() => workspace.open('file1')) runs(() => { expect(notificationSpy).toHaveBeenCalled() const notification = notificationSpy.mostRecentCall.args[0] expect(notification.getType()).toBe('warning') expect(notification.getMessage()).toContain('Unable to open') expect(notification.getMessage()).toContain('file1') }) }) }) describe('when the the file is already open in windows', () => { beforeEach(() => spyOn(fs, 'openSync').andCallFake(path => { const error = new Error(`EBUSY, resource busy or locked '${path}'`) error.path = path error.code = 'EBUSY' throw error }) ) it('creates a notification', () => { waitsForPromise(() => workspace.open('file1')) runs(() => { expect(notificationSpy).toHaveBeenCalled() const notification = notificationSpy.mostRecentCall.args[0] expect(notification.getType()).toBe('warning') expect(notification.getMessage()).toContain('Unable to open') expect(notification.getMessage()).toContain('file1') }) }) }) describe('when there is an unhandled error', () => { beforeEach(() => spyOn(fs, 'openSync').andCallFake(path => { throw new Error('I dont even know what is happening right now!!') }) ) it('rejects the promise', () => { waitsFor((done) => { workspace.open('file1').catch(error => { expect(error.message).toBe('I dont even know what is happening right now!!') done() }) }) }) }) }) describe('when the file is already open in pending state', () => { it('should terminate the pending state', () => { let editor = null let pane = null waitsForPromise(() => atom.workspace.open('sample.js', {pending: true}).then(o => { editor = o pane = atom.workspace.getActivePane() }) ) runs(() => expect(pane.getPendingItem()).toEqual(editor)) waitsForPromise(() => atom.workspace.open('sample.js')) runs(() => expect(pane.getPendingItem()).toBeNull()) }) }) describe('when opening will switch from a pending tab to a permanent tab', () => { it('keeps the pending tab open', () => { let editor1 = null let editor2 = null waitsForPromise(() => atom.workspace.open('sample.txt').then(o => { editor1 = o }) ) waitsForPromise(() => atom.workspace.open('sample2.txt', {pending: true}).then(o => { editor2 = o }) ) runs(() => { const pane = atom.workspace.getActivePane() pane.activateItem(editor1) expect(pane.getItems().length).toBe(2) expect(pane.getItems()).toEqual([editor1, editor2]) }) }) }) describe('when replacing a pending item which is the last item in a second pane', () => { it('does not destroy the pane even if core.destroyEmptyPanes is on', () => { atom.config.set('core.destroyEmptyPanes', true) let editor1 = null let editor2 = null const leftPane = atom.workspace.getActivePane() let rightPane = null waitsForPromise(() => atom.workspace.open('sample.js', {pending: true, split: 'right'}).then(o => { editor1 = o rightPane = atom.workspace.getActivePane() spyOn(rightPane, 'destroy').andCallThrough() }) ) runs(() => { expect(leftPane).not.toBe(rightPane) expect(atom.workspace.getActivePane()).toBe(rightPane) expect(atom.workspace.getActivePane().getItems().length).toBe(1) expect(rightPane.getPendingItem()).toBe(editor1) }) waitsForPromise(() => atom.workspace.open('sample.txt', {pending: true}).then(o => { editor2 = o }) ) runs(() => { expect(rightPane.getPendingItem()).toBe(editor2) expect(rightPane.destroy.callCount).toBe(0) }) }) }) }) describe('::hide(uri)', () => { let item const URI = 'atom://hide-test' beforeEach(() => { const el = document.createElement('div') item = { getTitle: () => 'Item', getElement: () => el, getURI: () => URI } }) describe('when called with a URI', () => { it('if the item for the given URI is in the center, removes it', () => { const pane = atom.workspace.getActivePane() pane.addItem(item) atom.workspace.hide(URI) expect(pane.getItems().length).toBe(0) }) it('if the item for the given URI is in a dock, hides the dock', () => { const dock = atom.workspace.getLeftDock() const pane = dock.getActivePane() pane.addItem(item) dock.activate() expect(dock.isVisible()).toBe(true) const itemFound = atom.workspace.hide(URI) expect(itemFound).toBe(true) expect(dock.isVisible()).toBe(false) }) }) describe('when called with an item', () => { it('if the item is in the center, removes it', () => { const pane = atom.workspace.getActivePane() pane.addItem(item) atom.workspace.hide(item) expect(pane.getItems().length).toBe(0) }) it('if the item is in a dock, hides the dock', () => { const dock = atom.workspace.getLeftDock() const pane = dock.getActivePane() pane.addItem(item) dock.activate() expect(dock.isVisible()).toBe(true) const itemFound = atom.workspace.hide(item) expect(itemFound).toBe(true) expect(dock.isVisible()).toBe(false) }) }) }) describe('::toggle(itemOrUri)', () => { describe('when the location resolves to a dock', () => { it('adds or shows the item and its dock if it is not currently visible, and otherwise hides the containing dock', async () => { const item1 = { getDefaultLocation () { return 'left' }, getElement () { return (this.element = document.createElement('div')) } } const item2 = { getDefaultLocation () { return 'left' }, getElement () { return (this.element = document.createElement('div')) } } const dock = workspace.getLeftDock() expect(dock.isVisible()).toBe(false) await workspace.toggle(item1) expect(dock.isVisible()).toBe(true) expect(dock.getActivePaneItem()).toBe(item1) await workspace.toggle(item2) expect(dock.isVisible()).toBe(true) expect(dock.getActivePaneItem()).toBe(item2) await workspace.toggle(item1) expect(dock.isVisible()).toBe(true) expect(dock.getActivePaneItem()).toBe(item1) await workspace.toggle(item1) expect(dock.isVisible()).toBe(false) expect(dock.getActivePaneItem()).toBe(item1) await workspace.toggle(item2) expect(dock.isVisible()).toBe(true) expect(dock.getActivePaneItem()).toBe(item2) }) }) describe('when the location resolves to the center', () => { it('adds or shows the item if it is not currently the active pane item, and otherwise removes the item', async () => { const item1 = { getDefaultLocation () { return 'center' }, getElement () { return (this.element = document.createElement('div')) } } const item2 = { getDefaultLocation () { return 'center' }, getElement () { return (this.element = document.createElement('div')) } } expect(workspace.getActivePaneItem()).toBeUndefined() await workspace.toggle(item1) expect(workspace.getActivePaneItem()).toBe(item1) await workspace.toggle(item2) expect(workspace.getActivePaneItem()).toBe(item2) await workspace.toggle(item1) expect(workspace.getActivePaneItem()).toBe(item1) await workspace.toggle(item1) expect(workspace.paneForItem(item1)).toBeUndefined() expect(workspace.getActivePaneItem()).toBe(item2) }) }) }) describe('active pane containers', () => { it('maintains the active pane and item globally across active pane containers', () => { const leftDock = workspace.getLeftDock() const leftItem1 = {element: document.createElement('div')} const leftItem2 = {element: document.createElement('div')} const leftItem3 = {element: document.createElement('div')} const leftPane1 = leftDock.getActivePane() leftPane1.addItems([leftItem1, leftItem2]) const leftPane2 = leftPane1.splitDown({items: [leftItem3]}) const rightDock = workspace.getRightDock() const rightItem1 = {element: document.createElement('div')} const rightItem2 = {element: document.createElement('div')} const rightItem3 = {element: document.createElement('div')} const rightPane1 = rightDock.getActivePane() rightPane1.addItems([rightItem1, rightItem2]) const rightPane2 = rightPane1.splitDown({items: [rightItem3]}) const bottomDock = workspace.getBottomDock() const bottomItem1 = {element: document.createElement('div')} const bottomItem2 = {element: document.createElement('div')} const bottomItem3 = {element: document.createElement('div')} const bottomPane1 = bottomDock.getActivePane() bottomPane1.addItems([bottomItem1, bottomItem2]) const bottomPane2 = bottomPane1.splitDown({items: [bottomItem3]}) const center = workspace.getCenter() const centerItem1 = {element: document.createElement('div')} const centerItem2 = {element: document.createElement('div')} const centerItem3 = {element: document.createElement('div')} const centerPane1 = center.getActivePane() centerPane1.addItems([centerItem1, centerItem2]) const centerPane2 = centerPane1.splitDown({items: [centerItem3]}) const activePaneContainers = [] const activePanes = [] const activeItems = [] workspace.onDidChangeActivePaneContainer((container) => activePaneContainers.push(container)) workspace.onDidChangeActivePane((pane) => activePanes.push(pane)) workspace.onDidChangeActivePaneItem((item) => activeItems.push(item)) function clearEvents () { activePaneContainers.length = 0 activePanes.length = 0 activeItems.length = 0 } expect(workspace.getActivePaneContainer()).toBe(center) expect(workspace.getActivePane()).toBe(centerPane2) expect(workspace.getActivePaneItem()).toBe(centerItem3) leftDock.activate() expect(workspace.getActivePaneContainer()).toBe(leftDock) expect(workspace.getActivePane()).toBe(leftPane2) expect(workspace.getActivePaneItem()).toBe(leftItem3) expect(activePaneContainers).toEqual([leftDock]) expect(activePanes).toEqual([leftPane2]) expect(activeItems).toEqual([leftItem3]) clearEvents() leftPane1.activate() leftPane1.activate() expect(workspace.getActivePaneContainer()).toBe(leftDock) expect(workspace.getActivePane()).toBe(leftPane1) expect(workspace.getActivePaneItem()).toBe(leftItem1) expect(activePaneContainers).toEqual([]) expect(activePanes).toEqual([leftPane1]) expect(activeItems).toEqual([leftItem1]) clearEvents() leftPane1.activateItem(leftItem2) leftPane1.activateItem(leftItem2) expect(workspace.getActivePaneContainer()).toBe(leftDock) expect(workspace.getActivePane()).toBe(leftPane1) expect(workspace.getActivePaneItem()).toBe(leftItem2) expect(activePaneContainers).toEqual([]) expect(activePanes).toEqual([]) expect(activeItems).toEqual([leftItem2]) clearEvents() expect(rightDock.getActivePane()).toBe(rightPane2) rightPane1.activate() rightPane1.activate() expect(workspace.getActivePaneContainer()).toBe(rightDock) expect(workspace.getActivePane()).toBe(rightPane1) expect(workspace.getActivePaneItem()).toBe(rightItem1) expect(activePaneContainers).toEqual([rightDock]) expect(activePanes).toEqual([rightPane1]) expect(activeItems).toEqual([rightItem1]) clearEvents() rightPane1.activateItem(rightItem2) expect(workspace.getActivePaneContainer()).toBe(rightDock) expect(workspace.getActivePane()).toBe(rightPane1) expect(workspace.getActivePaneItem()).toBe(rightItem2) expect(activePaneContainers).toEqual([]) expect(activePanes).toEqual([]) expect(activeItems).toEqual([rightItem2]) clearEvents() expect(bottomDock.getActivePane()).toBe(bottomPane2) bottomPane2.activate() bottomPane2.activate() expect(workspace.getActivePaneContainer()).toBe(bottomDock) expect(workspace.getActivePane()).toBe(bottomPane2) expect(workspace.getActivePaneItem()).toBe(bottomItem3) expect(activePaneContainers).toEqual([bottomDock]) expect(activePanes).toEqual([bottomPane2]) expect(activeItems).toEqual([bottomItem3]) clearEvents() center.activate() center.activate() expect(workspace.getActivePaneContainer()).toBe(center) expect(workspace.getActivePane()).toBe(centerPane2) expect(workspace.getActivePaneItem()).toBe(centerItem3) expect(activePaneContainers).toEqual([center]) expect(activePanes).toEqual([centerPane2]) expect(activeItems).toEqual([centerItem3]) clearEvents() centerPane1.activate() centerPane1.activate() expect(workspace.getActivePaneContainer()).toBe(center) expect(workspace.getActivePane()).toBe(centerPane1) expect(workspace.getActivePaneItem()).toBe(centerItem1) expect(activePaneContainers).toEqual([]) expect(activePanes).toEqual([centerPane1]) expect(activeItems).toEqual([centerItem1]) }) }) describe('::onDidStopChangingActivePaneItem()', function () { it('invokes observers when the active item of the active pane stops changing', function () { const pane1 = atom.workspace.getCenter().getActivePane() const pane2 = pane1.splitRight({items: [document.createElement('div'), document.createElement('div')]}); atom.workspace.getLeftDock().getActivePane().addItem(document.createElement('div')) emittedItems = [] atom.workspace.onDidStopChangingActivePaneItem(item => emittedItems.push(item)) pane2.activateNextItem() pane2.activateNextItem() pane1.activate() atom.workspace.getLeftDock().activate() advanceClock(100) expect(emittedItems).toEqual([atom.workspace.getLeftDock().getActivePaneItem()]) }) }) describe('the grammar-used hook', () => { it('fires when opening a file or changing the grammar of an open file', () => { let editor = null let javascriptGrammarUsed = false let coffeescriptGrammarUsed = false atom.packages.triggerDeferredActivationHooks() runs(() => { atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', () => { javascriptGrammarUsed = true }) atom.packages.onDidTriggerActivationHook('language-coffee-script:grammar-used', () => { coffeescriptGrammarUsed = true }) }) waitsForPromise(() => atom.workspace.open('sample.js', {autoIndent: false}).then(o => { editor = o })) waitsForPromise(() => atom.packages.activatePackage('language-javascript')) waitsFor(() => javascriptGrammarUsed) waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) runs(() => editor.setGrammar(atom.grammars.selectGrammar('.coffee'))) waitsFor(() => coffeescriptGrammarUsed) }) }) describe('::reopenItem()', () => { it("opens the uri associated with the last closed pane that isn't currently open", () => { const pane = workspace.getActivePane() waitsForPromise(() => workspace.open('a').then(() => workspace.open('b').then(() => workspace.open('file1').then(() => workspace.open()) ) ) ) runs(() => { // does not reopen items with no uri expect(workspace.getActivePaneItem().getURI()).toBeUndefined() pane.destroyActiveItem() }) waitsForPromise(() => workspace.reopenItem()) const firstDirectory = atom.project.getDirectories()[0] expect(firstDirectory).toBeDefined() runs(() => { expect(workspace.getActivePaneItem().getURI()).not.toBeUndefined() // destroy all items expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('file1')) pane.destroyActiveItem() expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('b')) pane.destroyActiveItem() expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('a')) pane.destroyActiveItem() // reopens items with uris expect(workspace.getActivePaneItem()).toBeUndefined() }) waitsForPromise(() => workspace.reopenItem()) runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('a'))) // does not reopen items that are already open waitsForPromise(() => workspace.open('b')) runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('b'))) waitsForPromise(() => workspace.reopenItem()) runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('file1'))) }) }) describe('::increase/decreaseFontSize()', () => { it('increases/decreases the font size without going below 1', () => { atom.config.set('editor.fontSize', 1) workspace.increaseFontSize() expect(atom.config.get('editor.fontSize')).toBe(2) workspace.increaseFontSize() expect(atom.config.get('editor.fontSize')).toBe(3) workspace.decreaseFontSize() expect(atom.config.get('editor.fontSize')).toBe(2) workspace.decreaseFontSize() expect(atom.config.get('editor.fontSize')).toBe(1) workspace.decreaseFontSize() expect(atom.config.get('editor.fontSize')).toBe(1) }) }) describe('::resetFontSize()', () => { it("resets the font size to the window's starting font size", () => { const originalFontSize = atom.config.get('editor.fontSize') workspace.increaseFontSize() expect(atom.config.get('editor.fontSize')).toBe(originalFontSize + 1) workspace.resetFontSize() expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) workspace.decreaseFontSize() expect(atom.config.get('editor.fontSize')).toBe(originalFontSize - 1) workspace.resetFontSize() expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) }) it('does nothing if the font size has not been changed', () => { const originalFontSize = atom.config.get('editor.fontSize') workspace.resetFontSize() expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) }) it("resets the font size when the editor's font size changes", () => { const originalFontSize = atom.config.get('editor.fontSize') atom.config.set('editor.fontSize', originalFontSize + 1) workspace.resetFontSize() expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) atom.config.set('editor.fontSize', originalFontSize - 1) workspace.resetFontSize() expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) }) }) describe('::openLicense()', () => { it('opens the license as plain-text in a buffer', () => { waitsForPromise(() => workspace.openLicense()) runs(() => expect(workspace.getActivePaneItem().getText()).toMatch(/Copyright/)) }) }) describe('::isTextEditor(obj)', () => { it('returns true when the passed object is an instance of `TextEditor`', () => { expect(workspace.isTextEditor(new TextEditor())).toBe(true) expect(workspace.isTextEditor({getText: () => null})).toBe(false) expect(workspace.isTextEditor(null)).toBe(false) expect(workspace.isTextEditor(undefined)).toBe(false) }) }) describe('::observeTextEditors()', () => { it('invokes the observer with current and future text editors', () => { const observed = [] waitsForPromise(() => workspace.open()) waitsForPromise(() => workspace.open()) waitsForPromise(() => workspace.openLicense()) runs(() => workspace.observeTextEditors(editor => observed.push(editor))) waitsForPromise(() => workspace.open()) expect(observed).toEqual(workspace.getTextEditors()) }) }) describe('when an editor is destroyed', () => { it('removes the editor', () => { let editor = null waitsForPromise(() => workspace.open('a').then(e => { editor = e })) runs(() => { expect(workspace.getTextEditors()).toHaveLength(1) editor.destroy() expect(workspace.getTextEditors()).toHaveLength(0) }) }) }) describe('when an editor is copied because its pane is split', () => { it('sets up the new editor to be configured by the text editor registry', () => { waitsForPromise(() => atom.packages.activatePackage('language-javascript')) waitsForPromise(() => workspace.open('a').then(editor => { atom.textEditors.setGrammarOverride(editor, 'source.js') expect(editor.getGrammar().name).toBe('JavaScript') workspace.getActivePane().splitRight({copyActiveItem: true}) const newEditor = workspace.getActiveTextEditor() expect(newEditor).not.toBe(editor) expect(newEditor.getGrammar().name).toBe('JavaScript') }) ) }) }) it('stores the active grammars used by all the open editors', () => { waitsForPromise(() => atom.packages.activatePackage('language-javascript')) waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) waitsForPromise(() => atom.packages.activatePackage('language-todo')) waitsForPromise(() => atom.workspace.open('sample.coffee')) runs(function () { atom.workspace.getActiveTextEditor().setText(`\ i = /test/; #FIXME\ ` ) const atom2 = new AtomEnvironment({applicationDelegate: atom.applicationDelegate}) atom2.initialize({ window: document.createElement('div'), document: Object.assign( document.createElement('div'), { body: document.createElement('div'), head: document.createElement('div') } ) }) atom2.packages.loadPackage('language-javascript') atom2.packages.loadPackage('language-coffee-script') atom2.packages.loadPackage('language-todo') atom2.project.deserialize(atom.project.serialize()) atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers) expect(atom2.grammars.getGrammars().map(grammar => grammar.name).sort()).toEqual([ 'CoffeeScript', 'CoffeeScript (Literate)', 'JavaScript', 'Null Grammar', 'Regular Expression Replacement (JavaScript)', 'Regular Expressions (JavaScript)', 'TODO' ]) atom2.destroy() }) }) describe('document.title', () => { describe('when there is no item open', () => { it('sets the title to the project path', () => expect(document.title).toMatch(escapeStringRegex(fs.tildify(atom.project.getPaths()[0])))) it("sets the title to 'untitled' if there is no project path", () => { atom.project.setPaths([]) expect(document.title).toMatch(/^untitled/) }) }) describe("when the active pane item's path is not inside a project path", () => { beforeEach(() => waitsForPromise(() => atom.workspace.open('b').then(() => atom.project.setPaths([])) ) ) it("sets the title to the pane item's title plus the item's path", () => { const item = atom.workspace.getActivePaneItem() const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))) expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) }) describe('when the title of the active pane item changes', () => { it("updates the window title based on the item's new title", () => { const editor = atom.workspace.getActivePaneItem() editor.buffer.setPath(path.join(temp.dir, 'hi')) const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(editor.getPath()))) expect(document.title).toMatch(new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`)) }) }) describe("when the active pane's item changes", () => { it("updates the title to the new item's title plus the project path", () => { atom.workspace.getActivePane().activateNextItem() const item = atom.workspace.getActivePaneItem() const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))) expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) }) }) describe("when an inactive pane's item changes", () => { it('does not update the title', () => { const pane = atom.workspace.getActivePane() pane.splitRight() const initialTitle = document.title pane.activateNextItem() expect(document.title).toBe(initialTitle) }) }) }) describe('when the active pane item is inside a project path', () => { beforeEach(() => waitsForPromise(() => atom.workspace.open('b')) ) describe('when there is an active pane item', () => { it("sets the title to the pane item's title plus the project path", () => { const item = atom.workspace.getActivePaneItem() const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) }) }) describe('when the title of the active pane item changes', () => { it("updates the window title based on the item's new title", () => { const editor = atom.workspace.getActivePaneItem() editor.buffer.setPath(path.join(atom.project.getPaths()[0], 'hi')) const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) expect(document.title).toMatch(new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`)) }) }) describe("when the active pane's item changes", () => { it("updates the title to the new item's title plus the project path", () => { atom.workspace.getActivePane().activateNextItem() const item = atom.workspace.getActivePaneItem() const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) }) }) describe('when the last pane item is removed', () => { it("updates the title to the project's first path", () => { atom.workspace.getActivePane().destroy() expect(atom.workspace.getActivePaneItem()).toBeUndefined() expect(document.title).toMatch(escapeStringRegex(fs.tildify(atom.project.getPaths()[0]))) }) }) describe("when an inactive pane's item changes", () => { it('does not update the title', () => { const pane = atom.workspace.getActivePane() pane.splitRight() const initialTitle = document.title pane.activateNextItem() expect(document.title).toBe(initialTitle) }) }) }) describe('when the workspace is deserialized', () => { beforeEach(() => waitsForPromise(() => atom.workspace.open('a'))) it("updates the title to contain the project's path", () => { document.title = null const atom2 = new AtomEnvironment({applicationDelegate: atom.applicationDelegate}) atom2.initialize({ window: document.createElement('div'), document: Object.assign( document.createElement('div'), { body: document.createElement('div'), head: document.createElement('div') } ) }) atom2.project.deserialize(atom.project.serialize()) atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers) const item = atom2.workspace.getActivePaneItem() const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) expect(document.title).toMatch(new RegExp(`^${item.getLongTitle()} \\u2014 ${pathEscaped}`)) atom2.destroy() }) }) }) describe('document edited status', () => { let item1 let item2 beforeEach(() => { waitsForPromise(() => atom.workspace.open('a')) waitsForPromise(() => atom.workspace.open('b')) runs(() => { [item1, item2] = atom.workspace.getPaneItems() }) }) it('calls setDocumentEdited when the active item changes', () => { expect(atom.workspace.getActivePaneItem()).toBe(item2) item1.insertText('a') expect(item1.isModified()).toBe(true) atom.workspace.getActivePane().activateNextItem() expect(setDocumentEdited).toHaveBeenCalledWith(true) }) it("calls atom.setDocumentEdited when the active item's modified status changes", () => { expect(atom.workspace.getActivePaneItem()).toBe(item2) item2.insertText('a') advanceClock(item2.getBuffer().getStoppedChangingDelay()) expect(item2.isModified()).toBe(true) expect(setDocumentEdited).toHaveBeenCalledWith(true) item2.undo() advanceClock(item2.getBuffer().getStoppedChangingDelay()) expect(item2.isModified()).toBe(false) expect(setDocumentEdited).toHaveBeenCalledWith(false) }) }) describe('adding panels', () => { class TestItem {} // Don't use ES6 classes because then we'll have to call `super()` which we can't do with // HTMLElement function TestItemElement () { this.constructor = TestItemElement } function Ctor () { this.constructor = TestItemElement } Ctor.prototype = HTMLElement.prototype TestItemElement.prototype = new Ctor() TestItemElement.__super__ = HTMLElement.prototype TestItemElement.prototype.initialize = function (model) { this.model = model; return this } TestItemElement.prototype.getModel = function () { return this.model } beforeEach(() => atom.views.addViewProvider(TestItem, model => new TestItemElement().initialize(model)) ) describe('::addLeftPanel(model)', () => { it('adds a panel to the correct panel container', () => { let addPanelSpy expect(atom.workspace.getLeftPanels().length).toBe(0) atom.workspace.panelContainers.left.onDidAddPanel(addPanelSpy = jasmine.createSpy()) const model = new TestItem() const panel = atom.workspace.addLeftPanel({item: model}) expect(panel).toBeDefined() expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) const itemView = atom.views.getView(atom.workspace.getLeftPanels()[0].getItem()) expect(itemView instanceof TestItemElement).toBe(true) expect(itemView.getModel()).toBe(model) }) }) describe('::addRightPanel(model)', () => { it('adds a panel to the correct panel container', () => { let addPanelSpy expect(atom.workspace.getRightPanels().length).toBe(0) atom.workspace.panelContainers.right.onDidAddPanel(addPanelSpy = jasmine.createSpy()) const model = new TestItem() const panel = atom.workspace.addRightPanel({item: model}) expect(panel).toBeDefined() expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) const itemView = atom.views.getView(atom.workspace.getRightPanels()[0].getItem()) expect(itemView instanceof TestItemElement).toBe(true) expect(itemView.getModel()).toBe(model) }) }) describe('::addTopPanel(model)', () => { it('adds a panel to the correct panel container', () => { let addPanelSpy expect(atom.workspace.getTopPanels().length).toBe(0) atom.workspace.panelContainers.top.onDidAddPanel(addPanelSpy = jasmine.createSpy()) const model = new TestItem() const panel = atom.workspace.addTopPanel({item: model}) expect(panel).toBeDefined() expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) const itemView = atom.views.getView(atom.workspace.getTopPanels()[0].getItem()) expect(itemView instanceof TestItemElement).toBe(true) expect(itemView.getModel()).toBe(model) }) }) describe('::addBottomPanel(model)', () => { it('adds a panel to the correct panel container', () => { let addPanelSpy expect(atom.workspace.getBottomPanels().length).toBe(0) atom.workspace.panelContainers.bottom.onDidAddPanel(addPanelSpy = jasmine.createSpy()) const model = new TestItem() const panel = atom.workspace.addBottomPanel({item: model}) expect(panel).toBeDefined() expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) const itemView = atom.views.getView(atom.workspace.getBottomPanels()[0].getItem()) expect(itemView instanceof TestItemElement).toBe(true) expect(itemView.getModel()).toBe(model) }) }) describe('::addHeaderPanel(model)', () => { it('adds a panel to the correct panel container', () => { let addPanelSpy expect(atom.workspace.getHeaderPanels().length).toBe(0) atom.workspace.panelContainers.header.onDidAddPanel(addPanelSpy = jasmine.createSpy()) const model = new TestItem() const panel = atom.workspace.addHeaderPanel({item: model}) expect(panel).toBeDefined() expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) const itemView = atom.views.getView(atom.workspace.getHeaderPanels()[0].getItem()) expect(itemView instanceof TestItemElement).toBe(true) expect(itemView.getModel()).toBe(model) }) }) describe('::addFooterPanel(model)', () => { it('adds a panel to the correct panel container', () => { let addPanelSpy expect(atom.workspace.getFooterPanels().length).toBe(0) atom.workspace.panelContainers.footer.onDidAddPanel(addPanelSpy = jasmine.createSpy()) const model = new TestItem() const panel = atom.workspace.addFooterPanel({item: model}) expect(panel).toBeDefined() expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) const itemView = atom.views.getView(atom.workspace.getFooterPanels()[0].getItem()) expect(itemView instanceof TestItemElement).toBe(true) expect(itemView.getModel()).toBe(model) }) }) describe('::addModalPanel(model)', () => { it('adds a panel to the correct panel container', () => { let addPanelSpy expect(atom.workspace.getModalPanels().length).toBe(0) atom.workspace.panelContainers.modal.onDidAddPanel(addPanelSpy = jasmine.createSpy()) const model = new TestItem() const panel = atom.workspace.addModalPanel({item: model}) expect(panel).toBeDefined() expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) const itemView = atom.views.getView(atom.workspace.getModalPanels()[0].getItem()) expect(itemView instanceof TestItemElement).toBe(true) expect(itemView.getModel()).toBe(model) }) }) describe('::panelForItem(item)', () => { it('returns the panel associated with the item', () => { const item = new TestItem() const panel = atom.workspace.addLeftPanel({item}) const itemWithNoPanel = new TestItem() expect(atom.workspace.panelForItem(item)).toBe(panel) expect(atom.workspace.panelForItem(itemWithNoPanel)).toBe(null) }) }) }) describe('::scan(regex, options, callback)', () => { describe('when called with a regex', () => { it('calls the callback with all regex results in all files in the project', () => { const results = [] waitsForPromise(() => atom.workspace.scan( /(a)+/, {leadingContextLineCount: 1, trailingContextLineCount: 1}, result => results.push(result)) ) runs(() => { expect(results).toHaveLength(3) expect(results[0].filePath).toBe(atom.project.getDirectories()[0].resolve('a')) expect(results[0].matches).toHaveLength(3) expect(results[0].matches[0]).toEqual({ matchText: 'aaa', lineText: 'aaa bbb', lineTextOffset: 0, range: [[0, 0], [0, 3]], leadingContextLines: [], trailingContextLines: ['cc aa cc'] }) }) }) it('works with with escaped literals (like $ and ^)', () => { const results = [] waitsForPromise(() => atom.workspace.scan( /\$\w+/, {leadingContextLineCount: 1, trailingContextLineCount: 1}, result => results.push(result))) runs(() => { expect(results.length).toBe(1) const {filePath, matches} = results[0] expect(filePath).toBe(atom.project.getDirectories()[0].resolve('a')) expect(matches).toHaveLength(1) expect(matches[0]).toEqual({ matchText: '$bill', lineText: 'dollar$bill', lineTextOffset: 0, range: [[2, 6], [2, 11]], leadingContextLines: ['cc aa cc'], trailingContextLines: [] }) }) }) it('works on evil filenames', () => { atom.config.set('core.excludeVcsIgnoredPaths', false) platform.generateEvilFiles() atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')]) const paths = [] let matches = [] waitsForPromise(() => atom.workspace.scan(/evil/, result => { paths.push(result.filePath) matches = matches.concat(result.matches) }) ) runs(() => { _.each(matches, m => expect(m.matchText).toEqual('evil')) if (platform.isWindows()) { expect(paths.length).toBe(3) expect(paths[0]).toMatch(/a_file_with_utf8.txt$/) expect(paths[1]).toMatch(/file with spaces.txt$/) expect(path.basename(paths[2])).toBe('utfa\u0306.md') } else { expect(paths.length).toBe(5) expect(paths[0]).toMatch(/a_file_with_utf8.txt$/) expect(paths[1]).toMatch(/file with spaces.txt$/) expect(paths[2]).toMatch(/goddam\nnewlines$/m) expect(paths[3]).toMatch(/quote".txt$/m) expect(path.basename(paths[4])).toBe('utfa\u0306.md') } }) }) it('ignores case if the regex includes the `i` flag', () => { const results = [] waitsForPromise(() => atom.workspace.scan(/DOLLAR/i, result => results.push(result))) runs(() => expect(results).toHaveLength(1)) }) describe('when the core.excludeVcsIgnoredPaths config is truthy', () => { let projectPath let ignoredPath beforeEach(() => { const sourceProjectPath = path.join(__dirname, 'fixtures', 'git', 'working-dir') projectPath = path.join(temp.mkdirSync('atom')) const writerStream = fstream.Writer(projectPath) fstream.Reader(sourceProjectPath).pipe(writerStream) waitsFor(done => { writerStream.on('close', done) writerStream.on('error', done) }) runs(() => { fs.rename(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')) ignoredPath = path.join(projectPath, 'ignored.txt') fs.writeFileSync(ignoredPath, 'this match should not be included') }) }) afterEach(() => { if (fs.existsSync(projectPath)) { fs.removeSync(projectPath) } }) it('excludes ignored files', () => { atom.project.setPaths([projectPath]) atom.config.set('core.excludeVcsIgnoredPaths', true) const resultHandler = jasmine.createSpy('result found') waitsForPromise(() => atom.workspace.scan(/match/, results => resultHandler()) ) runs(() => expect(resultHandler).not.toHaveBeenCalled()) }) }) it('includes only files when a directory filter is specified', () => { const projectPath = path.join(path.join(__dirname, 'fixtures', 'dir')) atom.project.setPaths([projectPath]) const filePath = path.join(projectPath, 'a-dir', 'oh-git') const paths = [] let matches = [] waitsForPromise(() => atom.workspace.scan(/aaa/, {paths: [`a-dir${path.sep}`]}, result => { paths.push(result.filePath) matches = matches.concat(result.matches) }) ) runs(() => { expect(paths.length).toBe(1) expect(paths[0]).toBe(filePath) expect(matches.length).toBe(1) }) }) it("includes files and folders that begin with a '.'", () => { const projectPath = temp.mkdirSync('atom-spec-workspace') const filePath = path.join(projectPath, '.text') fs.writeFileSync(filePath, 'match this') atom.project.setPaths([projectPath]) const paths = [] let matches = [] waitsForPromise(() => atom.workspace.scan(/match this/, result => { paths.push(result.filePath) matches = matches.concat(result.matches) }) ) runs(() => { expect(paths.length).toBe(1) expect(paths[0]).toBe(filePath) expect(matches.length).toBe(1) }) }) it('excludes values in core.ignoredNames', () => { const ignoredNames = atom.config.get('core.ignoredNames') ignoredNames.push('a') atom.config.set('core.ignoredNames', ignoredNames) const resultHandler = jasmine.createSpy('result found') waitsForPromise(() => atom.workspace.scan(/dollar/, results => resultHandler()) ) runs(() => expect(resultHandler).not.toHaveBeenCalled()) }) it('scans buffer contents if the buffer is modified', () => { let editor = null const results = [] waitsForPromise(() => atom.workspace.open('a').then(o => { editor = o editor.setText('Elephant') }) ) waitsForPromise(() => atom.workspace.scan(/a|Elephant/, result => results.push(result))) runs(() => { expect(results).toHaveLength(3) const resultForA = _.find(results, ({filePath}) => path.basename(filePath) === 'a') expect(resultForA.matches).toHaveLength(1) expect(resultForA.matches[0].matchText).toBe('Elephant') }) }) it('ignores buffers outside the project', () => { let editor = null const results = [] waitsForPromise(() => atom.workspace.open(temp.openSync().path).then(o => { editor = o editor.setText('Elephant') }) ) waitsForPromise(() => atom.workspace.scan(/Elephant/, result => results.push(result))) runs(() => expect(results).toHaveLength(0)) }) describe('when the project has multiple root directories', () => { let dir1 let dir2 let file1 let file2 beforeEach(() => { dir1 = atom.project.getPaths()[0] file1 = path.join(dir1, 'a-dir', 'oh-git') dir2 = temp.mkdirSync('a-second-dir') const aDir2 = path.join(dir2, 'a-dir') file2 = path.join(aDir2, 'a-file') fs.mkdirSync(aDir2) fs.writeFileSync(file2, 'ccc aaaa') atom.project.addPath(dir2) }) it("searches matching files in all of the project's root directories", () => { const resultPaths = [] waitsForPromise(() => atom.workspace.scan(/aaaa/, ({filePath}) => resultPaths.push(filePath)) ) runs(() => expect(resultPaths.sort()).toEqual([file1, file2].sort())) }) describe('when an inclusion path starts with the basename of a root directory', () => { it('interprets the inclusion path as starting from that directory', () => { waitsForPromise(() => { const resultPaths = [] return atom.workspace .scan(/aaaa/, {paths: ['dir']}, ({filePath}) => { if (!resultPaths.includes(filePath)) { resultPaths.push(filePath) } }) .then(() => expect(resultPaths).toEqual([file1])) }) waitsForPromise(() => { const resultPaths = [] return atom.workspace .scan(/aaaa/, {paths: [path.join('dir', 'a-dir')]}, ({filePath}) => { if (!resultPaths.includes(filePath)) { resultPaths.push(filePath) } }) .then(() => expect(resultPaths).toEqual([file1])) }) waitsForPromise(() => { const resultPaths = [] return atom.workspace .scan(/aaaa/, {paths: [path.basename(dir2)]}, ({filePath}) => { if (!resultPaths.includes(filePath)) { resultPaths.push(filePath) } }) .then(() => expect(resultPaths).toEqual([file2])) }) waitsForPromise(() => { const resultPaths = [] return atom.workspace .scan(/aaaa/, {paths: [path.join(path.basename(dir2), 'a-dir')]}, ({filePath}) => { if (!resultPaths.includes(filePath)) { resultPaths.push(filePath) } }) .then(() => expect(resultPaths).toEqual([file2])) }) }) }) describe('when a custom directory searcher is registered', () => { let fakeSearch = null // Function that is invoked once all of the fields on fakeSearch are set. let onFakeSearchCreated = null class FakeSearch { constructor (options) { // Note that hoisting resolve and reject in this way is generally frowned upon. this.options = options this.promise = new Promise((resolve, reject) => { this.hoistedResolve = resolve this.hoistedReject = reject if (typeof onFakeSearchCreated === 'function') { onFakeSearchCreated(this) } }) } then (...args) { return this.promise.then.apply(this.promise, args) } cancel () { this.cancelled = true // According to the spec for a DirectorySearcher, invoking `cancel()` should // resolve the thenable rather than reject it. this.hoistedResolve() } } beforeEach(() => { fakeSearch = null onFakeSearchCreated = null atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { canSearchDirectory (directory) { return directory.getPath() === dir1 }, search (directory, regex, options) { fakeSearch = new FakeSearch(options) return fakeSearch } }) waitsFor(() => atom.workspace.directorySearchers.length > 0) }) it('can override the DefaultDirectorySearcher on a per-directory basis', () => { const foreignFilePath = 'ssh://foreign-directory:8080/hello.txt' const numPathsSearchedInDir2 = 1 const numPathsToPretendToSearchInCustomDirectorySearcher = 10 const searchResult = { filePath: foreignFilePath, matches: [ { lineText: 'Hello world', lineTextOffset: 0, matchText: 'Hello', range: [[0, 0], [0, 5]] } ] } onFakeSearchCreated = fakeSearch => { fakeSearch.options.didMatch(searchResult) fakeSearch.options.didSearchPaths(numPathsToPretendToSearchInCustomDirectorySearcher) fakeSearch.hoistedResolve() } const resultPaths = [] const onPathsSearched = jasmine.createSpy('onPathsSearched') waitsForPromise(() => atom.workspace.scan(/aaaa/, {onPathsSearched}, ({filePath}) => resultPaths.push(filePath)) ) runs(() => { expect(resultPaths.sort()).toEqual([foreignFilePath, file2].sort()) // onPathsSearched should be called once by each DirectorySearcher. The order is not // guaranteed, so we can only verify the total number of paths searched is correct // after the second call. expect(onPathsSearched.callCount).toBe(2) expect(onPathsSearched.mostRecentCall.args[0]).toBe( numPathsToPretendToSearchInCustomDirectorySearcher + numPathsSearchedInDir2) }) }) it('can be cancelled when the object returned by scan() has its cancel() method invoked', () => { const thenable = atom.workspace.scan(/aaaa/, () => {}) let resultOfPromiseSearch = null waitsFor('fakeSearch to be defined', () => fakeSearch != null) runs(() => { expect(fakeSearch.cancelled).toBe(undefined) thenable.cancel() expect(fakeSearch.cancelled).toBe(true) }) waitsForPromise(() => thenable.then(promiseResult => { resultOfPromiseSearch = promiseResult })) runs(() => expect(resultOfPromiseSearch).toBe('cancelled')) }) it('will have the side-effect of failing the overall search if it fails', () => { // This provider's search should be cancelled when the first provider fails let cancelableSearch let fakeSearch2 = null atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { canSearchDirectory (directory) { return directory.getPath() === dir2 }, search (directory, regex, options) { fakeSearch2 = new FakeSearch(options) return fakeSearch2 } }) let didReject = false const promise = cancelableSearch = atom.workspace.scan(/aaaa/, () => {}) waitsFor('fakeSearch to be defined', () => fakeSearch != null) runs(() => fakeSearch.hoistedReject()) waitsForPromise(() => cancelableSearch.catch(() => { didReject = true })) waitsFor(done => promise.then(null, done)) runs(() => { expect(didReject).toBe(true) expect(fakeSearch2.cancelled).toBe(true) }) }) }) }) }) }) // Cancels other ongoing searches describe('::replace(regex, replacementText, paths, iterator)', () => { let filePath let commentFilePath let sampleContent let sampleCommentContent beforeEach(() => { atom.project.setPaths([atom.project.getDirectories()[0].resolve('../')]) filePath = atom.project.getDirectories()[0].resolve('sample.js') commentFilePath = atom.project.getDirectories()[0].resolve('sample-with-comments.js') sampleContent = fs.readFileSync(filePath).toString() sampleCommentContent = fs.readFileSync(commentFilePath).toString() }) afterEach(() => { fs.writeFileSync(filePath, sampleContent) fs.writeFileSync(commentFilePath, sampleCommentContent) }) describe("when a file doesn't exist", () => { it('calls back with an error', () => { const errors = [] const missingPath = path.resolve('/not-a-file.js') expect(fs.existsSync(missingPath)).toBeFalsy() waitsForPromise(() => atom.workspace.replace(/items/gi, 'items', [missingPath], (result, error) => errors.push(error)) ) runs(() => { expect(errors).toHaveLength(1) expect(errors[0].path).toBe(missingPath) }) }) }) describe('when called with unopened files', () => { it('replaces properly', () => { const results = [] waitsForPromise(() => atom.workspace.replace(/items/gi, 'items', [filePath], result => results.push(result)) ) runs(() => { expect(results).toHaveLength(1) expect(results[0].filePath).toBe(filePath) expect(results[0].replacements).toBe(6) }) }) }) describe('when a buffer is already open', () => { it('replaces properly and saves when not modified', () => { let editor = null const results = [] waitsForPromise(() => atom.workspace.open('sample.js').then(o => { editor = o })) runs(() => expect(editor.isModified()).toBeFalsy()) waitsForPromise(() => atom.workspace.replace(/items/gi, 'items', [filePath], result => results.push(result)) ) runs(() => { expect(results).toHaveLength(1) expect(results[0].filePath).toBe(filePath) expect(results[0].replacements).toBe(6) expect(editor.isModified()).toBeFalsy() }) }) it('does not replace when the path is not specified', () => { const results = [] waitsForPromise(() => atom.workspace.open('sample-with-comments.js')) waitsForPromise(() => atom.workspace.replace(/items/gi, 'items', [commentFilePath], result => results.push(result)) ) runs(() => { expect(results).toHaveLength(1) expect(results[0].filePath).toBe(commentFilePath) }) }) it('does NOT save when modified', () => { let editor = null const results = [] waitsForPromise(() => atom.workspace.open('sample.js').then(o => { editor = o })) runs(() => { editor.buffer.setTextInRange([[0, 0], [0, 0]], 'omg') expect(editor.isModified()).toBeTruthy() }) waitsForPromise(() => atom.workspace.replace(/items/gi, 'okthen', [filePath], result => results.push(result)) ) runs(() => { expect(results).toHaveLength(1) expect(results[0].filePath).toBe(filePath) expect(results[0].replacements).toBe(6) expect(editor.isModified()).toBeTruthy() }) }) }) }) describe('::saveActivePaneItem()', () => { let editor = null beforeEach(() => waitsForPromise(() => atom.workspace.open('sample.js').then(o => { editor = o })) ) describe('when there is an error', () => { it('emits a warning notification when the file cannot be saved', () => { let addedSpy spyOn(editor, 'save').andCallFake(() => { throw new Error("'/some/file' is a directory") }) atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) atom.workspace.saveActivePaneItem() expect(addedSpy).toHaveBeenCalled() expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') }) it('emits a warning notification when the directory cannot be written to', () => { let addedSpy spyOn(editor, 'save').andCallFake(() => { throw new Error("ENOTDIR, not a directory '/Some/dir/and-a-file.js'") }) atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) atom.workspace.saveActivePaneItem() expect(addedSpy).toHaveBeenCalled() expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') }) it('emits a warning notification when the user does not have permission', () => { let addedSpy spyOn(editor, 'save').andCallFake(() => { const error = new Error("EACCES, permission denied '/Some/dir/and-a-file.js'") error.code = 'EACCES' error.path = '/Some/dir/and-a-file.js' throw error }) atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) atom.workspace.saveActivePaneItem() expect(addedSpy).toHaveBeenCalled() expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') }) it('emits a warning notification when the operation is not permitted', () => { spyOn(editor, 'save').andCallFake(() => { const error = new Error("EPERM, operation not permitted '/Some/dir/and-a-file.js'") error.code = 'EPERM' error.path = '/Some/dir/and-a-file.js' throw error }) }) it('emits a warning notification when the file is already open by another app', () => { let addedSpy spyOn(editor, 'save').andCallFake(() => { const error = new Error("EBUSY, resource busy or locked '/Some/dir/and-a-file.js'") error.code = 'EBUSY' error.path = '/Some/dir/and-a-file.js' throw error }) atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) atom.workspace.saveActivePaneItem() expect(addedSpy).toHaveBeenCalled() const notificaiton = addedSpy.mostRecentCall.args[0] expect(notificaiton.getType()).toBe('warning') expect(notificaiton.getMessage()).toContain('Unable to save') }) it('emits a warning notification when the file system is read-only', () => { let addedSpy spyOn(editor, 'save').andCallFake(() => { const error = new Error("EROFS, read-only file system '/Some/dir/and-a-file.js'") error.code = 'EROFS' error.path = '/Some/dir/and-a-file.js' throw error }) atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) atom.workspace.saveActivePaneItem() expect(addedSpy).toHaveBeenCalled() const notification = addedSpy.mostRecentCall.args[0] expect(notification.getType()).toBe('warning') expect(notification.getMessage()).toContain('Unable to save') }) it('emits a warning notification when the file cannot be saved', () => { spyOn(editor, 'save').andCallFake(() => { throw new Error('no one knows') }) const save = () => atom.workspace.saveActivePaneItem() expect(save).toThrow() }) }) }) describe('::closeActivePaneItemOrEmptyPaneOrWindow', () => { beforeEach(() => { spyOn(atom, 'close') waitsForPromise(() => atom.workspace.open()) }) it('closes the active pane item, or the active pane if it is empty, or the current window if there is only the empty root pane', () => { atom.config.set('core.destroyEmptyPanes', false) const pane1 = atom.workspace.getActivePane() const pane2 = pane1.splitRight({copyActiveItem: true}) expect(atom.workspace.getCenter().getPanes().length).toBe(2) expect(pane2.getItems().length).toBe(1) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() expect(atom.workspace.getCenter().getPanes().length).toBe(2) expect(pane2.getItems().length).toBe(0) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() expect(atom.workspace.getCenter().getPanes().length).toBe(1) expect(pane1.getItems().length).toBe(1) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() expect(atom.workspace.getCenter().getPanes().length).toBe(1) expect(pane1.getItems().length).toBe(0) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() expect(atom.workspace.getCenter().getPanes().length).toBe(1) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() expect(atom.close).toHaveBeenCalled() }) }) describe('when the core.allowPendingPaneItems option is falsey', () => { it('does not open item with `pending: true` option as pending', () => { let pane = null atom.config.set('core.allowPendingPaneItems', false) waitsForPromise(() => atom.workspace.open('sample.js', {pending: true}).then(() => { pane = atom.workspace.getActivePane() }) ) runs(() => expect(pane.getPendingItem()).toBeFalsy()) }) }) describe('grammar activation', () => { it('notifies the workspace of which grammar is used', () => { atom.packages.triggerDeferredActivationHooks() const javascriptGrammarUsed = jasmine.createSpy('js grammar used') const rubyGrammarUsed = jasmine.createSpy('ruby grammar used') const cGrammarUsed = jasmine.createSpy('c grammar used') atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', javascriptGrammarUsed) atom.packages.onDidTriggerActivationHook('language-ruby:grammar-used', rubyGrammarUsed) atom.packages.onDidTriggerActivationHook('language-c:grammar-used', cGrammarUsed) waitsForPromise(() => atom.packages.activatePackage('language-ruby')) waitsForPromise(() => atom.packages.activatePackage('language-javascript')) waitsForPromise(() => atom.packages.activatePackage('language-c')) waitsForPromise(() => atom.workspace.open('sample-with-comments.js')) runs(() => { // Hooks are triggered when opening new editors expect(javascriptGrammarUsed).toHaveBeenCalled() // Hooks are triggered when changing existing editors grammars atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.c')) expect(cGrammarUsed).toHaveBeenCalled() // Hooks are triggered when editors are added in other ways. atom.workspace.getActivePane().splitRight({copyActiveItem: true}) atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.ruby')) expect(rubyGrammarUsed).toHaveBeenCalled() }) }) }) describe('.checkoutHeadRevision()', () => { let editor = null beforeEach(() => { atom.config.set('editor.confirmCheckoutHeadRevision', false) waitsForPromise(() => atom.workspace.open('sample-with-comments.js').then(o => { editor = o })) }) it('reverts to the version of its file checked into the project repository', () => { editor.setCursorBufferPosition([0, 0]) editor.insertText('---\n') expect(editor.lineTextForBufferRow(0)).toBe('---') waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor)) runs(() => expect(editor.lineTextForBufferRow(0)).toBe('')) }) describe("when there's no repository for the editor's file", () => { it("doesn't do anything", () => { editor = new TextEditor() editor.setText('stuff') atom.workspace.checkoutHeadRevision(editor) waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor)) }) }) }) describe('when an item is moved', () => { beforeEach(() => { atom.workspace.enablePersistence = true }) afterEach(async () => { await atom.workspace.itemLocationStore.clear() atom.workspace.enablePersistence = false }) it("stores the new location if it's not the default", () => { const ITEM_URI = 'atom://test' const item = { getURI: () => ITEM_URI, getDefaultLocation: () => 'left', getElement: () => document.createElement('div') } const centerPane = workspace.getActivePane() centerPane.addItem(item) const dockPane = atom.workspace.getRightDock().getActivePane() spyOn(workspace.itemLocationStore, 'save') centerPane.moveItemToPane(item, dockPane) expect(workspace.itemLocationStore.save).toHaveBeenCalledWith(ITEM_URI, 'right') }) it("clears the location if it's the default", () => { const ITEM_URI = 'atom://test' const item = { getURI: () => ITEM_URI, getDefaultLocation: () => 'right', getElement: () => document.createElement('div') } const centerPane = workspace.getActivePane() centerPane.addItem(item) const dockPane = atom.workspace.getRightDock().getActivePane() spyOn(workspace.itemLocationStore, 'save') spyOn(workspace.itemLocationStore, 'delete') centerPane.moveItemToPane(item, dockPane) expect(workspace.itemLocationStore.delete).toHaveBeenCalledWith(ITEM_URI) expect(workspace.itemLocationStore.save).not.toHaveBeenCalled() }) }) }) const escapeStringRegex = str => str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')