Files
atom/spec/atom-environment-spec.coffee
Antonio Scandurra 0f6eadcfce Wait for windows' state to be saved before closing the app or any window
Previously, we used to save the window's state in the renderer process
`beforeunload` event handler: because of the synchronous nature of event
handlers and the asynchronous design of IndexedDB, this could
potentially not save anything if windows close fast enough to prevent
IndexedDB from committing the pending transaction containing the state.
(Ref.: https://mzl.la/2bXCXDn)

With this commit, we will intercept the `before-quit` events on
`electron.app` and the `close` event on `BrowserWindow` (which will fire
respectively before quitting the application and before closing a
window), and prevent them from performing the default action. We will
then ask each renderer process to save its state and, finally, close the
window and/or the app.
2016-09-07 13:03:33 +02:00

434 lines
16 KiB
CoffeeScript

_ = require 'underscore-plus'
path = require 'path'
temp = require 'temp'
Package = require '../src/package'
ThemeManager = require '../src/theme-manager'
AtomEnvironment = require '../src/atom-environment'
StorageFolder = require '../src/storage-folder'
describe "AtomEnvironment", ->
describe 'window sizing methods', ->
describe '::getPosition and ::setPosition', ->
originalPosition = null
beforeEach ->
originalPosition = atom.getPosition()
afterEach ->
atom.setPosition(originalPosition.x, originalPosition.y)
it 'sets the position of the window, and can retrieve the position just set', ->
atom.setPosition(22, 45)
expect(atom.getPosition()).toEqual x: 22, y: 45
describe '::getSize and ::setSize', ->
originalSize = null
beforeEach ->
originalSize = atom.getSize()
afterEach ->
atom.setSize(originalSize.width, originalSize.height)
it 'sets the size of the window, and can retrieve the size just set', ->
newWidth = originalSize.width + 12
newHeight = originalSize.height + 23
atom.setSize(newWidth, newHeight)
expect(atom.getSize()).toEqual width: newWidth, height: newHeight
describe ".isReleasedVersion()", ->
it "returns false if the version is a SHA and true otherwise", ->
version = '0.1.0'
spyOn(atom, 'getVersion').andCallFake -> version
expect(atom.isReleasedVersion()).toBe true
version = '36b5518'
expect(atom.isReleasedVersion()).toBe false
describe "loading default config", ->
it 'loads the default core config schema', ->
expect(atom.config.get('core.excludeVcsIgnoredPaths')).toBe true
expect(atom.config.get('core.followSymlinks')).toBe true
expect(atom.config.get('editor.showInvisibles')).toBe false
describe "window onerror handler", ->
devToolsPromise = null
beforeEach ->
devToolsPromise = Promise.resolve()
spyOn(atom, 'openDevTools').andReturn(devToolsPromise)
spyOn(atom, 'executeJavaScriptInDevTools')
it "will open the dev tools when an error is triggered", ->
try
a + 1
catch e
window.onerror.call(window, e.toString(), 'abc', 2, 3, e)
waitsForPromise -> devToolsPromise
runs ->
expect(atom.openDevTools).toHaveBeenCalled()
expect(atom.executeJavaScriptInDevTools).toHaveBeenCalled()
describe "::onWillThrowError", ->
willThrowSpy = null
beforeEach ->
willThrowSpy = jasmine.createSpy()
it "is called when there is an error", ->
error = null
atom.onWillThrowError(willThrowSpy)
try
a + 1
catch e
error = e
window.onerror.call(window, e.toString(), 'abc', 2, 3, e)
delete willThrowSpy.mostRecentCall.args[0].preventDefault
expect(willThrowSpy).toHaveBeenCalledWith
message: error.toString()
url: 'abc'
line: 2
column: 3
originalError: error
it "will not show the devtools when preventDefault() is called", ->
willThrowSpy.andCallFake (errorObject) -> errorObject.preventDefault()
atom.onWillThrowError(willThrowSpy)
try
a + 1
catch e
window.onerror.call(window, e.toString(), 'abc', 2, 3, e)
expect(willThrowSpy).toHaveBeenCalled()
expect(atom.openDevTools).not.toHaveBeenCalled()
expect(atom.executeJavaScriptInDevTools).not.toHaveBeenCalled()
describe "::onDidThrowError", ->
didThrowSpy = null
beforeEach ->
didThrowSpy = jasmine.createSpy()
it "is called when there is an error", ->
error = null
atom.onDidThrowError(didThrowSpy)
try
a + 1
catch e
error = e
window.onerror.call(window, e.toString(), 'abc', 2, 3, e)
expect(didThrowSpy).toHaveBeenCalledWith
message: error.toString()
url: 'abc'
line: 2
column: 3
originalError: error
describe ".assert(condition, message, callback)", ->
errors = null
beforeEach ->
errors = []
atom.onDidFailAssertion (error) -> errors.push(error)
describe "if the condition is false", ->
it "notifies onDidFailAssertion handlers with an error object based on the call site of the assertion", ->
result = atom.assert(false, "a == b")
expect(result).toBe false
expect(errors.length).toBe 1
expect(errors[0].message).toBe "Assertion failed: a == b"
expect(errors[0].stack).toContain('atom-environment-spec')
describe "if passed a callback function", ->
it "calls the callback with the assertion failure's error object", ->
error = null
atom.assert(false, "a == b", (e) -> error = e)
expect(error).toBe errors[0]
describe "if the condition is true", ->
it "does nothing", ->
result = atom.assert(true, "a == b")
expect(result).toBe true
expect(errors).toEqual []
describe "saving and loading", ->
beforeEach ->
atom.enablePersistence = true
afterEach ->
atom.enablePersistence = false
it "selects the state based on the current project paths", ->
jasmine.useRealClock()
[dir1, dir2] = [temp.mkdirSync("dir1-"), temp.mkdirSync("dir2-")]
loadSettings = _.extend atom.getLoadSettings(),
initialPaths: [dir1]
windowState: null
spyOn(atom, 'getLoadSettings').andCallFake -> loadSettings
spyOn(atom, 'serialize').andReturn({stuff: 'cool'})
atom.project.setPaths([dir1, dir2])
# State persistence will fail if other Atom instances are running
waitsForPromise ->
atom.stateStore.connect().then (isConnected) ->
expect(isConnected).toBe true
waitsForPromise ->
atom.saveState().then ->
atom.loadState().then (state) ->
expect(state).toBeFalsy()
waitsForPromise ->
loadSettings.initialPaths = [dir2, dir1]
atom.loadState().then (state) ->
expect(state).toEqual({stuff: 'cool'})
it "loads state from the storage folder when it can't be found in atom.stateStore", ->
jasmine.useRealClock()
storageFolderState = {foo: 1, bar: 2}
serializedState = {someState: 42}
loadSettings = _.extend(atom.getLoadSettings(), {initialPaths: [temp.mkdirSync("project-directory")]})
spyOn(atom, 'getLoadSettings').andReturn(loadSettings)
spyOn(atom, 'serialize').andReturn(serializedState)
spyOn(atom, 'getStorageFolder').andReturn(new StorageFolder(temp.mkdirSync("config-directory")))
atom.project.setPaths(atom.getLoadSettings().initialPaths)
waitsForPromise ->
atom.stateStore.connect()
runs ->
atom.getStorageFolder().storeSync(atom.getStateKey(loadSettings.initialPaths), storageFolderState)
waitsForPromise ->
atom.loadState().then (state) -> expect(state).toEqual(storageFolderState)
waitsForPromise ->
atom.saveState()
waitsForPromise ->
atom.loadState().then (state) -> expect(state).toEqual(serializedState)
it "saves state when the CPU is idle after a keydown or mousedown event", ->
spyOn(atom, 'saveState')
idleCallbacks = []
spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback)
keydown = new KeyboardEvent('keydown')
atom.document.dispatchEvent(keydown)
advanceClock atom.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
atom.saveState.reset()
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
advanceClock atom.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
it "ignores mousedown/keydown events happening after calling unloadEditorWindow", ->
spyOn(atom, 'saveState')
idleCallbacks = []
spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback)
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
atom.unloadEditorWindow()
expect(atom.saveState).not.toHaveBeenCalled()
advanceClock atom.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atom.saveState).not.toHaveBeenCalled()
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
advanceClock atom.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atom.saveState).not.toHaveBeenCalled()
it "serializes the project state with all the options supplied in saveState", ->
spyOn(atom.project, 'serialize').andReturn({foo: 42})
waitsForPromise -> atom.saveState({anyOption: 'any option'})
runs ->
expect(atom.project.serialize.calls.length).toBe(1)
expect(atom.project.serialize.mostRecentCall.args[0]).toEqual({anyOption: 'any option'})
it "serializes the text editor registry", ->
editor = null
waitsForPromise ->
atom.workspace.open('sample.js').then (e) -> editor = e
runs ->
atom.textEditors.setGrammarOverride(editor, 'text.plain')
atom2 = new AtomEnvironment({
applicationDelegate: atom.applicationDelegate,
window: document.createElement('div'),
document: Object.assign(
document.createElement('div'),
{
body: document.createElement('div'),
head: document.createElement('div'),
}
)
})
atom2.deserialize(atom.serialize())
expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain')
describe "openInitialEmptyEditorIfNecessary", ->
describe "when there are no paths set", ->
beforeEach ->
spyOn(atom, 'getLoadSettings').andReturn(initialPaths: [])
it "opens an empty buffer", ->
spyOn(atom.workspace, 'open')
atom.openInitialEmptyEditorIfNecessary()
expect(atom.workspace.open).toHaveBeenCalledWith(null)
describe "when there is already a buffer open", ->
beforeEach ->
waitsForPromise -> atom.workspace.open()
it "does not open an empty buffer", ->
spyOn(atom.workspace, 'open')
atom.openInitialEmptyEditorIfNecessary()
expect(atom.workspace.open).not.toHaveBeenCalled()
describe "when the project has a path", ->
beforeEach ->
spyOn(atom, 'getLoadSettings').andReturn(initialPaths: ['something'])
spyOn(atom.workspace, 'open')
it "does not open an empty buffer", ->
atom.openInitialEmptyEditorIfNecessary()
expect(atom.workspace.open).not.toHaveBeenCalled()
describe "adding a project folder", ->
it "adds a second path to the project", ->
initialPaths = atom.project.getPaths()
tempDirectory = temp.mkdirSync("a-new-directory")
spyOn(atom, "pickFolder").andCallFake (callback) ->
callback([tempDirectory])
atom.addProjectFolder()
expect(atom.project.getPaths()).toEqual(initialPaths.concat([tempDirectory]))
it "does nothing if the user dismisses the file picker", ->
initialPaths = atom.project.getPaths()
tempDirectory = temp.mkdirSync("a-new-directory")
spyOn(atom, "pickFolder").andCallFake (callback) -> callback(null)
atom.addProjectFolder()
expect(atom.project.getPaths()).toEqual(initialPaths)
describe "::unloadEditorWindow()", ->
it "saves the BlobStore so it can be loaded after reload", ->
configDirPath = temp.mkdirSync()
fakeBlobStore = jasmine.createSpyObj("blob store", ["save"])
atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, enablePersistence: true, configDirPath, blobStore: fakeBlobStore, window, document})
atomEnvironment.unloadEditorWindow()
expect(fakeBlobStore.save).toHaveBeenCalled()
atomEnvironment.destroy()
describe "::destroy()", ->
it "does not throw exceptions when unsubscribing from ipc events (regression)", ->
configDirPath = temp.mkdirSync()
fakeDocument = {
addEventListener: ->
removeEventListener: ->
head: document.createElement('head')
body: document.createElement('body')
}
atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, window, document: fakeDocument})
spyOn(atomEnvironment.packages, 'getAvailablePackagePaths').andReturn []
spyOn(atomEnvironment, 'displayWindow').andReturn Promise.resolve()
atomEnvironment.startEditorWindow()
atomEnvironment.unloadEditorWindow()
atomEnvironment.destroy()
describe "::openLocations(locations) (called via IPC from browser process)", ->
beforeEach ->
spyOn(atom.workspace, 'open')
atom.project.setPaths([])
describe "when the opened path exists", ->
it "adds it to the project's paths", ->
pathToOpen = __filename
atom.openLocations([{pathToOpen}])
expect(atom.project.getPaths()[0]).toBe __dirname
describe "then a second path is opened with forceAddToWindow", ->
it "adds the second path to the project's paths", ->
firstPathToOpen = __dirname
secondPathToOpen = path.resolve(__dirname, './fixtures')
atom.openLocations([{pathToOpen: firstPathToOpen}])
atom.openLocations([{pathToOpen: secondPathToOpen, forceAddToWindow: true}])
expect(atom.project.getPaths()).toEqual([firstPathToOpen, secondPathToOpen])
describe "when the opened path does not exist but its parent directory does", ->
it "adds the parent directory to the project paths", ->
pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt')
atom.openLocations([{pathToOpen}])
expect(atom.project.getPaths()[0]).toBe __dirname
describe "when the opened path is a file", ->
it "opens it in the workspace", ->
pathToOpen = __filename
atom.openLocations([{pathToOpen}])
expect(atom.workspace.open.mostRecentCall.args[0]).toBe __filename
describe "when the opened path is a directory", ->
it "does not open it in the workspace", ->
pathToOpen = __dirname
atom.openLocations([{pathToOpen}])
expect(atom.workspace.open.callCount).toBe 0
describe "when the opened path is a uri", ->
it "adds it to the project's paths as is", ->
pathToOpen = 'remote://server:7644/some/dir/path'
atom.openLocations([{pathToOpen}])
expect(atom.project.getPaths()[0]).toBe pathToOpen
describe "::updateAvailable(info) (called via IPC from browser process)", ->
subscription = null
afterEach ->
subscription?.dispose()
it "invokes onUpdateAvailable listeners", ->
atom.listenForUpdates()
updateAvailableHandler = jasmine.createSpy("update-available-handler")
subscription = atom.onUpdateAvailable updateAvailableHandler
autoUpdater = require('electron').remote.require('auto-updater')
autoUpdater.emit 'update-downloaded', null, "notes", "version"
waitsFor ->
updateAvailableHandler.callCount > 0
runs ->
{releaseVersion} = updateAvailableHandler.mostRecentCall.args[0]
expect(releaseVersion).toBe 'version'
describe "::getReleaseChannel()", ->
[version] = []
beforeEach ->
spyOn(atom, 'getVersion').andCallFake -> version
it "returns the correct channel based on the version number", ->
version = '1.5.6'
expect(atom.getReleaseChannel()).toBe 'stable'
version = '1.5.0-beta10'
expect(atom.getReleaseChannel()).toBe 'beta'
version = '1.7.0-dev-5340c91'
expect(atom.getReleaseChannel()).toBe 'dev'