Merge branch 'master' into b3-failing-seed

This commit is contained in:
Wliu
2017-11-12 13:27:28 +01:00
110 changed files with 21316 additions and 18185 deletions

View File

@@ -34,7 +34,7 @@ export function afterEach (fn) {
}
})
export async function conditionPromise (condition) {
export async function conditionPromise (condition, description = 'anonymous condition') {
const startTime = Date.now()
while (true) {
@@ -45,7 +45,7 @@ export async function conditionPromise (condition) {
}
if (Date.now() - startTime > 5000) {
throw new Error('Timed out waiting on condition')
throw new Error('Timed out waiting on ' + description)
}
}
}

View File

@@ -1,711 +0,0 @@
_ = require 'underscore-plus'
path = require 'path'
temp = require('temp').track()
AtomEnvironment = require '../src/atom-environment'
StorageFolder = require '../src/storage-folder'
describe "AtomEnvironment", ->
afterEach ->
try
temp.cleanupSync()
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
waitsForPromise ->
atom.setSize(newWidth, newHeight)
runs ->
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 = []
spyOn(atom, 'isReleasedVersion').andReturn(true)
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 passed metadata", ->
it "assigns the metadata on the assertion failure's error object", ->
atom.assert(false, "a == b", {foo: 'bar'})
expect(errors[0].metadata).toEqual {foo: 'bar'}
describe "when Atom has been built from source", ->
it "throws an error", ->
atom.isReleasedVersion.andReturn(false)
expect(-> atom.assert(false, 'testing')).toThrow('Assertion failed: testing')
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", ->
atomEnv = new AtomEnvironment({
applicationDelegate: global.atom.applicationDelegate,
})
idleCallbacks = []
atomEnv.initialize({
window: {
requestIdleCallback: (callback) -> idleCallbacks.push(callback),
addEventListener: ->
removeEventListener: ->
},
document: document.implementation.createHTMLDocument()
})
spyOn(atomEnv, 'saveState')
keydown = new KeyboardEvent('keydown')
atomEnv.document.dispatchEvent(keydown)
advanceClock atomEnv.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false})
expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true})
atomEnv.saveState.reset()
mousedown = new MouseEvent('mousedown')
atomEnv.document.dispatchEvent(mousedown)
advanceClock atomEnv.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false})
expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true})
atomEnv.destroy()
it "ignores mousedown/keydown events happening after calling unloadEditorWindow", ->
atomEnv = new AtomEnvironment({
applicationDelegate: global.atom.applicationDelegate,
})
idleCallbacks = []
atomEnv.initialize({
window: {
requestIdleCallback: (callback) -> idleCallbacks.push(callback),
addEventListener: ->
removeEventListener: ->
},
document: document.implementation.createHTMLDocument()
})
spyOn(atomEnv, 'saveState')
mousedown = new MouseEvent('mousedown')
atomEnv.document.dispatchEvent(mousedown)
atomEnv.unloadEditorWindow()
expect(atomEnv.saveState).not.toHaveBeenCalled()
advanceClock atomEnv.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atomEnv.saveState).not.toHaveBeenCalled()
mousedown = new MouseEvent('mousedown')
atomEnv.document.dispatchEvent(mousedown)
advanceClock atomEnv.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atomEnv.saveState).not.toHaveBeenCalled()
atomEnv.destroy()
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
waitsForPromise ->
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.initialize({document, window})
atom2.deserialize(atom.serialize()).then ->
expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain')
atom2.destroy()
describe "deserialization failures", ->
it "propagates project state restoration failures", ->
spyOn(atom.project, 'deserialize').andCallFake ->
err = new Error('deserialization failure')
err.missingProjectPaths = ['/foo']
Promise.reject(err)
spyOn(atom.notifications, 'addError')
waitsForPromise -> atom.deserialize({project: 'should work'})
runs ->
expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open project directory',
{description: 'Project directory `/foo` is no longer on disk.'}
it "accumulates and reports two errors with one notification", ->
spyOn(atom.project, 'deserialize').andCallFake ->
err = new Error('deserialization failure')
err.missingProjectPaths = ['/foo', '/wat']
Promise.reject(err)
spyOn(atom.notifications, 'addError')
waitsForPromise -> atom.deserialize({project: 'should work'})
runs ->
expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open 2 project directories',
{description: 'Project directories `/foo` and `/wat` are no longer on disk.'}
it "accumulates and reports three+ errors with one notification", ->
spyOn(atom.project, 'deserialize').andCallFake ->
err = new Error('deserialization failure')
err.missingProjectPaths = ['/foo', '/wat', '/stuff', '/things']
Promise.reject(err)
spyOn(atom.notifications, 'addError')
waitsForPromise -> atom.deserialize({project: 'should work'})
runs ->
expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open 4 project directories',
{description: 'Project directories `/foo`, `/wat`, `/stuff`, and `/things` are no longer on disk.'}
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 "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 "when there is no saved state for the added folders", ->
beforeEach ->
spyOn(atom, 'loadState').andReturn(Promise.resolve(null))
spyOn(atom, 'attemptRestoreProjectStateForPaths')
it "adds the selected folder to the project", ->
initialPaths = atom.project.setPaths([])
tempDirectory = temp.mkdirSync("a-new-directory")
spyOn(atom, "pickFolder").andCallFake (callback) ->
callback([tempDirectory])
waitsForPromise ->
atom.addProjectFolder()
runs ->
expect(atom.project.getPaths()).toEqual([tempDirectory])
expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled()
describe "when there is saved state for the relevant directories", ->
state = Symbol('savedState')
beforeEach ->
spyOn(atom, "getStateKey").andCallFake (dirs) -> dirs.join(':')
spyOn(atom, "loadState").andCallFake (key) ->
if key is __dirname then Promise.resolve(state) else Promise.resolve(null)
spyOn(atom, "attemptRestoreProjectStateForPaths")
spyOn(atom, "pickFolder").andCallFake (callback) ->
callback([__dirname])
atom.project.setPaths([])
describe "when there are no project folders", ->
it "attempts to restore the project state", ->
waitsForPromise ->
atom.addProjectFolder()
runs ->
expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname])
expect(atom.project.getPaths()).toEqual([])
describe "when there are already project folders", ->
openedPath = path.join(__dirname, 'fixtures')
beforeEach ->
atom.project.setPaths([openedPath])
it "does not attempt to restore the project state, instead adding the project paths", ->
waitsForPromise ->
atom.addProjectFolder()
runs ->
expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled()
expect(atom.project.getPaths()).toEqual([openedPath, __dirname])
describe "attemptRestoreProjectStateForPaths(state, projectPaths, filesToOpen)", ->
describe "when the window is clean (empty or has only unnamed, unmodified buffers)", ->
beforeEach ->
# Unnamed, unmodified buffer doesn't count toward "clean"-ness
waitsForPromise -> atom.workspace.open()
it "automatically restores the saved state into the current environment", ->
state = Symbol()
spyOn(atom.workspace, 'open')
spyOn(atom, 'restoreStateIntoThisEnvironment')
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.restoreStateIntoThisEnvironment).toHaveBeenCalledWith(state)
expect(atom.workspace.open.callCount).toBe(1)
expect(atom.workspace.open).toHaveBeenCalledWith(__filename)
describe "when a dock has a non-text editor", ->
it "doesn't prompt the user to restore state", ->
dock = atom.workspace.getLeftDock()
dock.getActivePane().addItem
getTitle: -> 'title'
element: document.createElement 'div'
state = Symbol()
spyOn(atom, 'confirm')
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.confirm).not.toHaveBeenCalled()
describe "when the window is dirty", ->
editor = null
beforeEach ->
waitsForPromise -> atom.workspace.open().then (e) ->
editor = e
editor.setText('new editor')
describe "when a dock has a modified editor", ->
it "prompts the user to restore the state", ->
dock = atom.workspace.getLeftDock()
dock.getActivePane().addItem editor
spyOn(atom, "confirm").andReturn(1)
spyOn(atom.project, 'addPath')
spyOn(atom.workspace, 'open')
state = Symbol()
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.confirm).toHaveBeenCalled()
it "prompts the user to restore the state in a new window, discarding it and adding folder to current window", ->
spyOn(atom, "confirm").andReturn(1)
spyOn(atom.project, 'addPath')
spyOn(atom.workspace, 'open')
state = Symbol()
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.confirm).toHaveBeenCalled()
expect(atom.project.addPath.callCount).toBe(1)
expect(atom.project.addPath).toHaveBeenCalledWith(__dirname)
expect(atom.workspace.open.callCount).toBe(1)
expect(atom.workspace.open).toHaveBeenCalledWith(__filename)
it "prompts the user to restore the state in a new window, opening a new window", ->
spyOn(atom, "confirm").andReturn(0)
spyOn(atom, "open")
state = Symbol()
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.confirm).toHaveBeenCalled()
expect(atom.open).toHaveBeenCalledWith
pathsToOpen: [__dirname, __filename]
newWindow: true
devMode: atom.inDevMode()
safeMode: atom.inSafeMode()
describe "::unloadEditorWindow()", ->
it "saves the BlobStore so it can be loaded after reload", ->
configDirPath = temp.mkdirSync('atom-spec-environment')
fakeBlobStore = jasmine.createSpyObj("blob store", ["save"])
atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, enablePersistence: true})
atomEnvironment.initialize({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('atom-spec-environment')
fakeDocument = {
addEventListener: ->
removeEventListener: ->
head: document.createElement('head')
body: document.createElement('body')
}
atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate})
atomEnvironment.initialize({window, document: fakeDocument})
spyOn(atomEnvironment.packages, 'loadPackages').andReturn(Promise.resolve())
spyOn(atomEnvironment.packages, 'activate').andReturn(Promise.resolve())
spyOn(atomEnvironment, 'displayWindow').andReturn(Promise.resolve())
waitsForPromise ->
atomEnvironment.startEditorWindow()
runs ->
atomEnvironment.unloadEditorWindow()
atomEnvironment.destroy()
describe "::whenShellEnvironmentLoaded()", ->
[atomEnvironment, envLoaded, spy] = []
beforeEach ->
resolve = null
promise = new Promise (r) -> resolve = r
envLoaded = ->
resolve()
waitsForPromise -> promise
atomEnvironment = new AtomEnvironment
applicationDelegate: atom.applicationDelegate
updateProcessEnv: -> promise
atomEnvironment.initialize({window, document})
spy = jasmine.createSpy()
afterEach ->
atomEnvironment.destroy()
it "is triggered once the shell environment is loaded", ->
atomEnvironment.whenShellEnvironmentLoaded spy
atomEnvironment.updateProcessEnvAndTriggerHooks()
envLoaded()
runs -> expect(spy).toHaveBeenCalled()
it "triggers the callback immediately if the shell environment is already loaded", ->
atomEnvironment.updateProcessEnvAndTriggerHooks()
envLoaded()
runs ->
atomEnvironment.whenShellEnvironmentLoaded spy
expect(spy).toHaveBeenCalled()
describe "::openLocations(locations) (called via IPC from browser process)", ->
beforeEach ->
spyOn(atom.workspace, 'open')
atom.project.setPaths([])
describe "when there is no saved state", ->
beforeEach ->
spyOn(atom, "loadState").andReturn(Promise.resolve(null))
describe "when the opened path exists", ->
it "adds it to the project's paths", ->
pathToOpen = __filename
waitsForPromise -> atom.openLocations([{pathToOpen}])
runs -> 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')
waitsForPromise -> atom.openLocations([{pathToOpen: firstPathToOpen}])
waitsForPromise -> atom.openLocations([{pathToOpen: secondPathToOpen, forceAddToWindow: true}])
runs -> 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')
waitsForPromise -> atom.openLocations([{pathToOpen}])
runs -> expect(atom.project.getPaths()[0]).toBe __dirname
describe "when the opened path is a file", ->
it "opens it in the workspace", ->
pathToOpen = __filename
waitsForPromise -> atom.openLocations([{pathToOpen}])
runs -> 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
waitsForPromise -> atom.openLocations([{pathToOpen}])
runs -> 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'
spyOn(atom.project, 'addPath')
waitsForPromise -> atom.openLocations([{pathToOpen}])
runs -> expect(atom.project.addPath).toHaveBeenCalledWith(pathToOpen)
describe "when there is saved state for the relevant directories", ->
state = Symbol('savedState')
beforeEach ->
spyOn(atom, "getStateKey").andCallFake (dirs) -> dirs.join(':')
spyOn(atom, "loadState").andCallFake (key) ->
if key is __dirname then Promise.resolve(state) else Promise.resolve(null)
spyOn(atom, "attemptRestoreProjectStateForPaths")
describe "when there are no project folders", ->
it "attempts to restore the project state", ->
pathToOpen = __dirname
waitsForPromise -> atom.openLocations([{pathToOpen}])
runs ->
expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [pathToOpen], [])
expect(atom.project.getPaths()).toEqual([])
it "opens the specified files", ->
waitsForPromise -> atom.openLocations([{pathToOpen: __dirname}, {pathToOpen: __filename}])
runs ->
expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname], [__filename])
expect(atom.project.getPaths()).toEqual([])
describe "when there are already project folders", ->
beforeEach ->
atom.project.setPaths([__dirname])
it "does not attempt to restore the project state, instead adding the project paths", ->
pathToOpen = path.join(__dirname, 'fixtures')
waitsForPromise -> atom.openLocations([{pathToOpen, forceAddToWindow: true}])
runs ->
expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled()
expect(atom.project.getPaths()).toEqual([__dirname, pathToOpen])
it "opens the specified files", ->
pathToOpen = path.join(__dirname, 'fixtures')
fileToOpen = path.join(pathToOpen, 'michelle-is-awesome.txt')
waitsForPromise -> atom.openLocations([{pathToOpen}, {pathToOpen: fileToOpen}])
runs ->
expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalledWith(state, [pathToOpen], [fileToOpen])
expect(atom.project.getPaths()).toEqual([__dirname])
describe "::updateAvailable(info) (called via IPC from browser process)", ->
subscription = null
afterEach ->
subscription?.dispose()
it "invokes onUpdateAvailable listeners", ->
return unless process.platform is 'darwin' # Test tied to electron autoUpdater, we use something else on Linux and Win32
atom.listenForUpdates()
updateAvailableHandler = jasmine.createSpy("update-available-handler")
subscription = atom.onUpdateAvailable updateAvailableHandler
autoUpdater = require('electron').remote.autoUpdater
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'

View File

@@ -0,0 +1,770 @@
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
const _ = require('underscore-plus')
const path = require('path')
const temp = require('temp').track()
const AtomEnvironment = require('../src/atom-environment')
const StorageFolder = require('../src/storage-folder')
describe('AtomEnvironment', () => {
afterEach(() => {
try {
temp.cleanupSync()
} catch (error) {}
})
describe('window sizing methods', () => {
describe('::getPosition and ::setPosition', () => {
let 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', () => {
let 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', async () => {
const newWidth = originalSize.width - 12
const newHeight = originalSize.height - 23
await 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', () => {
let 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', () => {
let 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', async () => {
try {
a + 1
} catch (e) {
window.onerror.call(window, e.toString(), 'abc', 2, 3, e)
}
await devToolsPromise
expect(atom.openDevTools).toHaveBeenCalled()
expect(atom.executeJavaScriptInDevTools).toHaveBeenCalled()
})
describe('::onWillThrowError', () => {
let willThrowSpy = null
beforeEach(() => {
willThrowSpy = jasmine.createSpy()
})
it('is called when there is an error', () => {
let 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', () => {
let didThrowSpy = null
beforeEach(() => didThrowSpy = jasmine.createSpy())
it('is called when there is an error', () => {
let 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)', () => {
let errors = null
beforeEach(() => {
errors = []
spyOn(atom, 'isReleasedVersion').andReturn(true)
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', () => {
const 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", () => {
let error = null
atom.assert(false, 'a == b', e => error = e)
expect(error).toBe(errors[0])
})
})
describe('if passed metadata', () => {
it("assigns the metadata on the assertion failure's error object", () => {
atom.assert(false, 'a == b', {foo: 'bar'})
expect(errors[0].metadata).toEqual({foo: 'bar'})
})
})
describe('when Atom has been built from source', () => {
it('throws an error', () => {
atom.isReleasedVersion.andReturn(false)
expect(() => atom.assert(false, 'testing')).toThrow('Assertion failed: testing')
})
})
})
describe('if the condition is true', () => {
it('does nothing', () => {
const 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', async () => {
jasmine.useRealClock()
const [dir1, dir2] = [temp.mkdirSync('dir1-'), temp.mkdirSync('dir2-')]
const loadSettings = Object.assign(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
expect(await atom.stateStore.connect()).toBe(true)
await atom.saveState()
expect(await atom.loadState()).toBeFalsy()
loadSettings.initialPaths = [dir2, dir1]
expect(await atom.loadState()).toEqual({stuff: 'cool'})
})
it('saves state when the CPU is idle after a keydown or mousedown event', () => {
const atomEnv = new AtomEnvironment({
applicationDelegate: global.atom.applicationDelegate
})
const idleCallbacks = []
atomEnv.initialize({
window: {
requestIdleCallback (callback) { idleCallbacks.push(callback) },
addEventListener () {},
removeEventListener () {}
},
document: document.implementation.createHTMLDocument()
})
spyOn(atomEnv, 'saveState')
const keydown = new KeyboardEvent('keydown')
atomEnv.document.dispatchEvent(keydown)
advanceClock(atomEnv.saveStateDebounceInterval)
idleCallbacks.shift()()
expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false})
expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true})
atomEnv.saveState.reset()
const mousedown = new MouseEvent('mousedown')
atomEnv.document.dispatchEvent(mousedown)
advanceClock(atomEnv.saveStateDebounceInterval)
idleCallbacks.shift()()
expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false})
expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true})
atomEnv.destroy()
})
it('ignores mousedown/keydown events happening after calling unloadEditorWindow', () => {
const atomEnv = new AtomEnvironment({
applicationDelegate: global.atom.applicationDelegate
})
const idleCallbacks = []
atomEnv.initialize({
window: {
requestIdleCallback (callback) { idleCallbacks.push(callback) },
addEventListener () {},
removeEventListener () {}
},
document: document.implementation.createHTMLDocument()
})
spyOn(atomEnv, 'saveState')
let mousedown = new MouseEvent('mousedown')
atomEnv.document.dispatchEvent(mousedown)
atomEnv.unloadEditorWindow()
expect(atomEnv.saveState).not.toHaveBeenCalled()
advanceClock(atomEnv.saveStateDebounceInterval)
idleCallbacks.shift()()
expect(atomEnv.saveState).not.toHaveBeenCalled()
mousedown = new MouseEvent('mousedown')
atomEnv.document.dispatchEvent(mousedown)
advanceClock(atomEnv.saveStateDebounceInterval)
idleCallbacks.shift()()
expect(atomEnv.saveState).not.toHaveBeenCalled()
atomEnv.destroy()
})
it('serializes the project state with all the options supplied in saveState', async () => {
spyOn(atom.project, 'serialize').andReturn({foo: 42})
await atom.saveState({anyOption: 'any option'})
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', async () => {
const editor = await atom.workspace.open('sample.js')
atom.textEditors.setGrammarOverride(editor, 'text.plain')
const 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.initialize({document, window})
await atom2.deserialize(atom.serialize())
expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain')
atom2.destroy()
})
describe('deserialization failures', () => {
it('propagates project state restoration failures', async () => {
spyOn(atom.project, 'deserialize').andCallFake(() => {
const err = new Error('deserialization failure')
err.missingProjectPaths = ['/foo']
return Promise.reject(err)
})
spyOn(atom.notifications, 'addError')
await atom.deserialize({project: 'should work'})
expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open project directory', {
description: 'Project directory `/foo` is no longer on disk.'
})
})
it('accumulates and reports two errors with one notification', async () => {
spyOn(atom.project, 'deserialize').andCallFake(() => {
const err = new Error('deserialization failure')
err.missingProjectPaths = ['/foo', '/wat']
return Promise.reject(err)
})
spyOn(atom.notifications, 'addError')
await atom.deserialize({project: 'should work'})
expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open 2 project directories', {
description: 'Project directories `/foo` and `/wat` are no longer on disk.'
})
})
it('accumulates and reports three+ errors with one notification', async () => {
spyOn(atom.project, 'deserialize').andCallFake(() => {
const err = new Error('deserialization failure')
err.missingProjectPaths = ['/foo', '/wat', '/stuff', '/things']
return Promise.reject(err)
})
spyOn(atom.notifications, 'addError')
await atom.deserialize({project: 'should work'})
expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open 4 project directories', {
description: 'Project directories `/foo`, `/wat`, `/stuff`, and `/things` are no longer on disk.'
})
})
})
})
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(async () => {
await 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('does nothing if the user dismisses the file picker', () => {
const initialPaths = atom.project.getPaths()
const tempDirectory = temp.mkdirSync('a-new-directory')
spyOn(atom, 'pickFolder').andCallFake(callback => callback(null))
atom.addProjectFolder()
expect(atom.project.getPaths()).toEqual(initialPaths)
})
describe('when there is no saved state for the added folders', () => {
beforeEach(() => {
spyOn(atom, 'loadState').andReturn(Promise.resolve(null))
spyOn(atom, 'attemptRestoreProjectStateForPaths')
})
it('adds the selected folder to the project', async () => {
const initialPaths = atom.project.setPaths([])
const tempDirectory = temp.mkdirSync('a-new-directory')
spyOn(atom, 'pickFolder').andCallFake(callback => callback([tempDirectory]))
await atom.addProjectFolder()
expect(atom.project.getPaths()).toEqual([tempDirectory])
expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled()
})
})
describe('when there is saved state for the relevant directories', () => {
const state = Symbol('savedState')
beforeEach(() => {
spyOn(atom, 'getStateKey').andCallFake(dirs => dirs.join(':'))
spyOn(atom, 'loadState').andCallFake(async (key) => key === __dirname ? state : null)
spyOn(atom, 'attemptRestoreProjectStateForPaths')
spyOn(atom, 'pickFolder').andCallFake(callback => callback([__dirname]))
atom.project.setPaths([])
})
describe('when there are no project folders', () => {
it('attempts to restore the project state', async () => {
await atom.addProjectFolder()
expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname])
expect(atom.project.getPaths()).toEqual([])
})
})
describe('when there are already project folders', () => {
const openedPath = path.join(__dirname, 'fixtures')
beforeEach(() => atom.project.setPaths([openedPath]))
it('does not attempt to restore the project state, instead adding the project paths', async () => {
await atom.addProjectFolder()
expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled()
expect(atom.project.getPaths()).toEqual([openedPath, __dirname])
})
})
})
})
describe('attemptRestoreProjectStateForPaths(state, projectPaths, filesToOpen)', () => {
describe('when the window is clean (empty or has only unnamed, unmodified buffers)', () => {
beforeEach(async () => {
// Unnamed, unmodified buffer doesn't count toward "clean"-ness
await atom.workspace.open()
})
it('automatically restores the saved state into the current environment', () => {
const state = {}
spyOn(atom.workspace, 'open')
spyOn(atom, 'restoreStateIntoThisEnvironment')
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.restoreStateIntoThisEnvironment).toHaveBeenCalledWith(state)
expect(atom.workspace.open.callCount).toBe(1)
expect(atom.workspace.open).toHaveBeenCalledWith(__filename)
})
describe('when a dock has a non-text editor', () => {
it("doesn't prompt the user to restore state", () => {
const dock = atom.workspace.getLeftDock()
dock.getActivePane().addItem({
getTitle () { return 'title' },
element: document.createElement('div')
})
const state = {}
spyOn(atom, 'confirm')
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.confirm).not.toHaveBeenCalled()
})
})
})
describe('when the window is dirty', () => {
let editor
beforeEach(async () => {
editor = await atom.workspace.open()
editor.setText('new editor')
})
describe('when a dock has a modified editor', () => {
it('prompts the user to restore the state', () => {
const dock = atom.workspace.getLeftDock()
dock.getActivePane().addItem(editor)
spyOn(atom, 'confirm').andReturn(1)
spyOn(atom.project, 'addPath')
spyOn(atom.workspace, 'open')
const state = Symbol()
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.confirm).toHaveBeenCalled()
})
})
it('prompts the user to restore the state in a new window, discarding it and adding folder to current window', () => {
spyOn(atom, 'confirm').andReturn(1)
spyOn(atom.project, 'addPath')
spyOn(atom.workspace, 'open')
const state = Symbol()
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.confirm).toHaveBeenCalled()
expect(atom.project.addPath.callCount).toBe(1)
expect(atom.project.addPath).toHaveBeenCalledWith(__dirname)
expect(atom.workspace.open.callCount).toBe(1)
expect(atom.workspace.open).toHaveBeenCalledWith(__filename)
})
it('prompts the user to restore the state in a new window, opening a new window', () => {
spyOn(atom, 'confirm').andReturn(0)
spyOn(atom, 'open')
const state = Symbol()
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.confirm).toHaveBeenCalled()
expect(atom.open).toHaveBeenCalledWith({
pathsToOpen: [__dirname, __filename],
newWindow: true,
devMode: atom.inDevMode(),
safeMode: atom.inSafeMode()
})
})
})
})
describe('::unloadEditorWindow()', () => {
it('saves the BlobStore so it can be loaded after reload', () => {
const configDirPath = temp.mkdirSync('atom-spec-environment')
const fakeBlobStore = jasmine.createSpyObj('blob store', ['save'])
const atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, enablePersistence: true})
atomEnvironment.initialize({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)', async () => {
const configDirPath = temp.mkdirSync('atom-spec-environment')
const fakeDocument = {
addEventListener () {},
removeEventListener () {},
head: document.createElement('head'),
body: document.createElement('body')
}
const atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate})
atomEnvironment.initialize({window, document: fakeDocument})
spyOn(atomEnvironment.packages, 'loadPackages').andReturn(Promise.resolve())
spyOn(atomEnvironment.packages, 'activate').andReturn(Promise.resolve())
spyOn(atomEnvironment, 'displayWindow').andReturn(Promise.resolve())
await atomEnvironment.startEditorWindow()
atomEnvironment.unloadEditorWindow()
atomEnvironment.destroy()
})
})
describe('::whenShellEnvironmentLoaded()', () => {
let atomEnvironment, envLoaded, spy
beforeEach(() => {
let resolve = null
const promise = new Promise((r) => { resolve = r })
envLoaded = () => {
resolve()
promise
}
atomEnvironment = new AtomEnvironment({
applicationDelegate: atom.applicationDelegate,
updateProcessEnv () { return promise }
})
atomEnvironment.initialize({window, document})
spy = jasmine.createSpy()
})
afterEach(() => atomEnvironment.destroy())
it('is triggered once the shell environment is loaded', async () => {
atomEnvironment.whenShellEnvironmentLoaded(spy)
atomEnvironment.updateProcessEnvAndTriggerHooks()
await envLoaded()
expect(spy).toHaveBeenCalled()
})
it('triggers the callback immediately if the shell environment is already loaded', async () => {
atomEnvironment.updateProcessEnvAndTriggerHooks()
await envLoaded()
atomEnvironment.whenShellEnvironmentLoaded(spy)
expect(spy).toHaveBeenCalled()
})
})
describe('::openLocations(locations) (called via IPC from browser process)', () => {
beforeEach(() => {
spyOn(atom.workspace, 'open')
atom.project.setPaths([])
})
describe('when there is no saved state', () => {
beforeEach(() => {
spyOn(atom, 'loadState').andReturn(Promise.resolve(null))
})
describe('when the opened path exists', () => {
it("adds it to the project's paths", async () => {
const pathToOpen = __filename
await 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", async () => {
const firstPathToOpen = __dirname
const secondPathToOpen = path.resolve(__dirname, './fixtures')
await atom.openLocations([{pathToOpen: firstPathToOpen}])
await 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', async () => {
const pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt')
await atom.openLocations([{pathToOpen}])
expect(atom.project.getPaths()[0]).toBe(__dirname)
})
})
describe('when the opened path is a file', () => {
it('opens it in the workspace', async () => {
const pathToOpen = __filename
await 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', async () => {
const pathToOpen = __dirname
await 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", async () => {
const pathToOpen = 'remote://server:7644/some/dir/path'
spyOn(atom.project, 'addPath')
await atom.openLocations([{pathToOpen}])
expect(atom.project.addPath).toHaveBeenCalledWith(pathToOpen)
})
})
})
describe('when there is saved state for the relevant directories', () => {
const state = Symbol('savedState')
beforeEach(() => {
spyOn(atom, 'getStateKey').andCallFake(dirs => dirs.join(':'))
spyOn(atom, 'loadState').andCallFake(function (key) {
if (key === __dirname) { return Promise.resolve(state) } else { return Promise.resolve(null) }
})
spyOn(atom, 'attemptRestoreProjectStateForPaths')
})
describe('when there are no project folders', () => {
it('attempts to restore the project state', async () => {
const pathToOpen = __dirname
await atom.openLocations([{pathToOpen}])
expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [pathToOpen], [])
expect(atom.project.getPaths()).toEqual([])
})
it('opens the specified files', async () => {
await atom.openLocations([{pathToOpen: __dirname}, {pathToOpen: __filename}])
expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname], [__filename])
expect(atom.project.getPaths()).toEqual([])
})
})
describe('when there are already project folders', () => {
beforeEach(() => atom.project.setPaths([__dirname]))
it('does not attempt to restore the project state, instead adding the project paths', async () => {
const pathToOpen = path.join(__dirname, 'fixtures')
await atom.openLocations([{pathToOpen, forceAddToWindow: true}])
expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled()
expect(atom.project.getPaths()).toEqual([__dirname, pathToOpen])
})
it('opens the specified files', async () => {
const pathToOpen = path.join(__dirname, 'fixtures')
const fileToOpen = path.join(pathToOpen, 'michelle-is-awesome.txt')
await atom.openLocations([{pathToOpen}, {pathToOpen: fileToOpen}])
expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalledWith(state, [pathToOpen], [fileToOpen])
expect(atom.project.getPaths()).toEqual([__dirname])
})
})
})
})
describe('::updateAvailable(info) (called via IPC from browser process)', () => {
let subscription
afterEach(() => {
if (subscription) subscription.dispose()
})
it('invokes onUpdateAvailable listeners', async () => {
if (process.platform !== 'darwin') return // Test tied to electron autoUpdater, we use something else on Linux and Win32
const updateAvailablePromise = new Promise(resolve => {
subscription = atom.onUpdateAvailable(resolve)
})
atom.listenForUpdates()
const {autoUpdater} = require('electron').remote
autoUpdater.emit('update-downloaded', null, 'notes', 'version')
const {releaseVersion} = await updateAvailablePromise
expect(releaseVersion).toBe('version')
})
})
describe('::getReleaseChannel()', () => {
let 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')
})
})
})

View File

@@ -1,5 +1,6 @@
'name': 'Test Ruby'
'scopeName': 'test.rb'
'firstLineMatch': '^\\#!.*(?:\\s|\\/)(?:testruby)(?:$|\\s)'
'fileTypes': [
'rb'
]

View File

@@ -0,0 +1,5 @@
module.exports = {
activate: () => null,
deactivate: () => null,
handleURI: () => null,
}

View File

@@ -0,0 +1,6 @@
{
"name": "package-with-uri-handler",
"uriHandler": {
"method": "handleURI"
}
}

View File

@@ -1,371 +0,0 @@
temp = require('temp').track()
GitRepository = require '../src/git-repository'
fs = require 'fs-plus'
path = require 'path'
Project = require '../src/project'
copyRepository = ->
workingDirPath = temp.mkdirSync('atom-spec-git')
fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath)
fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git'))
workingDirPath
describe "GitRepository", ->
repo = null
beforeEach ->
gitPath = path.join(temp.dir, '.git')
fs.removeSync(gitPath) if fs.isDirectorySync(gitPath)
afterEach ->
repo.destroy() if repo?.repo?
try
temp.cleanupSync() # These tests sometimes lag at shutting down resources
describe "@open(path)", ->
it "returns null when no repository is found", ->
expect(GitRepository.open(path.join(temp.dir, 'nogit.txt'))).toBeNull()
describe "new GitRepository(path)", ->
it "throws an exception when no repository is found", ->
expect(-> new GitRepository(path.join(temp.dir, 'nogit.txt'))).toThrow()
describe ".getPath()", ->
it "returns the repository path for a .git directory path with a directory", ->
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects'))
expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git')
it "returns the repository path for a repository path", ->
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git'))
expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git')
describe ".isPathIgnored(path)", ->
it "returns true for an ignored path", ->
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git'))
expect(repo.isPathIgnored('a.txt')).toBeTruthy()
it "returns false for a non-ignored path", ->
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git'))
expect(repo.isPathIgnored('b.txt')).toBeFalsy()
describe ".isPathModified(path)", ->
[repo, filePath, newPath] = []
beforeEach ->
workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath)
filePath = path.join(workingDirPath, 'a.txt')
newPath = path.join(workingDirPath, 'new-path.txt')
describe "when the path is unstaged", ->
it "returns false if the path has not been modified", ->
expect(repo.isPathModified(filePath)).toBeFalsy()
it "returns true if the path is modified", ->
fs.writeFileSync(filePath, "change")
expect(repo.isPathModified(filePath)).toBeTruthy()
it "returns true if the path is deleted", ->
fs.removeSync(filePath)
expect(repo.isPathModified(filePath)).toBeTruthy()
it "returns false if the path is new", ->
expect(repo.isPathModified(newPath)).toBeFalsy()
describe ".isPathNew(path)", ->
[filePath, newPath] = []
beforeEach ->
workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath)
filePath = path.join(workingDirPath, 'a.txt')
newPath = path.join(workingDirPath, 'new-path.txt')
fs.writeFileSync(newPath, "i'm new here")
describe "when the path is unstaged", ->
it "returns true if the path is new", ->
expect(repo.isPathNew(newPath)).toBeTruthy()
it "returns false if the path isn't new", ->
expect(repo.isPathNew(filePath)).toBeFalsy()
describe ".checkoutHead(path)", ->
[filePath] = []
beforeEach ->
workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath)
filePath = path.join(workingDirPath, 'a.txt')
it "no longer reports a path as modified after checkout", ->
expect(repo.isPathModified(filePath)).toBeFalsy()
fs.writeFileSync(filePath, 'ch ch changes')
expect(repo.isPathModified(filePath)).toBeTruthy()
expect(repo.checkoutHead(filePath)).toBeTruthy()
expect(repo.isPathModified(filePath)).toBeFalsy()
it "restores the contents of the path to the original text", ->
fs.writeFileSync(filePath, 'ch ch changes')
expect(repo.checkoutHead(filePath)).toBeTruthy()
expect(fs.readFileSync(filePath, 'utf8')).toBe ''
it "fires a status-changed event if the checkout completes successfully", ->
fs.writeFileSync(filePath, 'ch ch changes')
repo.getPathStatus(filePath)
statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatus statusHandler
repo.checkoutHead(filePath)
expect(statusHandler.callCount).toBe 1
expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: 0}
repo.checkoutHead(filePath)
expect(statusHandler.callCount).toBe 1
describe ".checkoutHeadForEditor(editor)", ->
[filePath, editor] = []
beforeEach ->
spyOn(atom, "confirm")
workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm})
filePath = path.join(workingDirPath, 'a.txt')
fs.writeFileSync(filePath, 'ch ch changes')
waitsForPromise ->
atom.workspace.open(filePath)
runs ->
editor = atom.workspace.getActiveTextEditor()
it "displays a confirmation dialog by default", ->
return if process.platform is 'win32' # Permissions issues with this test on Windows
atom.confirm.andCallFake ({buttons}) -> buttons.OK()
atom.config.set('editor.confirmCheckoutHeadRevision', true)
repo.checkoutHeadForEditor(editor)
expect(fs.readFileSync(filePath, 'utf8')).toBe ''
it "does not display a dialog when confirmation is disabled", ->
return if process.platform is 'win32' # Flakey EPERM opening a.txt on Win32
atom.config.set('editor.confirmCheckoutHeadRevision', false)
repo.checkoutHeadForEditor(editor)
expect(fs.readFileSync(filePath, 'utf8')).toBe ''
expect(atom.confirm).not.toHaveBeenCalled()
describe ".destroy()", ->
it "throws an exception when any method is called after it is called", ->
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git'))
repo.destroy()
expect(-> repo.getShortHead()).toThrow()
describe ".getPathStatus(path)", ->
[filePath] = []
beforeEach ->
workingDirectory = copyRepository()
repo = new GitRepository(workingDirectory)
filePath = path.join(workingDirectory, 'file.txt')
it "trigger a status-changed event when the new status differs from the last cached one", ->
statusHandler = jasmine.createSpy("statusHandler")
repo.onDidChangeStatus statusHandler
fs.writeFileSync(filePath, '')
status = repo.getPathStatus(filePath)
expect(statusHandler.callCount).toBe 1
expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: status}
fs.writeFileSync(filePath, 'abc')
status = repo.getPathStatus(filePath)
expect(statusHandler.callCount).toBe 1
describe ".getDirectoryStatus(path)", ->
[directoryPath, filePath] = []
beforeEach ->
workingDirectory = copyRepository()
repo = new GitRepository(workingDirectory)
directoryPath = path.join(workingDirectory, 'dir')
filePath = path.join(directoryPath, 'b.txt')
it "gets the status based on the files inside the directory", ->
expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe false
fs.writeFileSync(filePath, 'abc')
repo.getPathStatus(filePath)
expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe true
describe ".refreshStatus()", ->
[newPath, modifiedPath, cleanPath, workingDirectory] = []
beforeEach ->
workingDirectory = copyRepository()
repo = new GitRepository(workingDirectory, {project: atom.project, config: atom.config})
modifiedPath = path.join(workingDirectory, 'file.txt')
newPath = path.join(workingDirectory, 'untracked.txt')
cleanPath = path.join(workingDirectory, 'other.txt')
fs.writeFileSync(cleanPath, 'Full of text')
fs.writeFileSync(newPath, '')
newPath = fs.absolute newPath # specs could be running under symbol path.
it "returns status information for all new and modified files", ->
fs.writeFileSync(modifiedPath, 'making this path modified')
statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses statusHandler
repo.refreshStatus()
waitsFor ->
statusHandler.callCount > 0
runs ->
expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined()
expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy()
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy()
it 'caches the proper statuses when a subdir is open', ->
subDir = path.join(workingDirectory, 'dir')
fs.mkdirSync(subDir)
filePath = path.join(subDir, 'b.txt')
fs.writeFileSync(filePath, '')
atom.project.setPaths([subDir])
waitsForPromise ->
atom.workspace.open('b.txt')
statusHandler = null
runs ->
repo = atom.project.getRepositories()[0]
statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses statusHandler
repo.refreshStatus()
waitsFor ->
statusHandler.callCount > 0
runs ->
status = repo.getCachedPathStatus(filePath)
expect(repo.isStatusModified(status)).toBe false
expect(repo.isStatusNew(status)).toBe false
it "works correctly when the project has multiple folders (regression)", ->
atom.project.addPath(workingDirectory)
atom.project.addPath(path.join(__dirname, 'fixtures', 'dir'))
statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses statusHandler
repo.refreshStatus()
waitsFor ->
statusHandler.callCount > 0
runs ->
expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined()
expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy()
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy()
it 'caches statuses that were looked up synchronously', ->
originalContent = 'undefined'
fs.writeFileSync(modifiedPath, 'making this path modified')
repo.getPathStatus('file.txt')
fs.writeFileSync(modifiedPath, originalContent)
waitsForPromise -> repo.refreshStatus()
runs ->
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy()
describe "buffer events", ->
[editor] = []
beforeEach ->
statusRefreshed = false
atom.project.setPaths([copyRepository()])
atom.project.getRepositories()[0].onDidChangeStatuses -> statusRefreshed = true
waitsForPromise ->
atom.workspace.open('other.txt').then (o) -> editor = o
waitsFor 'repo to refresh', -> statusRefreshed
it "emits a status-changed event when a buffer is saved", ->
editor.insertNewline()
statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepositories()[0].onDidChangeStatus statusHandler
waitsForPromise ->
editor.save()
runs ->
expect(statusHandler.callCount).toBe 1
expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256}
it "emits a status-changed event when a buffer is reloaded", ->
fs.writeFileSync(editor.getPath(), 'changed')
statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepositories()[0].onDidChangeStatus statusHandler
waitsForPromise ->
editor.getBuffer().reload()
runs ->
expect(statusHandler.callCount).toBe 1
expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256}
waitsForPromise ->
editor.getBuffer().reload()
runs ->
expect(statusHandler.callCount).toBe 1
it "emits a status-changed event when a buffer's path changes", ->
fs.writeFileSync(editor.getPath(), 'changed')
statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepositories()[0].onDidChangeStatus statusHandler
editor.getBuffer().emitter.emit 'did-change-path'
expect(statusHandler.callCount).toBe 1
expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256}
editor.getBuffer().emitter.emit 'did-change-path'
expect(statusHandler.callCount).toBe 1
it "stops listening to the buffer when the repository is destroyed (regression)", ->
atom.project.getRepositories()[0].destroy()
expect(-> editor.save()).not.toThrow()
describe "when a project is deserialized", ->
[buffer, project2, statusHandler] = []
afterEach ->
project2?.destroy()
it "subscribes to all the serialized buffers in the project", ->
atom.project.setPaths([copyRepository()])
waitsForPromise ->
atom.workspace.open('file.txt')
waitsForPromise ->
project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm, applicationDelegate: atom.applicationDelegate})
project2.deserialize(atom.project.serialize({isUnloading: false}))
waitsFor ->
buffer = project2.getBuffers()[0]
waitsForPromise ->
originalContent = buffer.getText()
buffer.append('changes')
statusHandler = jasmine.createSpy('statusHandler')
project2.getRepositories()[0].onDidChangeStatus statusHandler
buffer.save()
runs ->
expect(statusHandler.callCount).toBe 1
expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256}

393
spec/git-repository-spec.js Normal file
View File

@@ -0,0 +1,393 @@
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
const path = require('path')
const fs = require('fs-plus')
const temp = require('temp').track()
const GitRepository = require('../src/git-repository')
const Project = require('../src/project')
describe('GitRepository', () => {
let repo
beforeEach(() => {
const gitPath = path.join(temp.dir, '.git')
if (fs.isDirectorySync(gitPath)) fs.removeSync(gitPath)
})
afterEach(() => {
if (repo && !repo.isDestroyed()) repo.destroy()
// These tests sometimes lag at shutting down resources
try {
temp.cleanupSync()
} catch (error) {}
})
describe('@open(path)', () => {
it('returns null when no repository is found', () => {
expect(GitRepository.open(path.join(temp.dir, 'nogit.txt'))).toBeNull()
})
})
describe('new GitRepository(path)', () => {
it('throws an exception when no repository is found', () => {
expect(() => new GitRepository(path.join(temp.dir, 'nogit.txt'))).toThrow()
})
})
describe('.getPath()', () => {
it('returns the repository path for a .git directory path with a directory', () => {
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects'))
expect(repo.getPath()).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git'))
})
it('returns the repository path for a repository path', () => {
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git'))
expect(repo.getPath()).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git'))
})
})
describe('.isPathIgnored(path)', () => {
it('returns true for an ignored path', () => {
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git'))
expect(repo.isPathIgnored('a.txt')).toBeTruthy()
})
it('returns false for a non-ignored path', () => {
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git'))
expect(repo.isPathIgnored('b.txt')).toBeFalsy()
})
})
describe('.isPathModified(path)', () => {
let filePath, newPath
beforeEach(() => {
const workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath)
filePath = path.join(workingDirPath, 'a.txt')
newPath = path.join(workingDirPath, 'new-path.txt')
})
describe('when the path is unstaged', () => {
it('returns false if the path has not been modified', () => {
expect(repo.isPathModified(filePath)).toBeFalsy()
})
it('returns true if the path is modified', () => {
fs.writeFileSync(filePath, 'change')
expect(repo.isPathModified(filePath)).toBeTruthy()
})
it('returns true if the path is deleted', () => {
fs.removeSync(filePath)
expect(repo.isPathModified(filePath)).toBeTruthy()
})
it('returns false if the path is new', () => {
expect(repo.isPathModified(newPath)).toBeFalsy()
})
})
})
describe('.isPathNew(path)', () => {
let filePath, newPath
beforeEach(() => {
const workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath)
filePath = path.join(workingDirPath, 'a.txt')
newPath = path.join(workingDirPath, 'new-path.txt')
fs.writeFileSync(newPath, "i'm new here")
})
describe('when the path is unstaged', () => {
it('returns true if the path is new', () => {
expect(repo.isPathNew(newPath)).toBeTruthy()
})
it("returns false if the path isn't new", () => {
expect(repo.isPathNew(filePath)).toBeFalsy()
})
})
})
describe('.checkoutHead(path)', () => {
let filePath
beforeEach(() => {
const workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath)
filePath = path.join(workingDirPath, 'a.txt')
})
it('no longer reports a path as modified after checkout', () => {
expect(repo.isPathModified(filePath)).toBeFalsy()
fs.writeFileSync(filePath, 'ch ch changes')
expect(repo.isPathModified(filePath)).toBeTruthy()
expect(repo.checkoutHead(filePath)).toBeTruthy()
expect(repo.isPathModified(filePath)).toBeFalsy()
})
it('restores the contents of the path to the original text', () => {
fs.writeFileSync(filePath, 'ch ch changes')
expect(repo.checkoutHead(filePath)).toBeTruthy()
expect(fs.readFileSync(filePath, 'utf8')).toBe('')
})
it('fires a status-changed event if the checkout completes successfully', () => {
fs.writeFileSync(filePath, 'ch ch changes')
repo.getPathStatus(filePath)
const statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatus(statusHandler)
repo.checkoutHead(filePath)
expect(statusHandler.callCount).toBe(1)
expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: 0})
repo.checkoutHead(filePath)
expect(statusHandler.callCount).toBe(1)
})
})
describe('.checkoutHeadForEditor(editor)', () => {
let filePath, editor
beforeEach(async () => {
spyOn(atom, 'confirm')
const workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm})
filePath = path.join(workingDirPath, 'a.txt')
fs.writeFileSync(filePath, 'ch ch changes')
editor = await atom.workspace.open(filePath)
})
it('displays a confirmation dialog by default', () => {
// Permissions issues with this test on Windows
if (process.platform === 'win32') return
atom.confirm.andCallFake(({buttons}) => buttons.OK())
atom.config.set('editor.confirmCheckoutHeadRevision', true)
repo.checkoutHeadForEditor(editor)
expect(fs.readFileSync(filePath, 'utf8')).toBe('')
})
it('does not display a dialog when confirmation is disabled', () => {
// Flakey EPERM opening a.txt on Win32
if (process.platform === 'win32') return
atom.config.set('editor.confirmCheckoutHeadRevision', false)
repo.checkoutHeadForEditor(editor)
expect(fs.readFileSync(filePath, 'utf8')).toBe('')
expect(atom.confirm).not.toHaveBeenCalled()
})
})
describe('.destroy()', () => {
it('throws an exception when any method is called after it is called', () => {
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git'))
repo.destroy()
expect(() => repo.getShortHead()).toThrow()
})
})
describe('.getPathStatus(path)', () => {
let filePath
beforeEach(() => {
const workingDirectory = copyRepository()
repo = new GitRepository(workingDirectory)
filePath = path.join(workingDirectory, 'file.txt')
})
it('trigger a status-changed event when the new status differs from the last cached one', () => {
const statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatus(statusHandler)
fs.writeFileSync(filePath, '')
let status = repo.getPathStatus(filePath)
expect(statusHandler.callCount).toBe(1)
expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status})
fs.writeFileSync(filePath, 'abc')
status = repo.getPathStatus(filePath)
expect(statusHandler.callCount).toBe(1)
})
})
describe('.getDirectoryStatus(path)', () => {
let directoryPath, filePath
beforeEach(() => {
const workingDirectory = copyRepository()
repo = new GitRepository(workingDirectory)
directoryPath = path.join(workingDirectory, 'dir')
filePath = path.join(directoryPath, 'b.txt')
})
it('gets the status based on the files inside the directory', () => {
expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe(false)
fs.writeFileSync(filePath, 'abc')
repo.getPathStatus(filePath)
expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe(true)
})
})
describe('.refreshStatus()', () => {
let newPath, modifiedPath, cleanPath, workingDirectory
beforeEach(() => {
workingDirectory = copyRepository()
repo = new GitRepository(workingDirectory, {project: atom.project, config: atom.config})
modifiedPath = path.join(workingDirectory, 'file.txt')
newPath = path.join(workingDirectory, 'untracked.txt')
cleanPath = path.join(workingDirectory, 'other.txt')
fs.writeFileSync(cleanPath, 'Full of text')
fs.writeFileSync(newPath, '')
newPath = fs.absolute(newPath)
})
it('returns status information for all new and modified files', async () => {
const statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses(statusHandler)
fs.writeFileSync(modifiedPath, 'making this path modified')
await repo.refreshStatus()
expect(statusHandler.callCount).toBe(1)
expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined()
expect(repo.isStatusNew(repo.getCachedPathStatus(newPath) )).toBeTruthy()
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy()
})
it('caches the proper statuses when a subdir is open', async () => {
const subDir = path.join(workingDirectory, 'dir')
fs.mkdirSync(subDir)
const filePath = path.join(subDir, 'b.txt')
fs.writeFileSync(filePath, '')
atom.project.setPaths([subDir])
await atom.workspace.open('b.txt')
repo = atom.project.getRepositories()[0]
await repo.refreshStatus()
const status = repo.getCachedPathStatus(filePath)
expect(repo.isStatusModified(status)).toBe(false)
expect(repo.isStatusNew(status)).toBe(false)
})
it('works correctly when the project has multiple folders (regression)', async () => {
atom.project.addPath(workingDirectory)
atom.project.addPath(path.join(__dirname, 'fixtures', 'dir'))
await repo.refreshStatus()
expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined()
expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy()
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy()
})
it('caches statuses that were looked up synchronously', async () => {
const originalContent = 'undefined'
fs.writeFileSync(modifiedPath, 'making this path modified')
repo.getPathStatus('file.txt')
fs.writeFileSync(modifiedPath, originalContent)
await repo.refreshStatus()
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy()
})
})
describe('buffer events', () => {
let editor
beforeEach(async () => {
atom.project.setPaths([copyRepository()])
const refreshPromise = new Promise(resolve => atom.project.getRepositories()[0].onDidChangeStatuses(resolve))
editor = await atom.workspace.open('other.txt')
await refreshPromise
})
it('emits a status-changed event when a buffer is saved', async () => {
editor.insertNewline()
const statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepositories()[0].onDidChangeStatus(statusHandler)
await editor.save()
expect(statusHandler.callCount).toBe(1)
expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256})
})
it('emits a status-changed event when a buffer is reloaded', async () => {
fs.writeFileSync(editor.getPath(), 'changed')
const statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepositories()[0].onDidChangeStatus(statusHandler)
await editor.getBuffer().reload()
expect(statusHandler.callCount).toBe(1)
expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256})
await editor.getBuffer().reload()
expect(statusHandler.callCount).toBe(1)
})
it("emits a status-changed event when a buffer's path changes", () => {
fs.writeFileSync(editor.getPath(), 'changed')
const statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepositories()[0].onDidChangeStatus(statusHandler)
editor.getBuffer().emitter.emit('did-change-path')
expect(statusHandler.callCount).toBe(1)
expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256})
editor.getBuffer().emitter.emit('did-change-path')
expect(statusHandler.callCount).toBe(1)
})
it('stops listening to the buffer when the repository is destroyed (regression)', () => {
atom.project.getRepositories()[0].destroy()
expect(() => editor.save()).not.toThrow()
})
})
describe('when a project is deserialized', () => {
let buffer, project2, statusHandler
afterEach(() => {
if (project2) project2.destroy()
})
it('subscribes to all the serialized buffers in the project', async () => {
atom.project.setPaths([copyRepository()])
await atom.workspace.open('file.txt')
project2 = new Project({
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
applicationDelegate: atom.applicationDelegate
})
await project2.deserialize(atom.project.serialize({isUnloading: false}))
buffer = project2.getBuffers()[0]
const originalContent = buffer.getText()
buffer.append('changes')
statusHandler = jasmine.createSpy('statusHandler')
project2.getRepositories()[0].onDidChangeStatus(statusHandler)
await buffer.save()
expect(statusHandler.callCount).toBe(1)
expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256})
})
})
})
function copyRepository () {
const workingDirPath = temp.mkdirSync('atom-spec-git')
fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath)
fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git'))
return workingDirPath
}

View File

@@ -120,6 +120,8 @@ describe "the `grammars` global", ->
atom.grammars.grammarForScopeName('source.ruby').bundledPackage = true
atom.grammars.grammarForScopeName('test.rb').bundledPackage = false
expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env ruby').scopeName).toBe 'source.ruby'
expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env testruby').scopeName).toBe 'test.rb'
expect(atom.grammars.selectGrammar('test.rb').scopeName).toBe 'test.rb'
describe "when there is no file path", ->

View File

@@ -1,64 +0,0 @@
Gutter = require '../src/gutter'
GutterContainer = require '../src/gutter-container'
describe 'GutterContainer', ->
gutterContainer = null
fakeTextEditor = {
scheduleComponentUpdate: ->
}
beforeEach ->
gutterContainer = new GutterContainer fakeTextEditor
describe 'when initialized', ->
it 'it has no gutters', ->
expect(gutterContainer.getGutters().length).toBe 0
describe '::addGutter', ->
it 'creates a new gutter', ->
newGutter = gutterContainer.addGutter {'test-gutter', priority: 1}
expect(gutterContainer.getGutters()).toEqual [newGutter]
expect(newGutter.priority).toBe 1
it 'throws an error if the provided gutter name is already in use', ->
name = 'test-gutter'
gutterContainer.addGutter {name}
expect(gutterContainer.addGutter.bind(null, {name})).toThrow()
it 'keeps added gutters sorted by ascending priority', ->
gutter1 = gutterContainer.addGutter {name: 'first', priority: 1}
gutter3 = gutterContainer.addGutter {name: 'third', priority: 3}
gutter2 = gutterContainer.addGutter {name: 'second', priority: 2}
expect(gutterContainer.getGutters()).toEqual [gutter1, gutter2, gutter3]
describe '::removeGutter', ->
removedGutters = null
beforeEach ->
gutterContainer = new GutterContainer fakeTextEditor
removedGutters = []
gutterContainer.onDidRemoveGutter (gutterName) ->
removedGutters.push gutterName
it 'removes the gutter if it is contained by this GutterContainer', ->
gutter = gutterContainer.addGutter {'test-gutter'}
expect(gutterContainer.getGutters()).toEqual [gutter]
gutterContainer.removeGutter gutter
expect(gutterContainer.getGutters().length).toBe 0
expect(removedGutters).toEqual [gutter.name]
it 'throws an error if the gutter is not within this GutterContainer', ->
fakeOtherTextEditor = {}
otherGutterContainer = new GutterContainer fakeOtherTextEditor
gutter = new Gutter 'gutter-name', otherGutterContainer
expect(gutterContainer.removeGutter.bind(null, gutter)).toThrow()
describe '::destroy', ->
it 'clears its array of gutters and destroys custom gutters', ->
newGutter = gutterContainer.addGutter {'test-gutter', priority: 1}
newGutterSpy = jasmine.createSpy()
newGutter.onDidDestroy(newGutterSpy)
gutterContainer.destroy()
expect(newGutterSpy).toHaveBeenCalled()
expect(gutterContainer.getGutters()).toEqual []

View File

@@ -0,0 +1,77 @@
const Gutter = require('../src/gutter')
const GutterContainer = require('../src/gutter-container')
describe('GutterContainer', () => {
let gutterContainer = null
const fakeTextEditor = {
scheduleComponentUpdate () {}
}
beforeEach(() => {
gutterContainer = new GutterContainer(fakeTextEditor)
})
describe('when initialized', () =>
it('it has no gutters', () => {
expect(gutterContainer.getGutters().length).toBe(0)
})
)
describe('::addGutter', () => {
it('creates a new gutter', () => {
const newGutter = gutterContainer.addGutter({'test-gutter': 'test-gutter', priority: 1})
expect(gutterContainer.getGutters()).toEqual([newGutter])
expect(newGutter.priority).toBe(1)
})
it('throws an error if the provided gutter name is already in use', () => {
const name = 'test-gutter'
gutterContainer.addGutter({name})
expect(gutterContainer.addGutter.bind(null, {name})).toThrow()
})
it('keeps added gutters sorted by ascending priority', () => {
const gutter1 = gutterContainer.addGutter({name: 'first', priority: 1})
const gutter3 = gutterContainer.addGutter({name: 'third', priority: 3})
const gutter2 = gutterContainer.addGutter({name: 'second', priority: 2})
expect(gutterContainer.getGutters()).toEqual([gutter1, gutter2, gutter3])
})
})
describe('::removeGutter', () => {
let removedGutters
beforeEach(function () {
gutterContainer = new GutterContainer(fakeTextEditor)
removedGutters = []
gutterContainer.onDidRemoveGutter(gutterName => removedGutters.push(gutterName))
})
it('removes the gutter if it is contained by this GutterContainer', () => {
const gutter = gutterContainer.addGutter({'test-gutter': 'test-gutter'})
expect(gutterContainer.getGutters()).toEqual([gutter])
gutterContainer.removeGutter(gutter)
expect(gutterContainer.getGutters().length).toBe(0)
expect(removedGutters).toEqual([gutter.name])
})
it('throws an error if the gutter is not within this GutterContainer', () => {
const fakeOtherTextEditor = {}
const otherGutterContainer = new GutterContainer(fakeOtherTextEditor)
const gutter = new Gutter('gutter-name', otherGutterContainer)
expect(gutterContainer.removeGutter.bind(null, gutter)).toThrow()
})
})
describe('::destroy', () =>
it('clears its array of gutters and destroys custom gutters', () => {
const newGutter = gutterContainer.addGutter({'test-gutter': 'test-gutter', priority: 1})
const newGutterSpy = jasmine.createSpy()
newGutter.onDidDestroy(newGutterSpy)
gutterContainer.destroy()
expect(newGutterSpy).toHaveBeenCalled()
expect(gutterContainer.getGutters()).toEqual([])
})
)
})

View File

@@ -1,70 +0,0 @@
Gutter = require '../src/gutter'
describe 'Gutter', ->
fakeGutterContainer = {
scheduleComponentUpdate: ->
}
name = 'name'
describe '::hide', ->
it 'hides the gutter if it is visible.', ->
options =
name: name
visible: true
gutter = new Gutter fakeGutterContainer, options
events = []
gutter.onDidChangeVisible (gutter) ->
events.push gutter.isVisible()
expect(gutter.isVisible()).toBe true
gutter.hide()
expect(gutter.isVisible()).toBe false
expect(events).toEqual [false]
gutter.hide()
expect(gutter.isVisible()).toBe false
# An event should only be emitted when the visibility changes.
expect(events.length).toBe 1
describe '::show', ->
it 'shows the gutter if it is hidden.', ->
options =
name: name
visible: false
gutter = new Gutter fakeGutterContainer, options
events = []
gutter.onDidChangeVisible (gutter) ->
events.push gutter.isVisible()
expect(gutter.isVisible()).toBe false
gutter.show()
expect(gutter.isVisible()).toBe true
expect(events).toEqual [true]
gutter.show()
expect(gutter.isVisible()).toBe true
# An event should only be emitted when the visibility changes.
expect(events.length).toBe 1
describe '::destroy', ->
[mockGutterContainer, mockGutterContainerRemovedGutters] = []
beforeEach ->
mockGutterContainerRemovedGutters = []
mockGutterContainer = removeGutter: (destroyedGutter) ->
mockGutterContainerRemovedGutters.push destroyedGutter
it 'removes the gutter from its container.', ->
gutter = new Gutter mockGutterContainer, {name}
gutter.destroy()
expect(mockGutterContainerRemovedGutters).toEqual([gutter])
it 'calls all callbacks registered on ::onDidDestroy.', ->
gutter = new Gutter mockGutterContainer, {name}
didDestroy = false
gutter.onDidDestroy ->
didDestroy = true
gutter.destroy()
expect(didDestroy).toBe true
it 'does not allow destroying the line-number gutter', ->
gutter = new Gutter mockGutterContainer, {name: 'line-number'}
expect(gutter.destroy).toThrow()

82
spec/gutter-spec.js Normal file
View File

@@ -0,0 +1,82 @@
const Gutter = require('../src/gutter')
describe('Gutter', () => {
const fakeGutterContainer = {
scheduleComponentUpdate () {}
}
const name = 'name'
describe('::hide', () =>
it('hides the gutter if it is visible.', () => {
const options = {
name,
visible: true
}
const gutter = new Gutter(fakeGutterContainer, options)
const events = []
gutter.onDidChangeVisible(gutter => events.push(gutter.isVisible()))
expect(gutter.isVisible()).toBe(true)
gutter.hide()
expect(gutter.isVisible()).toBe(false)
expect(events).toEqual([false])
gutter.hide()
expect(gutter.isVisible()).toBe(false)
// An event should only be emitted when the visibility changes.
expect(events.length).toBe(1)
})
)
describe('::show', () =>
it('shows the gutter if it is hidden.', () => {
const options = {
name,
visible: false
}
const gutter = new Gutter(fakeGutterContainer, options)
const events = []
gutter.onDidChangeVisible(gutter => events.push(gutter.isVisible()))
expect(gutter.isVisible()).toBe(false)
gutter.show()
expect(gutter.isVisible()).toBe(true)
expect(events).toEqual([true])
gutter.show()
expect(gutter.isVisible()).toBe(true)
// An event should only be emitted when the visibility changes.
expect(events.length).toBe(1)
})
)
describe('::destroy', () => {
let mockGutterContainer, mockGutterContainerRemovedGutters
beforeEach(() => {
mockGutterContainerRemovedGutters = []
mockGutterContainer = {
removeGutter (destroyedGutter) {
mockGutterContainerRemovedGutters.push(destroyedGutter)
}
}
})
it('removes the gutter from its container.', () => {
const gutter = new Gutter(mockGutterContainer, {name})
gutter.destroy()
expect(mockGutterContainerRemovedGutters).toEqual([gutter])
})
it('calls all callbacks registered on ::onDidDestroy.', () => {
const gutter = new Gutter(mockGutterContainer, {name})
let didDestroy = false
gutter.onDidDestroy(() => { didDestroy = true })
gutter.destroy()
expect(didDestroy).toBe(true)
})
it('does not allow destroying the line-number gutter', () => {
const gutter = new Gutter(mockGutterContainer, {name: 'line-number'})
expect(gutter.destroy).toThrow()
})
})
})

View File

@@ -5,6 +5,7 @@ import dedent from 'dedent'
import electron from 'electron'
import fs from 'fs-plus'
import path from 'path'
import sinon from 'sinon'
import AtomApplication from '../../src/main-process/atom-application'
import parseCommandLine from '../../src/main-process/parse-command-line'
import {timeoutPromise, conditionPromise, emitterEventPromise} from '../async-spec-helpers'
@@ -137,7 +138,7 @@ describe('AtomApplication', function () {
// Does not change the project paths when doing so.
const reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath]))
assert.equal(reusedWindow, window1)
assert.deepEqual(atomApplication.windows, [window1])
assert.deepEqual(atomApplication.getAllWindows(), [window1])
activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
const subscription = atom.workspace.onDidChangeActivePaneItem(function (textEditor) {
sendBackToMainProcess(textEditor.getPath())
@@ -177,7 +178,7 @@ describe('AtomApplication', function () {
// parent directory to the project
let reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath, '--add']))
assert.equal(reusedWindow, window1)
assert.deepEqual(atomApplication.windows, [window1])
assert.deepEqual(atomApplication.getAllWindows(), [window1])
activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
const subscription = atom.workspace.onDidChangeActivePaneItem(function (textEditor) {
sendBackToMainProcess(textEditor.getPath())
@@ -191,7 +192,7 @@ describe('AtomApplication', function () {
// the directory to the project
reusedWindow = atomApplication.launch(parseCommandLine([dirBPath, '-a']))
assert.equal(reusedWindow, window1)
assert.deepEqual(atomApplication.windows, [window1])
assert.deepEqual(atomApplication.getAllWindows(), [window1])
await conditionPromise(async () => (await getTreeViewRootDirectories(reusedWindow)).length === 3)
assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirCPath, dirBPath])
@@ -276,7 +277,7 @@ describe('AtomApplication', function () {
})
assert.equal(window2EditorTitle, 'untitled')
assert.deepEqual(atomApplication.windows, [window1, window2])
assert.deepEqual(atomApplication.getAllWindows(), [window2, window1])
})
it('does not open an empty editor when opened with no path if the core.openEmptyEditorOnStart config setting is false', async function () {
@@ -461,6 +462,31 @@ describe('AtomApplication', function () {
assert.equal(reached, true);
windows[0].close();
})
it('triggers /core/open/file in the correct window', async function() {
const dirAPath = makeTempDir('a')
const dirBPath = makeTempDir('b')
const atomApplication = buildAtomApplication()
const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath)]))
await focusWindow(window1)
const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath)]))
await focusWindow(window2)
const fileA = path.join(dirAPath, 'file-a')
const uriA = `atom://core/open/file?filename=${fileA}`
const fileB = path.join(dirBPath, 'file-b')
const uriB = `atom://core/open/file?filename=${fileB}`
sinon.spy(window1, 'sendURIMessage')
sinon.spy(window2, 'sendURIMessage')
atomApplication.launch(parseCommandLine(['--uri-handler', uriA]))
await conditionPromise(() => window1.sendURIMessage.calledWith(uriA), `window1 to be focused from ${fileA}`)
atomApplication.launch(parseCommandLine(['--uri-handler', uriB]))
await conditionPromise(() => window2.sendURIMessage.calledWith(uriB), `window2 to be focused from ${fileB}`)
})
})
})
@@ -514,7 +540,7 @@ describe('AtomApplication', function () {
async function focusWindow (window) {
window.focus()
await window.loadedPromise
await conditionPromise(() => window.atomApplication.lastFocusedWindow === window)
await conditionPromise(() => window.atomApplication.getLastFocusedWindow() === window)
}
function mockElectronAppQuit () {

View File

@@ -0,0 +1,27 @@
/** @babel */
import parseCommandLine from '../../src/main-process/parse-command-line'
describe('parseCommandLine', function () {
describe('when --uri-handler is not passed', function () {
it('parses arguments as normal', function () {
const args = parseCommandLine(['-d', '--safe', '--test', '/some/path', 'atom://test/url', 'atom://other/url'])
assert.isTrue(args.devMode)
assert.isTrue(args.safeMode)
assert.isTrue(args.test)
assert.deepEqual(args.urlsToOpen, ['atom://test/url', 'atom://other/url'])
assert.deepEqual(args.pathsToOpen, ['/some/path'])
})
})
describe('when --uri-handler is passed', function () {
it('ignores other arguments and limits to one URL', function () {
const args = parseCommandLine(['-d', '--uri-handler', '--safe', '--test', '/some/path', 'atom://test/url', 'atom://other/url'])
assert.isUndefined(args.devMode)
assert.isUndefined(args.safeMode)
assert.isUndefined(args.test)
assert.deepEqual(args.urlsToOpen, ['atom://test/url'])
assert.deepEqual(args.pathsToOpen, [])
})
})
})

View File

@@ -6,6 +6,7 @@ describe "MenuManager", ->
beforeEach ->
menu = new MenuManager({keymapManager: atom.keymaps, packageManager: atom.packages})
spyOn(menu, 'sendToBrowserProcess') # Do not modify Atom's actual menus
menu.initialize({resourcePath: atom.getLoadSettings().resourcePath})
describe "::add(items)", ->
@@ -54,7 +55,6 @@ describe "MenuManager", ->
afterEach -> Object.defineProperty process, 'platform', value: originalPlatform
it "sends the current menu template and associated key bindings to the browser process", ->
spyOn(menu, 'sendToBrowserProcess')
menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}]
atom.keymaps.add 'test', 'atom-workspace': 'ctrl-b': 'b'
menu.update()
@@ -66,7 +66,6 @@ describe "MenuManager", ->
it "omits key bindings that are mapped to unset! in any context", ->
# it would be nice to be smarter about omitting, but that would require a much
# more dynamic interaction between the currently focused element and the menu
spyOn(menu, 'sendToBrowserProcess')
menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}]
atom.keymaps.add 'test', 'atom-workspace': 'ctrl-b': 'b'
atom.keymaps.add 'test', 'atom-text-editor': 'ctrl-b': 'unset!'
@@ -77,7 +76,6 @@ describe "MenuManager", ->
it "omits key bindings that could conflict with AltGraph characters on macOS", ->
Object.defineProperty process, 'platform', value: 'darwin'
spyOn(menu, 'sendToBrowserProcess')
menu.add [{label: "A", submenu: [
{label: "B", command: "b"},
{label: "C", command: "c"}
@@ -98,7 +96,6 @@ describe "MenuManager", ->
it "omits key bindings that could conflict with AltGraph characters on Windows", ->
Object.defineProperty process, 'platform', value: 'win32'
spyOn(menu, 'sendToBrowserProcess')
menu.add [{label: "A", submenu: [
{label: "B", command: "b"},
{label: "C", command: "c"}

View File

@@ -1,4 +1,5 @@
const path = require('path')
const url = require('url')
const Package = require('../src/package')
const PackageManager = require('../src/package-manager')
const temp = require('temp').track()
@@ -1038,6 +1039,20 @@ describe('PackageManager', () => {
})
})
describe("URI handler registration", () => {
it("registers the package's specified URI handler", async () => {
const uri = 'atom://package-with-uri-handler/some/url?with=args'
const mod = require('./fixtures/packages/package-with-uri-handler')
spyOn(mod, 'handleURI')
spyOn(atom.packages, 'hasLoadedInitialPackages').andReturn(true)
const activationPromise = atom.packages.activatePackage('package-with-uri-handler')
atom.dispatchURIMessage(uri)
await activationPromise
expect(mod.handleURI).toHaveBeenCalledWith(url.parse(uri, true), uri)
})
})
describe('service registration', () => {
it("registers the package's provided and consumed services", async () => {
const consumerModule = require('./fixtures/packages/package-with-consumed-services')

View File

@@ -1,802 +0,0 @@
temp = require('temp').track()
TextBuffer = require('text-buffer')
Project = require '../src/project'
fs = require 'fs-plus'
path = require 'path'
{Directory} = require 'pathwatcher'
{stopAllWatchers} = require '../src/path-watcher'
GitRepository = require '../src/git-repository'
describe "Project", ->
beforeEach ->
atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('dir')])
# Wait for project's service consumers to be asynchronously added
waits(1)
describe "serialization", ->
deserializedProject = null
notQuittingProject = null
quittingProject = null
afterEach ->
deserializedProject?.destroy()
notQuittingProject?.destroy()
quittingProject?.destroy()
it "does not deserialize paths to directories that don't exist", ->
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
state = atom.project.serialize()
state.paths.push('/directory/that/does/not/exist')
err = null
waitsForPromise ->
deserializedProject.deserialize(state, atom.deserializers)
.catch (e) -> err = e
runs ->
expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths())
expect(err.missingProjectPaths).toEqual ['/directory/that/does/not/exist']
it "does not deserialize paths that are now files", ->
childPath = path.join(temp.mkdirSync('atom-spec-project'), 'child')
fs.mkdirSync(childPath)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
atom.project.setPaths([childPath])
state = atom.project.serialize()
fs.rmdirSync(childPath)
fs.writeFileSync(childPath, 'surprise!\n')
err = null
waitsForPromise ->
deserializedProject.deserialize(state, atom.deserializers)
.catch (e) -> err = e
runs ->
expect(deserializedProject.getPaths()).toEqual([])
expect(err.missingProjectPaths).toEqual [childPath]
it "does not include unretained buffers in the serialized state", ->
waitsForPromise ->
atom.project.bufferForPath('a')
runs ->
expect(atom.project.getBuffers().length).toBe 1
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
waitsForPromise ->
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
runs ->
expect(deserializedProject.getBuffers().length).toBe 0
it "listens for destroyed events on deserialized buffers and removes them when they are destroyed", ->
waitsForPromise ->
atom.workspace.open('a')
runs ->
expect(atom.project.getBuffers().length).toBe 1
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
waitsForPromise ->
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
runs ->
expect(deserializedProject.getBuffers().length).toBe 1
deserializedProject.getBuffers()[0].destroy()
expect(deserializedProject.getBuffers().length).toBe 0
it "does not deserialize buffers when their path is now a directory", ->
pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
waitsForPromise ->
atom.workspace.open(pathToOpen)
runs ->
expect(atom.project.getBuffers().length).toBe 1
fs.mkdirSync(pathToOpen)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
waitsForPromise ->
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
runs ->
expect(deserializedProject.getBuffers().length).toBe 0
it "does not deserialize buffers when their path is inaccessible", ->
return if process.platform is 'win32' # chmod not supported on win32
pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
fs.writeFileSync(pathToOpen, '')
waitsForPromise ->
atom.workspace.open(pathToOpen)
runs ->
expect(atom.project.getBuffers().length).toBe 1
fs.chmodSync(pathToOpen, '000')
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
waitsForPromise ->
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
runs ->
expect(deserializedProject.getBuffers().length).toBe 0
it "does not deserialize buffers with their path is no longer present", ->
pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
fs.writeFileSync(pathToOpen, '')
waitsForPromise ->
atom.workspace.open(pathToOpen)
runs ->
expect(atom.project.getBuffers().length).toBe 1
fs.unlinkSync(pathToOpen)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
waitsForPromise ->
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
runs ->
expect(deserializedProject.getBuffers().length).toBe 0
it "deserializes buffers that have never been saved before", ->
pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
waitsForPromise ->
atom.workspace.open(pathToOpen)
runs ->
atom.workspace.getActiveTextEditor().setText('unsaved\n')
expect(atom.project.getBuffers().length).toBe 1
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
waitsForPromise ->
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
runs ->
expect(deserializedProject.getBuffers().length).toBe 1
expect(deserializedProject.getBuffers()[0].getPath()).toBe pathToOpen
expect(deserializedProject.getBuffers()[0].getText()).toBe 'unsaved\n'
it "serializes marker layers and history only if Atom is quitting", ->
waitsForPromise -> atom.workspace.open('a')
bufferA = null
layerA = null
markerA = null
runs ->
bufferA = atom.project.getBuffers()[0]
layerA = bufferA.addMarkerLayer(persistent: true)
markerA = layerA.markPosition([0, 3])
bufferA.append('!')
notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
waitsForPromise -> notQuittingProject.deserialize(atom.project.serialize({isUnloading: false}))
runs ->
expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined()
expect(notQuittingProject.getBuffers()[0].undo()).toBe(false)
quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
waitsForPromise -> quittingProject.deserialize(atom.project.serialize({isUnloading: true}))
runs ->
expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).not.toBeUndefined()
expect(quittingProject.getBuffers()[0].undo()).toBe(true)
describe "when an editor is saved and the project has no path", ->
it "sets the project's path to the saved file's parent directory", ->
tempFile = temp.openSync().path
atom.project.setPaths([])
expect(atom.project.getPaths()[0]).toBeUndefined()
editor = null
waitsForPromise ->
atom.workspace.open().then (o) -> editor = o
waitsForPromise ->
editor.saveAs(tempFile)
runs ->
expect(atom.project.getPaths()[0]).toBe path.dirname(tempFile)
describe "before and after saving a buffer", ->
[buffer] = []
beforeEach ->
waitsForPromise ->
atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then (o) ->
buffer = o
buffer.retain()
afterEach ->
buffer.release()
it "emits save events on the main process", ->
spyOn(atom.project.applicationDelegate, 'emitDidSavePath')
spyOn(atom.project.applicationDelegate, 'emitWillSavePath')
waitsForPromise -> buffer.save()
runs ->
expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1)
expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath())
expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1)
expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath())
describe "when a watch error is thrown from the TextBuffer", ->
editor = null
beforeEach ->
waitsForPromise ->
atom.workspace.open(require.resolve('./fixtures/dir/a')).then (o) -> editor = o
it "creates a warning notification", ->
atom.notifications.onDidAddNotification noteSpy = jasmine.createSpy()
error = new Error('SomeError')
error.eventType = 'resurrect'
editor.buffer.emitter.emit 'will-throw-watch-error',
handle: jasmine.createSpy()
error: error
expect(noteSpy).toHaveBeenCalled()
notification = noteSpy.mostRecentCall.args[0]
expect(notification.getType()).toBe 'warning'
expect(notification.getDetail()).toBe 'SomeError'
expect(notification.getMessage()).toContain '`resurrect`'
expect(notification.getMessage()).toContain path.join('fixtures', 'dir', 'a')
describe "when a custom repository-provider service is provided", ->
[fakeRepositoryProvider, fakeRepository] = []
beforeEach ->
fakeRepository = {destroy: -> null}
fakeRepositoryProvider = {
repositoryForDirectory: (directory) -> Promise.resolve(fakeRepository)
repositoryForDirectorySync: (directory) -> fakeRepository
}
it "uses it to create repositories for any directories that need one", ->
projectPath = temp.mkdirSync('atom-project')
atom.project.setPaths([projectPath])
expect(atom.project.getRepositories()).toEqual [null]
atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider)
waitsFor -> atom.project.repositoryProviders.length > 1
runs -> atom.project.getRepositories()[0] is fakeRepository
it "does not create any new repositories if every directory has a repository", ->
repositories = atom.project.getRepositories()
expect(repositories.length).toEqual 1
expect(repositories[0]).toBeTruthy()
atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider)
waitsFor -> atom.project.repositoryProviders.length > 1
runs -> expect(atom.project.getRepositories()).toBe repositories
it "stops using it to create repositories when the service is removed", ->
atom.project.setPaths([])
disposable = atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider)
waitsFor -> atom.project.repositoryProviders.length > 1
runs ->
disposable.dispose()
atom.project.addPath(temp.mkdirSync('atom-project'))
expect(atom.project.getRepositories()).toEqual [null]
describe "when a custom directory-provider service is provided", ->
class DummyDirectory
constructor: (@path) ->
getPath: -> @path
getFile: -> {existsSync: -> false}
getSubdirectory: -> {existsSync: -> false}
isRoot: -> true
existsSync: -> @path.endsWith('does-exist')
contains: (filePath) -> filePath.startsWith(@path)
serviceDisposable = null
beforeEach ->
serviceDisposable = atom.packages.serviceHub.provide("atom.directory-provider", "0.1.0", {
directoryForURISync: (uri) ->
if uri.startsWith("ssh://")
new DummyDirectory(uri)
else
null
})
waitsFor ->
atom.project.directoryProviders.length > 0
it "uses the provider's custom directories for any paths that it handles", ->
localPath = temp.mkdirSync('local-path')
remotePath = "ssh://foreign-directory:8080/does-exist"
atom.project.setPaths([localPath, remotePath])
directories = atom.project.getDirectories()
expect(directories[0].getPath()).toBe localPath
expect(directories[0] instanceof Directory).toBe true
expect(directories[1].getPath()).toBe remotePath
expect(directories[1] instanceof DummyDirectory).toBe true
# It does not add new remote paths that do not exist
nonExistentRemotePath = "ssh://another-directory:8080/does-not-exist"
atom.project.addPath(nonExistentRemotePath)
expect(atom.project.getDirectories().length).toBe 2
# It adds new remote paths if their directories exist.
newRemotePath = "ssh://another-directory:8080/does-exist"
atom.project.addPath(newRemotePath)
directories = atom.project.getDirectories()
expect(directories[2].getPath()).toBe newRemotePath
expect(directories[2] instanceof DummyDirectory).toBe true
it "stops using the provider when the service is removed", ->
serviceDisposable.dispose()
atom.project.setPaths(["ssh://foreign-directory:8080/does-exist"])
expect(atom.project.getDirectories().length).toBe(0)
describe ".open(path)", ->
[absolutePath, newBufferHandler] = []
beforeEach ->
absolutePath = require.resolve('./fixtures/dir/a')
newBufferHandler = jasmine.createSpy('newBufferHandler')
atom.project.onDidAddBuffer(newBufferHandler)
describe "when given an absolute path that isn't currently open", ->
it "returns a new edit session for the given path and emits 'buffer-created'", ->
editor = null
waitsForPromise ->
atom.workspace.open(absolutePath).then (o) -> editor = o
runs ->
expect(editor.buffer.getPath()).toBe absolutePath
expect(newBufferHandler).toHaveBeenCalledWith editor.buffer
describe "when given a relative path that isn't currently opened", ->
it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", ->
editor = null
waitsForPromise ->
atom.workspace.open(absolutePath).then (o) -> editor = o
runs ->
expect(editor.buffer.getPath()).toBe absolutePath
expect(newBufferHandler).toHaveBeenCalledWith editor.buffer
describe "when passed the path to a buffer that is currently opened", ->
it "returns a new edit session containing currently opened buffer", ->
editor = null
waitsForPromise ->
atom.workspace.open(absolutePath).then (o) -> editor = o
runs ->
newBufferHandler.reset()
waitsForPromise ->
atom.workspace.open(absolutePath).then ({buffer}) ->
expect(buffer).toBe editor.buffer
waitsForPromise ->
atom.workspace.open('a').then ({buffer}) ->
expect(buffer).toBe editor.buffer
expect(newBufferHandler).not.toHaveBeenCalled()
describe "when not passed a path", ->
it "returns a new edit session and emits 'buffer-created'", ->
editor = null
waitsForPromise ->
atom.workspace.open().then (o) -> editor = o
runs ->
expect(editor.buffer.getPath()).toBeUndefined()
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
describe ".bufferForPath(path)", ->
buffer = null
beforeEach ->
waitsForPromise ->
atom.project.bufferForPath("a").then (o) ->
buffer = o
buffer.retain()
afterEach ->
buffer.release()
describe "when opening a previously opened path", ->
it "does not create a new buffer", ->
waitsForPromise ->
atom.project.bufferForPath("a").then (anotherBuffer) ->
expect(anotherBuffer).toBe buffer
waitsForPromise ->
atom.project.bufferForPath("b").then (anotherBuffer) ->
expect(anotherBuffer).not.toBe buffer
waitsForPromise ->
Promise.all([
atom.project.bufferForPath('c'),
atom.project.bufferForPath('c')
]).then ([buffer1, buffer2]) ->
expect(buffer1).toBe(buffer2)
it "retries loading the buffer if it previously failed", ->
waitsForPromise shouldReject: true, ->
spyOn(TextBuffer, 'load').andCallFake ->
Promise.reject(new Error('Could not open file'))
atom.project.bufferForPath('b')
waitsForPromise shouldReject: false, ->
TextBuffer.load.andCallThrough()
atom.project.bufferForPath('b')
it "creates a new buffer if the previous buffer was destroyed", ->
buffer.release()
waitsForPromise ->
atom.project.bufferForPath("b").then (anotherBuffer) ->
expect(anotherBuffer).not.toBe buffer
describe ".repositoryForDirectory(directory)", ->
it "resolves to null when the directory does not have a repository", ->
waitsForPromise ->
directory = new Directory("/tmp")
atom.project.repositoryForDirectory(directory).then (result) ->
expect(result).toBeNull()
expect(atom.project.repositoryProviders.length).toBeGreaterThan 0
expect(atom.project.repositoryPromisesByPath.size).toBe 0
it "resolves to a GitRepository and is cached when the given directory is a Git repo", ->
waitsForPromise ->
directory = new Directory(path.join(__dirname, '..'))
promise = atom.project.repositoryForDirectory(directory)
promise.then (result) ->
expect(result).toBeInstanceOf GitRepository
dirPath = directory.getRealPathSync()
expect(result.getPath()).toBe path.join(dirPath, '.git')
# Verify that the result is cached.
expect(atom.project.repositoryForDirectory(directory)).toBe(promise)
it "creates a new repository if a previous one with the same directory had been destroyed", ->
repository = null
directory = new Directory(path.join(__dirname, '..'))
waitsForPromise ->
atom.project.repositoryForDirectory(directory).then (repo) -> repository = repo
runs ->
expect(repository.isDestroyed()).toBe(false)
repository.destroy()
expect(repository.isDestroyed()).toBe(true)
waitsForPromise ->
atom.project.repositoryForDirectory(directory).then (repo) -> repository = repo
runs ->
expect(repository.isDestroyed()).toBe(false)
describe ".setPaths(paths, options)", ->
describe "when path is a file", ->
it "sets its path to the file's parent directory and updates the root directory", ->
filePath = require.resolve('./fixtures/dir/a')
atom.project.setPaths([filePath])
expect(atom.project.getPaths()[0]).toEqual path.dirname(filePath)
expect(atom.project.getDirectories()[0].path).toEqual path.dirname(filePath)
describe "when path is a directory", ->
it "assigns the directories and repositories", ->
directory1 = temp.mkdirSync("non-git-repo")
directory2 = temp.mkdirSync("git-repo1")
directory3 = temp.mkdirSync("git-repo2")
gitDirPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git'))
fs.copySync(gitDirPath, path.join(directory2, ".git"))
fs.copySync(gitDirPath, path.join(directory3, ".git"))
atom.project.setPaths([directory1, directory2, directory3])
[repo1, repo2, repo3] = atom.project.getRepositories()
expect(repo1).toBeNull()
expect(repo2.getShortHead()).toBe "master"
expect(repo2.getPath()).toBe fs.realpathSync(path.join(directory2, ".git"))
expect(repo3.getShortHead()).toBe "master"
expect(repo3.getPath()).toBe fs.realpathSync(path.join(directory3, ".git"))
it "calls callbacks registered with ::onDidChangePaths", ->
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
atom.project.onDidChangePaths(onDidChangePathsSpy)
paths = [ temp.mkdirSync("dir1"), temp.mkdirSync("dir2") ]
atom.project.setPaths(paths)
expect(onDidChangePathsSpy.callCount).toBe 1
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths)
it "optionally throws an error with any paths that did not exist", ->
paths = [temp.mkdirSync("exists0"), "/doesnt-exists/0", temp.mkdirSync("exists1"), "/doesnt-exists/1"]
try
atom.project.setPaths paths, mustExist: true
expect('no exception thrown').toBeUndefined()
catch e
expect(e.missingProjectPaths).toEqual [paths[1], paths[3]]
expect(atom.project.getPaths()).toEqual [paths[0], paths[2]]
describe "when no paths are given", ->
it "clears its path", ->
atom.project.setPaths([])
expect(atom.project.getPaths()).toEqual []
expect(atom.project.getDirectories()).toEqual []
it "normalizes the path to remove consecutive slashes, ., and .. segments", ->
atom.project.setPaths(["#{require.resolve('./fixtures/dir/a')}#{path.sep}b#{path.sep}#{path.sep}.."])
expect(atom.project.getPaths()[0]).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
expect(atom.project.getDirectories()[0].path).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
describe ".addPath(path, options)", ->
it "calls callbacks registered with ::onDidChangePaths", ->
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
atom.project.onDidChangePaths(onDidChangePathsSpy)
[oldPath] = atom.project.getPaths()
newPath = temp.mkdirSync("dir")
atom.project.addPath(newPath)
expect(onDidChangePathsSpy.callCount).toBe 1
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath])
it "doesn't add redundant paths", ->
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
atom.project.onDidChangePaths(onDidChangePathsSpy)
[oldPath] = atom.project.getPaths()
# Doesn't re-add an existing root directory
atom.project.addPath(oldPath)
expect(atom.project.getPaths()).toEqual([oldPath])
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
# Doesn't add an entry for a file-path within an existing root directory
atom.project.addPath(path.join(oldPath, 'some-file.txt'))
expect(atom.project.getPaths()).toEqual([oldPath])
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
# Does add an entry for a directory within an existing directory
newPath = path.join(oldPath, "a-dir")
atom.project.addPath(newPath)
expect(atom.project.getPaths()).toEqual([oldPath, newPath])
expect(onDidChangePathsSpy).toHaveBeenCalled()
it "doesn't add non-existent directories", ->
previousPaths = atom.project.getPaths()
atom.project.addPath('/this-definitely/does-not-exist')
expect(atom.project.getPaths()).toEqual(previousPaths)
it "optionally throws on non-existent directories", ->
expect ->
atom.project.addPath '/this-definitely/does-not-exist', mustExist: true
.toThrow()
describe ".removePath(path)", ->
onDidChangePathsSpy = null
beforeEach ->
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener')
atom.project.onDidChangePaths(onDidChangePathsSpy)
it "removes the directory and repository for the path", ->
result = atom.project.removePath(atom.project.getPaths()[0])
expect(atom.project.getDirectories()).toEqual([])
expect(atom.project.getRepositories()).toEqual([])
expect(atom.project.getPaths()).toEqual([])
expect(result).toBe true
expect(onDidChangePathsSpy).toHaveBeenCalled()
it "does nothing if the path is not one of the project's root paths", ->
originalPaths = atom.project.getPaths()
result = atom.project.removePath(originalPaths[0] + "xyz")
expect(result).toBe false
expect(atom.project.getPaths()).toEqual(originalPaths)
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
it "doesn't destroy the repository if it is shared by another root directory", ->
atom.project.setPaths([__dirname, path.join(__dirname, "..", "src")])
atom.project.removePath(__dirname)
expect(atom.project.getPaths()).toEqual([path.join(__dirname, "..", "src")])
expect(atom.project.getRepositories()[0].isSubmodule("src")).toBe false
it "removes a path that is represented as a URI", ->
atom.packages.serviceHub.provide("atom.directory-provider", "0.1.0", {
directoryForURISync: (uri) ->
{
getPath: -> uri
getSubdirectory: -> {}
isRoot: -> true
existsSync: -> true
off: ->
}
})
ftpURI = "ftp://example.com/some/folder"
atom.project.setPaths([ftpURI])
expect(atom.project.getPaths()).toEqual [ftpURI]
atom.project.removePath(ftpURI)
expect(atom.project.getPaths()).toEqual []
describe ".onDidChangeFiles()", ->
sub = []
events = []
checkCallback = ->
beforeEach ->
sub = atom.project.onDidChangeFiles (incoming) ->
events.push incoming...
checkCallback()
afterEach ->
sub.dispose()
waitForEvents = (paths) ->
remaining = new Set(fs.realpathSync(p) for p in paths)
new Promise (resolve, reject) ->
checkCallback = ->
remaining.delete(event.path) for event in events
resolve() if remaining.size is 0
expire = ->
checkCallback = ->
console.error "Paths not seen:", Array.from(remaining)
reject(new Error('Expired before all expected events were delivered.'))
checkCallback()
setTimeout expire, 2000
it "reports filesystem changes within project paths", ->
dirOne = temp.mkdirSync('atom-spec-project-one')
fileOne = path.join(dirOne, 'file-one.txt')
fileTwo = path.join(dirOne, 'file-two.txt')
dirTwo = temp.mkdirSync('atom-spec-project-two')
fileThree = path.join(dirTwo, 'file-three.txt')
# Ensure that all preexisting watchers are stopped
waitsForPromise -> stopAllWatchers()
runs -> atom.project.setPaths([dirOne])
waitsForPromise -> atom.project.getWatcherPromise dirOne
runs ->
expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual undefined
fs.writeFileSync fileThree, "three\n"
fs.writeFileSync fileTwo, "two\n"
fs.writeFileSync fileOne, "one\n"
waitsForPromise -> waitForEvents [fileOne, fileTwo]
runs ->
expect(events.some (event) -> event.path is fileThree).toBeFalsy()
describe ".onDidAddBuffer()", ->
it "invokes the callback with added text buffers", ->
buffers = []
added = []
waitsForPromise ->
atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
.then (o) -> buffers.push(o)
runs ->
expect(buffers.length).toBe 1
atom.project.onDidAddBuffer (buffer) -> added.push(buffer)
waitsForPromise ->
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
.then (o) -> buffers.push(o)
runs ->
expect(buffers.length).toBe 2
expect(added).toEqual [buffers[1]]
describe ".observeBuffers()", ->
it "invokes the observer with current and future text buffers", ->
buffers = []
observed = []
waitsForPromise ->
atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
.then (o) -> buffers.push(o)
waitsForPromise ->
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
.then (o) -> buffers.push(o)
runs ->
expect(buffers.length).toBe 2
atom.project.observeBuffers (buffer) -> observed.push(buffer)
expect(observed).toEqual buffers
waitsForPromise ->
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
.then (o) -> buffers.push(o)
runs ->
expect(observed.length).toBe 3
expect(buffers.length).toBe 3
expect(observed).toEqual buffers
describe ".relativize(path)", ->
it "returns the path, relative to whichever root directory it is inside of", ->
atom.project.addPath(temp.mkdirSync("another-path"))
rootPath = atom.project.getPaths()[0]
childPath = path.join(rootPath, "some", "child", "directory")
expect(atom.project.relativize(childPath)).toBe path.join("some", "child", "directory")
rootPath = atom.project.getPaths()[1]
childPath = path.join(rootPath, "some", "child", "directory")
expect(atom.project.relativize(childPath)).toBe path.join("some", "child", "directory")
it "returns the given path if it is not in any of the root directories", ->
randomPath = path.join("some", "random", "path")
expect(atom.project.relativize(randomPath)).toBe randomPath
describe ".relativizePath(path)", ->
it "returns the root path that contains the given path, and the path relativized to that root path", ->
atom.project.addPath(temp.mkdirSync("another-path"))
rootPath = atom.project.getPaths()[0]
childPath = path.join(rootPath, "some", "child", "directory")
expect(atom.project.relativizePath(childPath)).toEqual [rootPath, path.join("some", "child", "directory")]
rootPath = atom.project.getPaths()[1]
childPath = path.join(rootPath, "some", "child", "directory")
expect(atom.project.relativizePath(childPath)).toEqual [rootPath, path.join("some", "child", "directory")]
describe "when the given path isn't inside of any of the project's path", ->
it "returns null for the root path, and the given path unchanged", ->
randomPath = path.join("some", "random", "path")
expect(atom.project.relativizePath(randomPath)).toEqual [null, randomPath]
describe "when the given path is a URL", ->
it "returns null for the root path, and the given path unchanged", ->
url = "http://the-path"
expect(atom.project.relativizePath(url)).toEqual [null, url]
describe "when the given path is inside more than one root folder", ->
it "uses the root folder that is closest to the given path", ->
atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir'))
inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt')
expect(atom.project.getDirectories()[0].contains(inputPath)).toBe true
expect(atom.project.getDirectories()[1].contains(inputPath)).toBe true
expect(atom.project.relativizePath(inputPath)).toEqual [
atom.project.getPaths()[1],
path.join('somewhere', 'something.txt')
]
describe ".contains(path)", ->
it "returns whether or not the given path is in one of the root directories", ->
rootPath = atom.project.getPaths()[0]
childPath = path.join(rootPath, "some", "child", "directory")
expect(atom.project.contains(childPath)).toBe true
randomPath = path.join("some", "random", "path")
expect(atom.project.contains(randomPath)).toBe false
describe ".resolvePath(uri)", ->
it "normalizes disk drive letter in passed path on #win32", ->
expect(atom.project.resolvePath("d:\\file.txt")).toEqual "D:\\file.txt"

955
spec/project-spec.js Normal file
View File

@@ -0,0 +1,955 @@
const temp = require('temp').track()
const TextBuffer = require('text-buffer')
const Project = require('../src/project')
const fs = require('fs-plus')
const path = require('path')
const {Directory} = require('pathwatcher')
const {stopAllWatchers} = require('../src/path-watcher')
const GitRepository = require('../src/git-repository')
describe('Project', () => {
beforeEach(() => {
const directory = atom.project.getDirectories()[0]
const paths = directory ? [directory.resolve('dir')] : [null]
atom.project.setPaths(paths)
// Wait for project's service consumers to be asynchronously added
waits(1)
})
describe('serialization', () => {
let deserializedProject = null
let notQuittingProject = null
let quittingProject = null
afterEach(() => {
if (deserializedProject != null) {
deserializedProject.destroy()
}
if (notQuittingProject != null) {
notQuittingProject.destroy()
}
if (quittingProject != null) {
quittingProject.destroy()
}
})
it("does not deserialize paths to directories that don't exist", () => {
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
const state = atom.project.serialize()
state.paths.push('/directory/that/does/not/exist')
let err = null
waitsForPromise(() =>
deserializedProject.deserialize(state, atom.deserializers)
.catch(e => { err = e })
)
runs(() => {
expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths())
expect(err.missingProjectPaths).toEqual(['/directory/that/does/not/exist'])
})
})
it('does not deserialize paths that are now files', () => {
const childPath = path.join(temp.mkdirSync('atom-spec-project'), 'child')
fs.mkdirSync(childPath)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
atom.project.setPaths([childPath])
const state = atom.project.serialize()
fs.rmdirSync(childPath)
fs.writeFileSync(childPath, 'surprise!\n')
let err = null
waitsForPromise(() =>
deserializedProject.deserialize(state, atom.deserializers)
.catch(e => { err = e })
)
runs(() => {
expect(deserializedProject.getPaths()).toEqual([])
expect(err.missingProjectPaths).toEqual([childPath])
})
})
it('does not include unretained buffers in the serialized state', () => {
waitsForPromise(() => atom.project.bufferForPath('a'))
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
})
it('listens for destroyed events on deserialized buffers and removes them when they are destroyed', () => {
waitsForPromise(() => atom.workspace.open('a'))
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
runs(() => {
expect(deserializedProject.getBuffers().length).toBe(1)
deserializedProject.getBuffers()[0].destroy()
expect(deserializedProject.getBuffers().length).toBe(0)
})
})
it('does not deserialize buffers when their path is now a directory', () => {
const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
waitsForPromise(() => atom.workspace.open(pathToOpen))
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
fs.mkdirSync(pathToOpen)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
})
it('does not deserialize buffers when their path is inaccessible', () => {
if (process.platform === 'win32') { return } // chmod not supported on win32
const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
fs.writeFileSync(pathToOpen, '')
waitsForPromise(() => atom.workspace.open(pathToOpen))
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
fs.chmodSync(pathToOpen, '000')
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
})
it('does not deserialize buffers with their path is no longer present', () => {
const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
fs.writeFileSync(pathToOpen, '')
waitsForPromise(() => atom.workspace.open(pathToOpen))
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
fs.unlinkSync(pathToOpen)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
})
it('deserializes buffers that have never been saved before', () => {
const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
waitsForPromise(() => atom.workspace.open(pathToOpen))
runs(() => {
atom.workspace.getActiveTextEditor().setText('unsaved\n')
expect(atom.project.getBuffers().length).toBe(1)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
runs(() => {
expect(deserializedProject.getBuffers().length).toBe(1)
expect(deserializedProject.getBuffers()[0].getPath()).toBe(pathToOpen)
expect(deserializedProject.getBuffers()[0].getText()).toBe('unsaved\n')
})
})
it('serializes marker layers and history only if Atom is quitting', () => {
waitsForPromise(() => atom.workspace.open('a'))
let bufferA = null
let layerA = null
let markerA = null
runs(() => {
bufferA = atom.project.getBuffers()[0]
layerA = bufferA.addMarkerLayer({persistent: true})
markerA = layerA.markPosition([0, 3])
bufferA.append('!')
notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
})
waitsForPromise(() => notQuittingProject.deserialize(atom.project.serialize({isUnloading: false})))
runs(() => {
expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).toBeUndefined()
expect(notQuittingProject.getBuffers()[0].undo()).toBe(false)
quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
})
waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true})))
runs(() => {
expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).not.toBeUndefined()
expect(quittingProject.getBuffers()[0].undo()).toBe(true)
})
})
})
describe('when an editor is saved and the project has no path', () =>
it("sets the project's path to the saved file's parent directory", () => {
const tempFile = temp.openSync().path
atom.project.setPaths([])
expect(atom.project.getPaths()[0]).toBeUndefined()
let editor = null
waitsForPromise(() => atom.workspace.open().then(o => { editor = o }))
waitsForPromise(() => editor.saveAs(tempFile))
runs(() => expect(atom.project.getPaths()[0]).toBe(path.dirname(tempFile)))
})
)
describe('before and after saving a buffer', () => {
let buffer
beforeEach(() =>
waitsForPromise(() =>
atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then((o) => {
buffer = o
buffer.retain()
})
)
)
afterEach(() => buffer.release())
it('emits save events on the main process', () => {
spyOn(atom.project.applicationDelegate, 'emitDidSavePath')
spyOn(atom.project.applicationDelegate, 'emitWillSavePath')
waitsForPromise(() => buffer.save())
runs(() => {
expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1)
expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath())
expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1)
expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath())
})
})
})
describe('when a watch error is thrown from the TextBuffer', () => {
let editor = null
beforeEach(() =>
waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => { editor = o }))
)
it('creates a warning notification', () => {
let noteSpy
atom.notifications.onDidAddNotification(noteSpy = jasmine.createSpy())
const error = new Error('SomeError')
error.eventType = 'resurrect'
editor.buffer.emitter.emit('will-throw-watch-error', {
handle: jasmine.createSpy(),
error
}
)
expect(noteSpy).toHaveBeenCalled()
const notification = noteSpy.mostRecentCall.args[0]
expect(notification.getType()).toBe('warning')
expect(notification.getDetail()).toBe('SomeError')
expect(notification.getMessage()).toContain('`resurrect`')
expect(notification.getMessage()).toContain(path.join('fixtures', 'dir', 'a'))
})
})
describe('when a custom repository-provider service is provided', () => {
let fakeRepositoryProvider, fakeRepository
beforeEach(() => {
fakeRepository = {destroy () { return null }}
fakeRepositoryProvider = {
repositoryForDirectory (directory) { return Promise.resolve(fakeRepository) },
repositoryForDirectorySync (directory) { return fakeRepository }
}
})
it('uses it to create repositories for any directories that need one', () => {
const projectPath = temp.mkdirSync('atom-project')
atom.project.setPaths([projectPath])
expect(atom.project.getRepositories()).toEqual([null])
atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider)
waitsFor(() => atom.project.repositoryProviders.length > 1)
runs(() => atom.project.getRepositories()[0] === fakeRepository)
})
it('does not create any new repositories if every directory has a repository', () => {
const repositories = atom.project.getRepositories()
expect(repositories.length).toEqual(1)
expect(repositories[0]).toBeTruthy()
atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider)
waitsFor(() => atom.project.repositoryProviders.length > 1)
runs(() => expect(atom.project.getRepositories()).toBe(repositories))
})
it('stops using it to create repositories when the service is removed', () => {
atom.project.setPaths([])
const disposable = atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider)
waitsFor(() => atom.project.repositoryProviders.length > 1)
runs(() => {
disposable.dispose()
atom.project.addPath(temp.mkdirSync('atom-project'))
expect(atom.project.getRepositories()).toEqual([null])
})
})
})
describe('when a custom directory-provider service is provided', () => {
class DummyDirectory {
constructor (aPath) {
this.path = aPath
}
getPath () { return this.path }
getFile () { return {existsSync () { return false }} }
getSubdirectory () { return {existsSync () { return false }} }
isRoot () { return true }
existsSync () { return this.path.endsWith('does-exist') }
contains (filePath) { return filePath.startsWith(this.path) }
onDidChangeFiles (callback) {
onDidChangeFilesCallback = callback
return {dispose: () => {}}
}
}
let serviceDisposable = null
let onDidChangeFilesCallback = null
beforeEach(() => {
serviceDisposable = atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', {
directoryForURISync (uri) {
if (uri.startsWith('ssh://')) {
return new DummyDirectory(uri)
} else {
return null
}
}
})
onDidChangeFilesCallback = null
waitsFor(() => atom.project.directoryProviders.length > 0)
})
it("uses the provider's custom directories for any paths that it handles", () => {
const localPath = temp.mkdirSync('local-path')
const remotePath = 'ssh://foreign-directory:8080/does-exist'
atom.project.setPaths([localPath, remotePath])
let directories = atom.project.getDirectories()
expect(directories[0].getPath()).toBe(localPath)
expect(directories[0] instanceof Directory).toBe(true)
expect(directories[1].getPath()).toBe(remotePath)
expect(directories[1] instanceof DummyDirectory).toBe(true)
// It does not add new remote paths that do not exist
const nonExistentRemotePath = 'ssh://another-directory:8080/does-not-exist'
atom.project.addPath(nonExistentRemotePath)
expect(atom.project.getDirectories().length).toBe(2)
// It adds new remote paths if their directories exist.
const newRemotePath = 'ssh://another-directory:8080/does-exist'
atom.project.addPath(newRemotePath)
directories = atom.project.getDirectories()
expect(directories[2].getPath()).toBe(newRemotePath)
expect(directories[2] instanceof DummyDirectory).toBe(true)
})
it('stops using the provider when the service is removed', () => {
serviceDisposable.dispose()
atom.project.setPaths(['ssh://foreign-directory:8080/does-exist'])
expect(atom.project.getDirectories().length).toBe(0)
})
it('uses the custom onDidChangeFiles as the watcher if available', () => {
// Ensure that all preexisting watchers are stopped
waitsForPromise(() => stopAllWatchers())
const remotePath = 'ssh://another-directory:8080/does-exist'
runs(() => atom.project.setPaths([remotePath]))
waitsForPromise(() => atom.project.getWatcherPromise(remotePath))
runs(() => {
expect(onDidChangeFilesCallback).not.toBeNull()
const changeSpy = jasmine.createSpy('atom.project.onDidChangeFiles')
const disposable = atom.project.onDidChangeFiles(changeSpy)
const events = [{action: 'created', path: remotePath + '/test.txt'}]
onDidChangeFilesCallback(events)
expect(changeSpy).toHaveBeenCalledWith(events)
disposable.dispose()
})
})
})
describe('.open(path)', () => {
let absolutePath, newBufferHandler
beforeEach(() => {
absolutePath = require.resolve('./fixtures/dir/a')
newBufferHandler = jasmine.createSpy('newBufferHandler')
atom.project.onDidAddBuffer(newBufferHandler)
})
describe("when given an absolute path that isn't currently open", () =>
it("returns a new edit session for the given path and emits 'buffer-created'", () => {
let editor = null
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
runs(() => {
expect(editor.buffer.getPath()).toBe(absolutePath)
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
})
})
)
describe("when given a relative path that isn't currently opened", () =>
it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", () => {
let editor = null
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
runs(() => {
expect(editor.buffer.getPath()).toBe(absolutePath)
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
})
})
)
describe('when passed the path to a buffer that is currently opened', () =>
it('returns a new edit session containing currently opened buffer', () => {
let editor = null
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
runs(() => newBufferHandler.reset())
waitsForPromise(() =>
atom.workspace.open(absolutePath).then(({buffer}) => expect(buffer).toBe(editor.buffer))
)
waitsForPromise(() =>
atom.workspace.open('a').then(({buffer}) => {
expect(buffer).toBe(editor.buffer)
expect(newBufferHandler).not.toHaveBeenCalled()
})
)
})
)
describe('when not passed a path', () =>
it("returns a new edit session and emits 'buffer-created'", () => {
let editor = null
waitsForPromise(() => atom.workspace.open().then(o => { editor = o }))
runs(() => {
expect(editor.buffer.getPath()).toBeUndefined()
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
})
})
)
})
describe('.bufferForPath(path)', () => {
let buffer = null
beforeEach(() =>
waitsForPromise(() =>
atom.project.bufferForPath('a').then((o) => {
buffer = o
buffer.retain()
})
)
)
afterEach(() => buffer.release())
describe('when opening a previously opened path', () => {
it('does not create a new buffer', () => {
waitsForPromise(() =>
atom.project.bufferForPath('a').then(anotherBuffer => expect(anotherBuffer).toBe(buffer))
)
waitsForPromise(() =>
atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer))
)
waitsForPromise(() =>
Promise.all([
atom.project.bufferForPath('c'),
atom.project.bufferForPath('c')
]).then(([buffer1, buffer2]) => {
expect(buffer1).toBe(buffer2)
})
)
})
it('retries loading the buffer if it previously failed', () => {
waitsForPromise({shouldReject: true}, () => {
spyOn(TextBuffer, 'load').andCallFake(() => Promise.reject(new Error('Could not open file')))
return atom.project.bufferForPath('b')
})
waitsForPromise({shouldReject: false}, () => {
TextBuffer.load.andCallThrough()
return atom.project.bufferForPath('b')
})
})
it('creates a new buffer if the previous buffer was destroyed', () => {
buffer.release()
waitsForPromise(() =>
atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer))
)
})
})
})
describe('.repositoryForDirectory(directory)', () => {
it('resolves to null when the directory does not have a repository', () =>
waitsForPromise(() => {
const directory = new Directory('/tmp')
return atom.project.repositoryForDirectory(directory).then((result) => {
expect(result).toBeNull()
expect(atom.project.repositoryProviders.length).toBeGreaterThan(0)
expect(atom.project.repositoryPromisesByPath.size).toBe(0)
})
})
)
it('resolves to a GitRepository and is cached when the given directory is a Git repo', () =>
waitsForPromise(() => {
const directory = new Directory(path.join(__dirname, '..'))
const promise = atom.project.repositoryForDirectory(directory)
return promise.then((result) => {
expect(result).toBeInstanceOf(GitRepository)
const dirPath = directory.getRealPathSync()
expect(result.getPath()).toBe(path.join(dirPath, '.git'))
// Verify that the result is cached.
expect(atom.project.repositoryForDirectory(directory)).toBe(promise)
})
})
)
it('creates a new repository if a previous one with the same directory had been destroyed', () => {
let repository = null
const directory = new Directory(path.join(__dirname, '..'))
waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => { repository = repo }))
runs(() => {
expect(repository.isDestroyed()).toBe(false)
repository.destroy()
expect(repository.isDestroyed()).toBe(true)
})
waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => { repository = repo }))
runs(() => expect(repository.isDestroyed()).toBe(false))
})
})
describe('.setPaths(paths, options)', () => {
describe('when path is a file', () =>
it("sets its path to the file's parent directory and updates the root directory", () => {
const filePath = require.resolve('./fixtures/dir/a')
atom.project.setPaths([filePath])
expect(atom.project.getPaths()[0]).toEqual(path.dirname(filePath))
expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(filePath))
})
)
describe('when path is a directory', () => {
it('assigns the directories and repositories', () => {
const directory1 = temp.mkdirSync('non-git-repo')
const directory2 = temp.mkdirSync('git-repo1')
const directory3 = temp.mkdirSync('git-repo2')
const gitDirPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git'))
fs.copySync(gitDirPath, path.join(directory2, '.git'))
fs.copySync(gitDirPath, path.join(directory3, '.git'))
atom.project.setPaths([directory1, directory2, directory3])
const [repo1, repo2, repo3] = atom.project.getRepositories()
expect(repo1).toBeNull()
expect(repo2.getShortHead()).toBe('master')
expect(repo2.getPath()).toBe(fs.realpathSync(path.join(directory2, '.git')))
expect(repo3.getShortHead()).toBe('master')
expect(repo3.getPath()).toBe(fs.realpathSync(path.join(directory3, '.git')))
})
it('calls callbacks registered with ::onDidChangePaths', () => {
const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
atom.project.onDidChangePaths(onDidChangePathsSpy)
const paths = [ temp.mkdirSync('dir1'), temp.mkdirSync('dir2') ]
atom.project.setPaths(paths)
expect(onDidChangePathsSpy.callCount).toBe(1)
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths)
})
it('optionally throws an error with any paths that did not exist', () => {
const paths = [temp.mkdirSync('exists0'), '/doesnt-exists/0', temp.mkdirSync('exists1'), '/doesnt-exists/1']
try {
atom.project.setPaths(paths, {mustExist: true})
expect('no exception thrown').toBeUndefined()
} catch (e) {
expect(e.missingProjectPaths).toEqual([paths[1], paths[3]])
}
expect(atom.project.getPaths()).toEqual([paths[0], paths[2]])
})
})
describe('when no paths are given', () =>
it('clears its path', () => {
atom.project.setPaths([])
expect(atom.project.getPaths()).toEqual([])
expect(atom.project.getDirectories()).toEqual([])
})
)
it('normalizes the path to remove consecutive slashes, ., and .. segments', () => {
atom.project.setPaths([`${require.resolve('./fixtures/dir/a')}${path.sep}b${path.sep}${path.sep}..`])
expect(atom.project.getPaths()[0]).toEqual(path.dirname(require.resolve('./fixtures/dir/a')))
expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(require.resolve('./fixtures/dir/a')))
})
})
describe('.addPath(path, options)', () => {
it('calls callbacks registered with ::onDidChangePaths', () => {
const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
atom.project.onDidChangePaths(onDidChangePathsSpy)
const [oldPath] = atom.project.getPaths()
const newPath = temp.mkdirSync('dir')
atom.project.addPath(newPath)
expect(onDidChangePathsSpy.callCount).toBe(1)
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath])
})
it("doesn't add redundant paths", () => {
const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
atom.project.onDidChangePaths(onDidChangePathsSpy)
const [oldPath] = atom.project.getPaths()
// Doesn't re-add an existing root directory
atom.project.addPath(oldPath)
expect(atom.project.getPaths()).toEqual([oldPath])
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
// Doesn't add an entry for a file-path within an existing root directory
atom.project.addPath(path.join(oldPath, 'some-file.txt'))
expect(atom.project.getPaths()).toEqual([oldPath])
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
// Does add an entry for a directory within an existing directory
const newPath = path.join(oldPath, 'a-dir')
atom.project.addPath(newPath)
expect(atom.project.getPaths()).toEqual([oldPath, newPath])
expect(onDidChangePathsSpy).toHaveBeenCalled()
})
it("doesn't add non-existent directories", () => {
const previousPaths = atom.project.getPaths()
atom.project.addPath('/this-definitely/does-not-exist')
expect(atom.project.getPaths()).toEqual(previousPaths)
})
it('optionally throws on non-existent directories', () =>
expect(() => atom.project.addPath('/this-definitely/does-not-exist', {mustExist: true})).toThrow()
)
})
describe('.removePath(path)', () => {
let onDidChangePathsSpy = null
beforeEach(() => {
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener')
atom.project.onDidChangePaths(onDidChangePathsSpy)
})
it('removes the directory and repository for the path', () => {
const result = atom.project.removePath(atom.project.getPaths()[0])
expect(atom.project.getDirectories()).toEqual([])
expect(atom.project.getRepositories()).toEqual([])
expect(atom.project.getPaths()).toEqual([])
expect(result).toBe(true)
expect(onDidChangePathsSpy).toHaveBeenCalled()
})
it("does nothing if the path is not one of the project's root paths", () => {
const originalPaths = atom.project.getPaths()
const result = atom.project.removePath(originalPaths[0] + 'xyz')
expect(result).toBe(false)
expect(atom.project.getPaths()).toEqual(originalPaths)
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
})
it("doesn't destroy the repository if it is shared by another root directory", () => {
atom.project.setPaths([__dirname, path.join(__dirname, '..', 'src')])
atom.project.removePath(__dirname)
expect(atom.project.getPaths()).toEqual([path.join(__dirname, '..', 'src')])
expect(atom.project.getRepositories()[0].isSubmodule('src')).toBe(false)
})
it('removes a path that is represented as a URI', () => {
atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', {
directoryForURISync (uri) {
return {
getPath () { return uri },
getSubdirectory () { return {} },
isRoot () { return true },
existsSync () { return true },
off () {}
}
}
})
const ftpURI = 'ftp://example.com/some/folder'
atom.project.setPaths([ftpURI])
expect(atom.project.getPaths()).toEqual([ftpURI])
atom.project.removePath(ftpURI)
expect(atom.project.getPaths()).toEqual([])
})
})
describe('.onDidChangeFiles()', () => {
let sub = []
const events = []
let checkCallback = () => {}
beforeEach(() => {
sub = atom.project.onDidChangeFiles((incoming) => {
events.push(...incoming)
checkCallback()
})
})
afterEach(() => sub.dispose())
const waitForEvents = (paths) => {
const remaining = new Set(paths.map((p) => fs.realpathSync(p)))
return new Promise((resolve, reject) => {
checkCallback = () => {
for (let event of events) { remaining.delete(event.path) }
if (remaining.size === 0) { resolve() }
}
const expire = () => {
checkCallback = () => {}
console.error('Paths not seen:', remaining)
reject(new Error('Expired before all expected events were delivered.'))
}
checkCallback()
setTimeout(expire, 2000)
})
}
it('reports filesystem changes within project paths', () => {
const dirOne = temp.mkdirSync('atom-spec-project-one')
const fileOne = path.join(dirOne, 'file-one.txt')
const fileTwo = path.join(dirOne, 'file-two.txt')
const dirTwo = temp.mkdirSync('atom-spec-project-two')
const fileThree = path.join(dirTwo, 'file-three.txt')
// Ensure that all preexisting watchers are stopped
waitsForPromise(() => stopAllWatchers())
runs(() => atom.project.setPaths([dirOne]))
waitsForPromise(() => atom.project.getWatcherPromise(dirOne))
runs(() => {
expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual(undefined)
fs.writeFileSync(fileThree, 'three\n')
fs.writeFileSync(fileTwo, 'two\n')
fs.writeFileSync(fileOne, 'one\n')
})
waitsForPromise(() => waitForEvents([fileOne, fileTwo]))
runs(() => expect(events.some(event => event.path === fileThree)).toBeFalsy())
})
})
describe('.onDidAddBuffer()', () =>
it('invokes the callback with added text buffers', () => {
const buffers = []
const added = []
waitsForPromise(() =>
atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
.then(o => buffers.push(o))
)
runs(() => {
expect(buffers.length).toBe(1)
atom.project.onDidAddBuffer(buffer => added.push(buffer))
})
waitsForPromise(() =>
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
.then(o => buffers.push(o))
)
runs(() => {
expect(buffers.length).toBe(2)
expect(added).toEqual([buffers[1]])
})
})
)
describe('.observeBuffers()', () =>
it('invokes the observer with current and future text buffers', () => {
const buffers = []
const observed = []
waitsForPromise(() =>
atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
.then(o => buffers.push(o))
)
waitsForPromise(() =>
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
.then(o => buffers.push(o))
)
runs(() => {
expect(buffers.length).toBe(2)
atom.project.observeBuffers(buffer => observed.push(buffer))
expect(observed).toEqual(buffers)
})
waitsForPromise(() =>
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
.then(o => buffers.push(o))
)
runs(() => {
expect(observed.length).toBe(3)
expect(buffers.length).toBe(3)
expect(observed).toEqual(buffers)
})
})
)
describe('.relativize(path)', () => {
it('returns the path, relative to whichever root directory it is inside of', () => {
atom.project.addPath(temp.mkdirSync('another-path'))
let rootPath = atom.project.getPaths()[0]
let childPath = path.join(rootPath, 'some', 'child', 'directory')
expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory'))
rootPath = atom.project.getPaths()[1]
childPath = path.join(rootPath, 'some', 'child', 'directory')
expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory'))
})
it('returns the given path if it is not in any of the root directories', () => {
const randomPath = path.join('some', 'random', 'path')
expect(atom.project.relativize(randomPath)).toBe(randomPath)
})
})
describe('.relativizePath(path)', () => {
it('returns the root path that contains the given path, and the path relativized to that root path', () => {
atom.project.addPath(temp.mkdirSync('another-path'))
let rootPath = atom.project.getPaths()[0]
let childPath = path.join(rootPath, 'some', 'child', 'directory')
expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')])
rootPath = atom.project.getPaths()[1]
childPath = path.join(rootPath, 'some', 'child', 'directory')
expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')])
})
describe("when the given path isn't inside of any of the project's path", () =>
it('returns null for the root path, and the given path unchanged', () => {
const randomPath = path.join('some', 'random', 'path')
expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath])
})
)
describe('when the given path is a URL', () =>
it('returns null for the root path, and the given path unchanged', () => {
const url = 'http://the-path'
expect(atom.project.relativizePath(url)).toEqual([null, url])
})
)
describe('when the given path is inside more than one root folder', () =>
it('uses the root folder that is closest to the given path', () => {
atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir'))
const inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt')
expect(atom.project.getDirectories()[0].contains(inputPath)).toBe(true)
expect(atom.project.getDirectories()[1].contains(inputPath)).toBe(true)
expect(atom.project.relativizePath(inputPath)).toEqual([
atom.project.getPaths()[1],
path.join('somewhere', 'something.txt')
])
})
)
})
describe('.contains(path)', () =>
it('returns whether or not the given path is in one of the root directories', () => {
const rootPath = atom.project.getPaths()[0]
const childPath = path.join(rootPath, 'some', 'child', 'directory')
expect(atom.project.contains(childPath)).toBe(true)
const randomPath = path.join('some', 'random', 'path')
expect(atom.project.contains(randomPath)).toBe(false)
})
)
describe('.resolvePath(uri)', () =>
it('normalizes disk drive letter in passed path on #win32', () => {
expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')
})
)
})

View File

@@ -1,123 +0,0 @@
TextEditor = require '../src/text-editor'
describe "Selection", ->
[buffer, editor, selection] = []
beforeEach ->
buffer = atom.project.bufferForPathSync('sample.js')
editor = new TextEditor({buffer: buffer, tabLength: 2})
selection = editor.getLastSelection()
afterEach ->
buffer.destroy()
describe ".deleteSelectedText()", ->
describe "when nothing is selected", ->
it "deletes nothing", ->
selection.setBufferRange [[0, 3], [0, 3]]
selection.deleteSelectedText()
expect(buffer.lineForRow(0)).toBe "var quicksort = function () {"
describe "when one line is selected", ->
it "deletes selected text and clears the selection", ->
selection.setBufferRange [[0, 4], [0, 14]]
selection.deleteSelectedText()
expect(buffer.lineForRow(0)).toBe "var = function () {"
endOfLine = buffer.lineForRow(0).length
selection.setBufferRange [[0, 0], [0, endOfLine]]
selection.deleteSelectedText()
expect(buffer.lineForRow(0)).toBe ""
expect(selection.isEmpty()).toBeTruthy()
describe "when multiple lines are selected", ->
it "deletes selected text and clears the selection", ->
selection.setBufferRange [[0, 1], [2, 39]]
selection.deleteSelectedText()
expect(buffer.lineForRow(0)).toBe "v;"
expect(selection.isEmpty()).toBeTruthy()
describe "when the cursor precedes the tail", ->
it "deletes selected text and clears the selection", ->
selection.cursor.setScreenPosition [0, 13]
selection.selectToScreenPosition [0, 4]
selection.delete()
expect(buffer.lineForRow(0)).toBe "var = function () {"
expect(selection.isEmpty()).toBeTruthy()
describe ".isReversed()", ->
it "returns true if the cursor precedes the tail", ->
selection.cursor.setScreenPosition([0, 20])
selection.selectToScreenPosition([0, 10])
expect(selection.isReversed()).toBeTruthy()
selection.selectToScreenPosition([0, 25])
expect(selection.isReversed()).toBeFalsy()
describe ".selectLine(row)", ->
describe "when passed a row", ->
it "selects the specified row", ->
selection.setBufferRange([[2, 4], [3, 4]])
selection.selectLine(5)
expect(selection.getBufferRange()).toEqual [[5, 0], [6, 0]]
describe "when not passed a row", ->
it "selects all rows spanned by the selection", ->
selection.setBufferRange([[2, 4], [3, 4]])
selection.selectLine()
expect(selection.getBufferRange()).toEqual [[2, 0], [4, 0]]
describe "when only the selection's tail is moved (regression)", ->
it "notifies ::onDidChangeRange observers", ->
selection.setBufferRange([[2, 0], [2, 10]], reversed: true)
changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler')
selection.onDidChangeRange changeScreenRangeHandler
buffer.insert([2, 5], 'abc')
expect(changeScreenRangeHandler).toHaveBeenCalled()
describe "when the selection is destroyed", ->
it "destroys its marker", ->
selection.setBufferRange([[2, 0], [2, 10]])
marker = selection.marker
selection.destroy()
expect(marker.isDestroyed()).toBeTruthy()
describe ".insertText(text, options)", ->
it "allows pasting white space only lines when autoIndent is enabled", ->
selection.setBufferRange [[0, 0], [0, 0]]
selection.insertText(" \n \n\n", autoIndent: true)
expect(buffer.lineForRow(0)).toBe " "
expect(buffer.lineForRow(1)).toBe " "
expect(buffer.lineForRow(2)).toBe ""
it "auto-indents if only a newline is inserted", ->
selection.setBufferRange [[2, 0], [3, 0]]
selection.insertText("\n", autoIndent: true)
expect(buffer.lineForRow(2)).toBe " "
it "auto-indents if only a carriage return + newline is inserted", ->
selection.setBufferRange [[2, 0], [3, 0]]
selection.insertText("\r\n", autoIndent: true)
expect(buffer.lineForRow(2)).toBe " "
describe ".fold()", ->
it "folds the buffer range spanned by the selection", ->
selection.setBufferRange([[0, 3], [1, 6]])
selection.fold()
expect(selection.getScreenRange()).toEqual([[0, 4], [0, 4]])
expect(selection.getBufferRange()).toEqual([[1, 6], [1, 6]])
expect(editor.lineTextForScreenRow(0)).toBe "var#{editor.displayLayer.foldCharacter}sort = function(items) {"
expect(editor.isFoldedAtBufferRow(0)).toBe(true)
it "doesn't create a fold when the selection is empty", ->
selection.setBufferRange([[0, 3], [0, 3]])
selection.fold()
expect(selection.getScreenRange()).toEqual([[0, 3], [0, 3]])
expect(selection.getBufferRange()).toEqual([[0, 3], [0, 3]])
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
expect(editor.isFoldedAtBufferRow(0)).toBe(false)

157
spec/selection-spec.js Normal file
View File

@@ -0,0 +1,157 @@
const TextEditor = require('../src/text-editor')
describe('Selection', () => {
let buffer, editor, selection
beforeEach(() => {
buffer = atom.project.bufferForPathSync('sample.js')
editor = new TextEditor({buffer, tabLength: 2})
selection = editor.getLastSelection()
})
afterEach(() => buffer.destroy())
describe('.deleteSelectedText()', () => {
describe('when nothing is selected', () => {
it('deletes nothing', () => {
selection.setBufferRange([[0, 3], [0, 3]])
selection.deleteSelectedText()
expect(buffer.lineForRow(0)).toBe('var quicksort = function () {')
})
})
describe('when one line is selected', () => {
it('deletes selected text and clears the selection', () => {
selection.setBufferRange([[0, 4], [0, 14]])
selection.deleteSelectedText()
expect(buffer.lineForRow(0)).toBe('var = function () {')
const endOfLine = buffer.lineForRow(0).length
selection.setBufferRange([[0, 0], [0, endOfLine]])
selection.deleteSelectedText()
expect(buffer.lineForRow(0)).toBe('')
expect(selection.isEmpty()).toBeTruthy()
})
})
describe('when multiple lines are selected', () => {
it('deletes selected text and clears the selection', () => {
selection.setBufferRange([[0, 1], [2, 39]])
selection.deleteSelectedText()
expect(buffer.lineForRow(0)).toBe('v;')
expect(selection.isEmpty()).toBeTruthy()
})
})
describe('when the cursor precedes the tail', () => {
it('deletes selected text and clears the selection', () => {
selection.cursor.setScreenPosition([0, 13])
selection.selectToScreenPosition([0, 4])
selection.delete()
expect(buffer.lineForRow(0)).toBe('var = function () {')
expect(selection.isEmpty()).toBeTruthy()
})
})
})
describe('.isReversed()', () => {
it('returns true if the cursor precedes the tail', () => {
selection.cursor.setScreenPosition([0, 20])
selection.selectToScreenPosition([0, 10])
expect(selection.isReversed()).toBeTruthy()
selection.selectToScreenPosition([0, 25])
expect(selection.isReversed()).toBeFalsy()
})
})
describe('.selectLine(row)', () => {
describe('when passed a row', () => {
it('selects the specified row', () => {
selection.setBufferRange([[2, 4], [3, 4]])
selection.selectLine(5)
expect(selection.getBufferRange()).toEqual([[5, 0], [6, 0]])
})
})
describe('when not passed a row', () => {
it('selects all rows spanned by the selection', () => {
selection.setBufferRange([[2, 4], [3, 4]])
selection.selectLine()
expect(selection.getBufferRange()).toEqual([[2, 0], [4, 0]])
})
})
})
describe("when only the selection's tail is moved (regression)", () => {
it('notifies ::onDidChangeRange observers', () => {
selection.setBufferRange([[2, 0], [2, 10]], {reversed: true})
const changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler')
selection.onDidChangeRange(changeScreenRangeHandler)
buffer.insert([2, 5], 'abc')
expect(changeScreenRangeHandler).toHaveBeenCalled()
})
})
describe('when the selection is destroyed', () => {
it('destroys its marker', () => {
selection.setBufferRange([[2, 0], [2, 10]])
const { marker } = selection
selection.destroy()
expect(marker.isDestroyed()).toBeTruthy()
})
})
describe('.insertText(text, options)', () => {
it('allows pasting white space only lines when autoIndent is enabled', () => {
selection.setBufferRange([[0, 0], [0, 0]])
selection.insertText(' \n \n\n', {autoIndent: true})
expect(buffer.lineForRow(0)).toBe(' ')
expect(buffer.lineForRow(1)).toBe(' ')
expect(buffer.lineForRow(2)).toBe('')
})
it('auto-indents if only a newline is inserted', () => {
selection.setBufferRange([[2, 0], [3, 0]])
selection.insertText('\n', {autoIndent: true})
expect(buffer.lineForRow(2)).toBe(' ')
})
it('auto-indents if only a carriage return + newline is inserted', () => {
selection.setBufferRange([[2, 0], [3, 0]])
selection.insertText('\r\n', {autoIndent: true})
expect(buffer.lineForRow(2)).toBe(' ')
})
it('does not adjust the indent of trailing lines if preserveTrailingLineIndentation is true', () => {
selection.setBufferRange([[5, 0], [5, 0]])
selection.insertText(' foo\n bar\n', {preserveTrailingLineIndentation: true, indentBasis: 1})
expect(buffer.lineForRow(6)).toBe(' bar')
})
})
describe('.fold()', () => {
it('folds the buffer range spanned by the selection', () => {
selection.setBufferRange([[0, 3], [1, 6]])
selection.fold()
expect(selection.getScreenRange()).toEqual([[0, 4], [0, 4]])
expect(selection.getBufferRange()).toEqual([[1, 6], [1, 6]])
expect(editor.lineTextForScreenRow(0)).toBe(`var${editor.displayLayer.foldCharacter}sort = function(items) {`)
expect(editor.isFoldedAtBufferRow(0)).toBe(true)
})
it("doesn't create a fold when the selection is empty", () => {
selection.setBufferRange([[0, 3], [0, 3]])
selection.fold()
expect(selection.getScreenRange()).toEqual([[0, 3], [0, 3]])
expect(selection.getBufferRange()).toEqual([[0, 3], [0, 3]])
expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {')
expect(editor.isFoldedAtBufferRow(0)).toBe(false)
})
})
})

View File

@@ -58,7 +58,7 @@ if specPackagePath = FindParentDir.sync(testPaths[0], 'package.json')
if specDirectory = FindParentDir.sync(testPaths[0], 'fixtures')
specProjectPath = path.join(specDirectory, 'fixtures')
else
specProjectPath = path.join(__dirname, 'fixtures')
specProjectPath = require('os').tmpdir()
beforeEach ->
atom.project.setPaths([specProjectPath])

View File

@@ -1930,6 +1930,8 @@ describe('TextEditorComponent', () => {
const decoration = editor.decorateMarker(marker, {type: 'overlay', item: overlayElement, class: 'a'})
await component.getNextUpdatePromise()
const overlayComponent = component.overlayComponents.values().next().value
const overlayWrapper = overlayElement.parentElement
expect(overlayWrapper.classList.contains('a')).toBe(true)
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
@@ -1960,12 +1962,12 @@ describe('TextEditorComponent', () => {
await setScrollTop(component, 20)
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
overlayElement.style.height = 60 + 'px'
await component.getNextUpdatePromise()
await overlayComponent.getNextUpdatePromise()
expect(overlayWrapper.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 4))
// Does not flip the overlay vertically if it would overflow the top of the window
overlayElement.style.height = 80 + 'px'
await component.getNextUpdatePromise()
await overlayComponent.getNextUpdatePromise()
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
// Can update overlay wrapper class
@@ -2575,6 +2577,24 @@ describe('TextEditorComponent', () => {
])
})
it('does not throw exceptions when destroying a block decoration inside a marker change event (regression)', async () => {
const {editor, component} = buildComponent({rowsPerTile: 3})
const marker = editor.markScreenPosition([2, 0])
marker.onDidChange(() => { marker.destroy() })
const item = document.createElement('div')
editor.decorateMarker(marker, {type: 'block', item})
await component.getNextUpdatePromise()
expect(item.nextSibling).toBe(lineNodeForScreenRow(component, 2))
marker.setBufferRange([[0, 0], [0, 0]])
expect(marker.isDestroyed()).toBe(true)
await component.getNextUpdatePromise()
expect(item.parentElement).toBeNull()
})
it('does not attempt to render block decorations located outside the visible range', async () => {
const {editor, component} = buildComponent({autoHeight: false, rowsPerTile: 2})
await setEditorHeightInLines(component, 2)
@@ -4438,24 +4458,44 @@ describe('TextEditorComponent', () => {
expect(dragEvents).toEqual([])
})
it('calls `didStopDragging` if the buffer changes while dragging', async () => {
it('calls `didStopDragging` if the user interacts with the keyboard while dragging', async () => {
const {component, editor} = buildComponent()
let dragging = false
component.handleMouseDragUntilMouseUp({
didDrag: (event) => { dragging = true },
didStopDragging: () => { dragging = false }
})
function startDragging () {
component.handleMouseDragUntilMouseUp({
didDrag: (event) => { dragging = true },
didStopDragging: () => { dragging = false }
})
}
startDragging()
window.dispatchEvent(new MouseEvent('mousemove'))
await getNextAnimationFramePromise()
expect(dragging).toBe(true)
editor.delete()
// Buffer changes don't cause dragging to be stopped.
editor.insertText('X')
expect(dragging).toBe(true)
// Keyboard interaction prevents users from dragging further.
component.didKeydown({code: 'KeyX'})
expect(dragging).toBe(false)
window.dispatchEvent(new MouseEvent('mousemove'))
await getNextAnimationFramePromise()
expect(dragging).toBe(false)
// Pressing a modifier key does not terminate dragging, (to ensure we can add new selections with the mouse)
startDragging()
window.dispatchEvent(new MouseEvent('mousemove'))
await getNextAnimationFramePromise()
expect(dragging).toBe(true)
component.didKeydown({key: 'Control'})
component.didKeydown({key: 'Alt'})
component.didKeydown({key: 'Shift'})
component.didKeydown({key: 'Meta'})
expect(dragging).toBe(true)
})
function getNextAnimationFramePromise () {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,97 +0,0 @@
textUtils = require '../src/text-utils'
describe 'text utilities', ->
describe '.hasPairedCharacter(string)', ->
it 'returns true when the string contains a surrogate pair, variation sequence, or combined character', ->
expect(textUtils.hasPairedCharacter('abc')).toBe false
expect(textUtils.hasPairedCharacter('a\uD835\uDF97b\uD835\uDF97c')).toBe true
expect(textUtils.hasPairedCharacter('\uD835\uDF97')).toBe true
expect(textUtils.hasPairedCharacter('\u2714\uFE0E')).toBe true
expect(textUtils.hasPairedCharacter('e\u0301')).toBe true
expect(textUtils.hasPairedCharacter('\uD835')).toBe false
expect(textUtils.hasPairedCharacter('\uDF97')).toBe false
expect(textUtils.hasPairedCharacter('\uFE0E')).toBe false
expect(textUtils.hasPairedCharacter('\u0301')).toBe false
expect(textUtils.hasPairedCharacter('\uFE0E\uFE0E')).toBe false
expect(textUtils.hasPairedCharacter('\u0301\u0301')).toBe false
describe '.isPairedCharacter(string, index)', ->
it 'returns true when the index is the start of a high/low surrogate pair, variation sequence, or combined character', ->
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 0)).toBe false
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 1)).toBe true
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 2)).toBe false
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 3)).toBe false
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 4)).toBe true
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 5)).toBe false
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 6)).toBe false
expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 0)).toBe false
expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 1)).toBe true
expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 2)).toBe false
expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 3)).toBe false
expect(textUtils.isPairedCharacter('\uD835')).toBe false
expect(textUtils.isPairedCharacter('\uDF97')).toBe false
expect(textUtils.isPairedCharacter('\uFE0E')).toBe false
expect(textUtils.isPairedCharacter('\uFE0E')).toBe false
expect(textUtils.isPairedCharacter('\uFE0E\uFE0E')).toBe false
expect(textUtils.isPairedCharacter('ae\u0301c', 0)).toBe false
expect(textUtils.isPairedCharacter('ae\u0301c', 1)).toBe true
expect(textUtils.isPairedCharacter('ae\u0301c', 2)).toBe false
expect(textUtils.isPairedCharacter('ae\u0301c', 3)).toBe false
expect(textUtils.isPairedCharacter('ae\u0301c', 4)).toBe false
describe ".isDoubleWidthCharacter(character)", ->
it "returns true when the character is either japanese, chinese or a full width form", ->
expect(textUtils.isDoubleWidthCharacter("")).toBe(true)
expect(textUtils.isDoubleWidthCharacter("")).toBe(true)
expect(textUtils.isDoubleWidthCharacter("")).toBe(true)
expect(textUtils.isDoubleWidthCharacter("")).toBe(true)
expect(textUtils.isDoubleWidthCharacter("")).toBe(true)
expect(textUtils.isDoubleWidthCharacter("a")).toBe(false)
describe ".isHalfWidthCharacter(character)", ->
it "returns true when the character is an half width form", ->
expect(textUtils.isHalfWidthCharacter("")).toBe(true)
expect(textUtils.isHalfWidthCharacter("")).toBe(true)
expect(textUtils.isHalfWidthCharacter("")).toBe(true)
expect(textUtils.isHalfWidthCharacter("")).toBe(true)
expect(textUtils.isHalfWidthCharacter("B")).toBe(false)
describe ".isKoreanCharacter(character)", ->
it "returns true when the character is a korean character", ->
expect(textUtils.isKoreanCharacter("")).toBe(true)
expect(textUtils.isKoreanCharacter("")).toBe(true)
expect(textUtils.isKoreanCharacter("")).toBe(true)
expect(textUtils.isKoreanCharacter("")).toBe(true)
expect(textUtils.isKoreanCharacter("O")).toBe(false)
describe ".isWrapBoundary(previousCharacter, character)", ->
it "returns true when the character is CJK or when the previous character is a space/tab", ->
anyCharacter = 'x'
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(' ', 'h')).toBe(true)
expect(textUtils.isWrapBoundary('\t', 'h')).toBe(true)
expect(textUtils.isWrapBoundary('a', 'h')).toBe(false)

110
spec/text-utils-spec.js Normal file
View File

@@ -0,0 +1,110 @@
const textUtils = require('../src/text-utils')
describe('text utilities', () => {
describe('.hasPairedCharacter(string)', () =>
it('returns true when the string contains a surrogate pair, variation sequence, or combined character', () => {
expect(textUtils.hasPairedCharacter('abc')).toBe(false)
expect(textUtils.hasPairedCharacter('a\uD835\uDF97b\uD835\uDF97c')).toBe(true)
expect(textUtils.hasPairedCharacter('\uD835\uDF97')).toBe(true)
expect(textUtils.hasPairedCharacter('\u2714\uFE0E')).toBe(true)
expect(textUtils.hasPairedCharacter('e\u0301')).toBe(true)
expect(textUtils.hasPairedCharacter('\uD835')).toBe(false)
expect(textUtils.hasPairedCharacter('\uDF97')).toBe(false)
expect(textUtils.hasPairedCharacter('\uFE0E')).toBe(false)
expect(textUtils.hasPairedCharacter('\u0301')).toBe(false)
expect(textUtils.hasPairedCharacter('\uFE0E\uFE0E')).toBe(false)
expect(textUtils.hasPairedCharacter('\u0301\u0301')).toBe(false)
})
)
describe('.isPairedCharacter(string, index)', () =>
it('returns true when the index is the start of a high/low surrogate pair, variation sequence, or combined character', () => {
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 0)).toBe(false)
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 1)).toBe(true)
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 2)).toBe(false)
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 3)).toBe(false)
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 4)).toBe(true)
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 5)).toBe(false)
expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 6)).toBe(false)
expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 0)).toBe(false)
expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 1)).toBe(true)
expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 2)).toBe(false)
expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 3)).toBe(false)
expect(textUtils.isPairedCharacter('\uD835')).toBe(false)
expect(textUtils.isPairedCharacter('\uDF97')).toBe(false)
expect(textUtils.isPairedCharacter('\uFE0E')).toBe(false)
expect(textUtils.isPairedCharacter('\uFE0E')).toBe(false)
expect(textUtils.isPairedCharacter('\uFE0E\uFE0E')).toBe(false)
expect(textUtils.isPairedCharacter('ae\u0301c', 0)).toBe(false)
expect(textUtils.isPairedCharacter('ae\u0301c', 1)).toBe(true)
expect(textUtils.isPairedCharacter('ae\u0301c', 2)).toBe(false)
expect(textUtils.isPairedCharacter('ae\u0301c', 3)).toBe(false)
expect(textUtils.isPairedCharacter('ae\u0301c', 4)).toBe(false)
})
)
describe('.isDoubleWidthCharacter(character)', () =>
it('returns true when the character is either japanese, chinese or a full width form', () => {
expect(textUtils.isDoubleWidthCharacter('我')).toBe(true)
expect(textUtils.isDoubleWidthCharacter('私')).toBe(true)
expect(textUtils.isDoubleWidthCharacter('')).toBe(true)
expect(textUtils.isDoubleWidthCharacter('')).toBe(true)
expect(textUtils.isDoubleWidthCharacter('¢')).toBe(true)
expect(textUtils.isDoubleWidthCharacter('a')).toBe(false)
})
)
describe('.isHalfWidthCharacter(character)', () =>
it('returns true when the character is an half width form', () => {
expect(textUtils.isHalfWidthCharacter('ハ')).toBe(true)
expect(textUtils.isHalfWidthCharacter('ヒ')).toBe(true)
expect(textUtils.isHalfWidthCharacter('ᆲ')).toBe(true)
expect(textUtils.isHalfWidthCharacter('■')).toBe(true)
expect(textUtils.isHalfWidthCharacter('B')).toBe(false)
})
)
describe('.isKoreanCharacter(character)', () =>
it('returns true when the character is a korean character', () => {
expect(textUtils.isKoreanCharacter('우')).toBe(true)
expect(textUtils.isKoreanCharacter('가')).toBe(true)
expect(textUtils.isKoreanCharacter('ㅢ')).toBe(true)
expect(textUtils.isKoreanCharacter('ㄼ')).toBe(true)
expect(textUtils.isKoreanCharacter('O')).toBe(false)
})
)
describe('.isWrapBoundary(previousCharacter, character)', () =>
it('returns true when the character is CJK or when the previous character is a space/tab', () => {
const anyCharacter = 'x'
expect(textUtils.isWrapBoundary(anyCharacter, '我')).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, '私')).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, '')).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, '')).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, '¢')).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, 'ハ')).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, 'ヒ')).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, 'ᆲ')).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, '■')).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, '우')).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, '가')).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, 'ㅢ')).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, 'ㄼ')).toBe(true)
expect(textUtils.isWrapBoundary(' ', 'h')).toBe(true)
expect(textUtils.isWrapBoundary('\t', 'h')).toBe(true)
expect(textUtils.isWrapBoundary('a', 'h')).toBe(false)
})
)
})

View File

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

503
spec/theme-manager-spec.js Normal file
View File

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

View File

@@ -1,37 +0,0 @@
TextBuffer = require 'text-buffer'
TokenizedBuffer = require '../src/tokenized-buffer'
describe "TokenIterator", ->
it "correctly terminates scopes at the beginning of the line (regression)", ->
grammar = atom.grammars.createGrammar('test', {
'scopeName': 'text.broken'
'name': 'Broken grammar'
'patterns': [
{
'begin': 'start'
'end': '(?=end)'
'name': 'blue.broken'
}
{
'match': '.'
'name': 'yellow.broken'
}
]
})
buffer = new TextBuffer(text: """
start x
end x
x
""")
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
tokenizedBuffer.setGrammar(grammar)
tokenIterator = tokenizedBuffer.tokenizedLines[1].getTokenIterator()
tokenIterator.next()
expect(tokenIterator.getBufferStart()).toBe 0
expect(tokenIterator.getScopeEnds()).toEqual []
expect(tokenIterator.getScopeStarts()).toEqual ['text.broken', 'yellow.broken']

View File

@@ -0,0 +1,43 @@
const TextBuffer = require('text-buffer')
const TokenizedBuffer = require('../src/tokenized-buffer')
describe('TokenIterator', () =>
it('correctly terminates scopes at the beginning of the line (regression)', () => {
const grammar = atom.grammars.createGrammar('test', {
'scopeName': 'text.broken',
'name': 'Broken grammar',
'patterns': [
{
'begin': 'start',
'end': '(?=end)',
'name': 'blue.broken'
},
{
'match': '.',
'name': 'yellow.broken'
}
]
})
const buffer = new TextBuffer({text: `\
start x
end x
x\
`})
const tokenizedBuffer = new TokenizedBuffer({
buffer,
config: atom.config,
grammarRegistry: atom.grammars,
packageManager: atom.packages,
assert: atom.assert
})
tokenizedBuffer.setGrammar(grammar)
const tokenIterator = tokenizedBuffer.tokenizedLines[1].getTokenIterator()
tokenIterator.next()
expect(tokenIterator.getBufferStart()).toBe(0)
expect(tokenIterator.getScopeEnds()).toEqual([])
expect(tokenIterator.getScopeStarts()).toEqual(['text.broken', 'yellow.broken'])
})
)

View File

@@ -643,186 +643,6 @@ describe('TokenizedBuffer', () => {
})
})
describe('.toggleLineCommentsForBufferRows', () => {
describe('xml', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-xml')
buffer = new TextBuffer('<!-- test -->')
tokenizedBuffer = new TokenizedBuffer({
buffer,
grammar: atom.grammars.grammarForScopeName('text.xml'),
scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config)
})
})
it('removes the leading whitespace from the comment end pattern match when uncommenting lines', () => {
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0)
expect(buffer.lineForRow(0)).toBe('test')
})
})
describe('less', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-less')
await atom.packages.activatePackage('language-css')
buffer = await TextBuffer.load(require.resolve('./fixtures/sample.less'))
tokenizedBuffer = new TokenizedBuffer({
buffer,
grammar: atom.grammars.grammarForScopeName('source.css.less'),
scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config)
})
})
it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart` when commenting lines', () => {
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0)
expect(buffer.lineForRow(0)).toBe('// @color: #4D926F;')
})
})
describe('css', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-css')
buffer = await TextBuffer.load(require.resolve('./fixtures/css.css'))
tokenizedBuffer = new TokenizedBuffer({
buffer,
grammar: atom.grammars.grammarForScopeName('source.css'),
scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config)
})
})
it('comments/uncomments lines in the given range', () => {
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1)
expect(buffer.lineForRow(0)).toBe('/*body {')
expect(buffer.lineForRow(1)).toBe(' font-size: 1234px;*/')
expect(buffer.lineForRow(2)).toBe(' width: 110%;')
expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;')
tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2)
expect(buffer.lineForRow(0)).toBe('/*body {')
expect(buffer.lineForRow(1)).toBe(' font-size: 1234px;*/')
expect(buffer.lineForRow(2)).toBe(' /*width: 110%;*/')
expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;')
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1)
expect(buffer.lineForRow(0)).toBe('body {')
expect(buffer.lineForRow(1)).toBe(' font-size: 1234px;')
expect(buffer.lineForRow(2)).toBe(' /*width: 110%;*/')
expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;')
})
it('uncomments lines with leading whitespace', () => {
buffer.setTextInRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/')
tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2)
expect(buffer.lineForRow(2)).toBe(' width: 110%;')
})
it('uncomments lines with trailing whitespace', () => {
buffer.setTextInRange([[2, 0], [2, Infinity]], '/*width: 110%;*/ ')
tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2)
expect(buffer.lineForRow(2)).toBe('width: 110%; ')
})
it('uncomments lines with leading and trailing whitespace', () => {
buffer.setTextInRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/ ')
tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2)
expect(buffer.lineForRow(2)).toBe(' width: 110%; ')
})
})
describe('coffeescript', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-coffee-script')
buffer = await TextBuffer.load(require.resolve('./fixtures/coffee.coffee'))
tokenizedBuffer = new TokenizedBuffer({
buffer,
tabLength: 2,
grammar: atom.grammars.grammarForScopeName('source.coffee'),
scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config)
})
})
it('comments/uncomments lines in the given range', () => {
tokenizedBuffer.toggleLineCommentsForBufferRows(4, 6)
expect(buffer.lineForRow(4)).toBe(' # pivot = items.shift()')
expect(buffer.lineForRow(5)).toBe(' # left = []')
expect(buffer.lineForRow(6)).toBe(' # right = []')
tokenizedBuffer.toggleLineCommentsForBufferRows(4, 5)
expect(buffer.lineForRow(4)).toBe(' pivot = items.shift()')
expect(buffer.lineForRow(5)).toBe(' left = []')
expect(buffer.lineForRow(6)).toBe(' # right = []')
})
it('comments/uncomments empty lines', () => {
tokenizedBuffer.toggleLineCommentsForBufferRows(4, 7)
expect(buffer.lineForRow(4)).toBe(' # pivot = items.shift()')
expect(buffer.lineForRow(5)).toBe(' # left = []')
expect(buffer.lineForRow(6)).toBe(' # right = []')
expect(buffer.lineForRow(7)).toBe(' # ')
tokenizedBuffer.toggleLineCommentsForBufferRows(4, 5)
expect(buffer.lineForRow(4)).toBe(' pivot = items.shift()')
expect(buffer.lineForRow(5)).toBe(' left = []')
expect(buffer.lineForRow(6)).toBe(' # right = []')
expect(buffer.lineForRow(7)).toBe(' # ')
})
})
describe('javascript', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-javascript')
buffer = await TextBuffer.load(require.resolve('./fixtures/sample.js'))
tokenizedBuffer = new TokenizedBuffer({
buffer,
tabLength: 2,
grammar: atom.grammars.grammarForScopeName('source.js'),
scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config)
})
})
it('comments/uncomments lines in the given range', () => {
tokenizedBuffer.toggleLineCommentsForBufferRows(4, 7)
expect(buffer.lineForRow(4)).toBe(' // while(items.length > 0) {')
expect(buffer.lineForRow(5)).toBe(' // current = items.shift();')
expect(buffer.lineForRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
expect(buffer.lineForRow(7)).toBe(' // }')
tokenizedBuffer.toggleLineCommentsForBufferRows(4, 5)
expect(buffer.lineForRow(4)).toBe(' while(items.length > 0) {')
expect(buffer.lineForRow(5)).toBe(' current = items.shift();')
expect(buffer.lineForRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
expect(buffer.lineForRow(7)).toBe(' // }')
buffer.setText('\tvar i;')
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0)
expect(buffer.lineForRow(0)).toBe('\t// var i;')
buffer.setText('var i;')
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0)
expect(buffer.lineForRow(0)).toBe('// var i;')
buffer.setText(' var i;')
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0)
expect(buffer.lineForRow(0)).toBe(' // var i;')
buffer.setText(' ')
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0)
expect(buffer.lineForRow(0)).toBe(' // ')
buffer.setText(' a\n \n b')
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 2)
expect(buffer.lineForRow(0)).toBe(' // a')
expect(buffer.lineForRow(1)).toBe(' // ')
expect(buffer.lineForRow(2)).toBe(' // b')
buffer.setText(' \n // var i;')
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1)
expect(buffer.lineForRow(0)).toBe(' ')
expect(buffer.lineForRow(1)).toBe(' var i;')
})
})
})
describe('.isFoldableAtRow(row)', () => {
beforeEach(() => {
buffer = atom.project.bufferForPathSync('sample.js')

View File

@@ -1,213 +0,0 @@
{CompositeDisposable} = require 'atom'
TooltipManager = require '../src/tooltip-manager'
Tooltip = require '../src/tooltip'
_ = require 'underscore-plus'
describe "TooltipManager", ->
[manager, element] = []
ctrlX = _.humanizeKeystroke("ctrl-x")
ctrlY = _.humanizeKeystroke("ctrl-y")
beforeEach ->
manager = new TooltipManager(keymapManager: atom.keymaps, viewRegistry: atom.views)
element = createElement 'foo'
createElement = (className) ->
el = document.createElement('div')
el.classList.add(className)
jasmine.attachToDOM(el)
el
mouseEnter = (element) ->
element.dispatchEvent(new CustomEvent('mouseenter', bubbles: false))
element.dispatchEvent(new CustomEvent('mouseover', bubbles: true))
mouseLeave = (element) ->
element.dispatchEvent(new CustomEvent('mouseleave', bubbles: false))
element.dispatchEvent(new CustomEvent('mouseout', bubbles: true))
hover = (element, fn) ->
mouseEnter(element)
advanceClock(manager.hoverDefaults.delay.show)
fn()
mouseLeave(element)
advanceClock(manager.hoverDefaults.delay.hide)
describe "::add(target, options)", ->
describe "when the trigger is 'hover' (the default)", ->
it "creates a tooltip when hovering over the target element", ->
manager.add element, title: "Title"
hover element, ->
expect(document.body.querySelector(".tooltip")).toHaveText("Title")
it "displays tooltips immediately when hovering over new elements once a tooltip has been displayed once", ->
disposables = new CompositeDisposable
element1 = createElement('foo')
disposables.add(manager.add element1, title: 'Title')
element2 = createElement('bar')
disposables.add(manager.add element2, title: 'Title')
element3 = createElement('baz')
disposables.add(manager.add element3, title: 'Title')
hover element1, ->
expect(document.body.querySelector(".tooltip")).toBeNull()
mouseEnter(element2)
expect(document.body.querySelector(".tooltip")).not.toBeNull()
mouseLeave(element2)
advanceClock(manager.hoverDefaults.delay.hide)
expect(document.body.querySelector(".tooltip")).toBeNull()
advanceClock(Tooltip.FOLLOW_THROUGH_DURATION)
mouseEnter(element3)
expect(document.body.querySelector(".tooltip")).toBeNull()
advanceClock(manager.hoverDefaults.delay.show)
expect(document.body.querySelector(".tooltip")).not.toBeNull()
disposables.dispose()
describe "when the trigger is 'manual'", ->
it "creates a tooltip immediately and only hides it on dispose", ->
disposable = manager.add element, title: "Title", trigger: "manual"
expect(document.body.querySelector(".tooltip")).toHaveText("Title")
disposable.dispose()
expect(document.body.querySelector(".tooltip")).toBeNull()
describe "when the trigger is 'click'", ->
it "shows and hides the tooltip when the target element is clicked", ->
disposable = manager.add element, title: "Title", trigger: "click"
expect(document.body.querySelector(".tooltip")).toBeNull()
element.click()
expect(document.body.querySelector(".tooltip")).not.toBeNull()
element.click()
expect(document.body.querySelector(".tooltip")).toBeNull()
# Hide the tooltip when clicking anywhere but inside the tooltip element
element.click()
expect(document.body.querySelector(".tooltip")).not.toBeNull()
document.body.querySelector(".tooltip").click()
expect(document.body.querySelector(".tooltip")).not.toBeNull()
document.body.querySelector(".tooltip").firstChild.click()
expect(document.body.querySelector(".tooltip")).not.toBeNull()
document.body.click()
expect(document.body.querySelector(".tooltip")).toBeNull()
# Tooltip can show again after hiding due to clicking outside of the tooltip
element.click()
expect(document.body.querySelector(".tooltip")).not.toBeNull()
element.click()
expect(document.body.querySelector(".tooltip")).toBeNull()
it "allows a custom item to be specified for the content of the tooltip", ->
tooltipElement = document.createElement('div')
manager.add element, item: {element: tooltipElement}
hover element, ->
expect(tooltipElement.closest(".tooltip")).not.toBeNull()
it "allows a custom class to be specified for the tooltip", ->
tooltipElement = document.createElement('div')
manager.add element, title: 'Title', class: 'custom-tooltip-class'
hover element, ->
expect(document.body.querySelector(".tooltip").classList.contains('custom-tooltip-class')).toBe(true)
it "allows jQuery elements to be passed as the target", ->
element2 = document.createElement('div')
jasmine.attachToDOM(element2)
fakeJqueryWrapper = [element, element2]
fakeJqueryWrapper.jquery = 'any-version'
disposable = manager.add fakeJqueryWrapper, title: "Title"
hover element, -> expect(document.body.querySelector(".tooltip")).toHaveText("Title")
expect(document.body.querySelector(".tooltip")).toBeNull()
hover element2, -> expect(document.body.querySelector(".tooltip")).toHaveText("Title")
expect(document.body.querySelector(".tooltip")).toBeNull()
disposable.dispose()
hover element, -> expect(document.body.querySelector(".tooltip")).toBeNull()
hover element2, -> expect(document.body.querySelector(".tooltip")).toBeNull()
describe "when a keyBindingCommand is specified", ->
describe "when a title is specified", ->
it "appends the key binding corresponding to the command to the title", ->
atom.keymaps.add 'test',
'.foo': 'ctrl-x ctrl-y': 'test-command'
'.bar': 'ctrl-x ctrl-z': 'test-command'
manager.add element, title: "Title", keyBindingCommand: 'test-command'
hover element, ->
tooltipElement = document.body.querySelector(".tooltip")
expect(tooltipElement).toHaveText "Title #{ctrlX} #{ctrlY}"
describe "when no title is specified", ->
it "shows the key binding corresponding to the command alone", ->
atom.keymaps.add 'test', '.foo': 'ctrl-x ctrl-y': 'test-command'
manager.add element, keyBindingCommand: 'test-command'
hover element, ->
tooltipElement = document.body.querySelector(".tooltip")
expect(tooltipElement).toHaveText "#{ctrlX} #{ctrlY}"
describe "when a keyBindingTarget is specified", ->
it "looks up the key binding relative to the target", ->
atom.keymaps.add 'test',
'.bar': 'ctrl-x ctrl-z': 'test-command'
'.foo': 'ctrl-x ctrl-y': 'test-command'
manager.add element, keyBindingCommand: 'test-command', keyBindingTarget: element
hover element, ->
tooltipElement = document.body.querySelector(".tooltip")
expect(tooltipElement).toHaveText "#{ctrlX} #{ctrlY}"
it "does not display the keybinding if there is nothing mapped to the specified keyBindingCommand", ->
manager.add element, title: 'A Title', keyBindingCommand: 'test-command', keyBindingTarget: element
hover element, ->
tooltipElement = document.body.querySelector(".tooltip")
expect(tooltipElement.textContent).toBe "A Title"
describe "when .dispose() is called on the returned disposable", ->
it "no longer displays the tooltip on hover", ->
disposable = manager.add element, title: "Title"
hover element, ->
expect(document.body.querySelector(".tooltip")).toHaveText("Title")
disposable.dispose()
hover element, ->
expect(document.body.querySelector(".tooltip")).toBeNull()
describe "when the window is resized", ->
it "hides the tooltips", ->
disposable = manager.add element, title: "Title"
hover element, ->
expect(document.body.querySelector(".tooltip")).not.toBeNull()
window.dispatchEvent(new CustomEvent('resize'))
expect(document.body.querySelector(".tooltip")).toBeNull()
disposable.dispose()
describe "findTooltips", ->
it "adds and remove tooltips correctly", ->
expect(manager.findTooltips(element).length).toBe(0)
disposable1 = manager.add element, title: "elem1"
expect(manager.findTooltips(element).length).toBe(1)
disposable2 = manager.add element, title: "elem2"
expect(manager.findTooltips(element).length).toBe(2)
disposable1.dispose()
expect(manager.findTooltips(element).length).toBe(1)
disposable2.dispose()
expect(manager.findTooltips(element).length).toBe(0)
it "lets us hide tooltips programmatically", ->
disposable = manager.add element, title: "Title"
hover element, ->
expect(document.body.querySelector(".tooltip")).not.toBeNull()
manager.findTooltips(element)[0].hide()
expect(document.body.querySelector(".tooltip")).toBeNull()
disposable.dispose()

View File

@@ -0,0 +1,257 @@
const {CompositeDisposable} = require('atom')
const TooltipManager = require('../src/tooltip-manager')
const Tooltip = require('../src/tooltip')
const _ = require('underscore-plus')
describe('TooltipManager', () => {
let manager, element
const ctrlX = _.humanizeKeystroke('ctrl-x')
const ctrlY = _.humanizeKeystroke('ctrl-y')
const hover = function (element, fn) {
mouseEnter(element)
advanceClock(manager.hoverDefaults.delay.show)
fn()
mouseLeave(element)
advanceClock(manager.hoverDefaults.delay.hide)
}
beforeEach(function () {
manager = new TooltipManager({keymapManager: atom.keymaps, viewRegistry: atom.views})
element = createElement('foo')
})
describe('::add(target, options)', () => {
describe("when the trigger is 'hover' (the default)", () => {
it('creates a tooltip when hovering over the target element', () => {
manager.add(element, {title: 'Title'})
hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title'))
})
it('displays tooltips immediately when hovering over new elements once a tooltip has been displayed once', () => {
const disposables = new CompositeDisposable()
const element1 = createElement('foo')
disposables.add(manager.add(element1, {title: 'Title'}))
const element2 = createElement('bar')
disposables.add(manager.add(element2, {title: 'Title'}))
const element3 = createElement('baz')
disposables.add(manager.add(element3, {title: 'Title'}))
hover(element1, () => {})
expect(document.body.querySelector('.tooltip')).toBeNull()
mouseEnter(element2)
expect(document.body.querySelector('.tooltip')).not.toBeNull()
mouseLeave(element2)
advanceClock(manager.hoverDefaults.delay.hide)
expect(document.body.querySelector('.tooltip')).toBeNull()
advanceClock(Tooltip.FOLLOW_THROUGH_DURATION)
mouseEnter(element3)
expect(document.body.querySelector('.tooltip')).toBeNull()
advanceClock(manager.hoverDefaults.delay.show)
expect(document.body.querySelector('.tooltip')).not.toBeNull()
disposables.dispose()
})
})
describe("when the trigger is 'manual'", () =>
it('creates a tooltip immediately and only hides it on dispose', () => {
const disposable = manager.add(element, {title: 'Title', trigger: 'manual'})
expect(document.body.querySelector('.tooltip')).toHaveText('Title')
disposable.dispose()
expect(document.body.querySelector('.tooltip')).toBeNull()
})
)
describe("when the trigger is 'click'", () =>
it('shows and hides the tooltip when the target element is clicked', () => {
manager.add(element, {title: 'Title', trigger: 'click'})
expect(document.body.querySelector('.tooltip')).toBeNull()
element.click()
expect(document.body.querySelector('.tooltip')).not.toBeNull()
element.click()
expect(document.body.querySelector('.tooltip')).toBeNull()
// Hide the tooltip when clicking anywhere but inside the tooltip element
element.click()
expect(document.body.querySelector('.tooltip')).not.toBeNull()
document.body.querySelector('.tooltip').click()
expect(document.body.querySelector('.tooltip')).not.toBeNull()
document.body.querySelector('.tooltip').firstChild.click()
expect(document.body.querySelector('.tooltip')).not.toBeNull()
document.body.click()
expect(document.body.querySelector('.tooltip')).toBeNull()
// Tooltip can show again after hiding due to clicking outside of the tooltip
element.click()
expect(document.body.querySelector('.tooltip')).not.toBeNull()
element.click()
expect(document.body.querySelector('.tooltip')).toBeNull()
})
)
it('allows a custom item to be specified for the content of the tooltip', () => {
const tooltipElement = document.createElement('div')
manager.add(element, {item: {element: tooltipElement}})
hover(element, () => expect(tooltipElement.closest('.tooltip')).not.toBeNull())
})
it('allows a custom class to be specified for the tooltip', () => {
manager.add(element, {title: 'Title', class: 'custom-tooltip-class'})
hover(element, () => expect(document.body.querySelector('.tooltip').classList.contains('custom-tooltip-class')).toBe(true))
})
it('allows jQuery elements to be passed as the target', () => {
const element2 = document.createElement('div')
jasmine.attachToDOM(element2)
const fakeJqueryWrapper = {
0: element,
1: element2,
length: 2,
jquery: 'any-version'
}
const disposable = manager.add(fakeJqueryWrapper, {title: 'Title'})
hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title'))
expect(document.body.querySelector('.tooltip')).toBeNull()
hover(element2, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title'))
expect(document.body.querySelector('.tooltip')).toBeNull()
disposable.dispose()
hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull())
hover(element2, () => expect(document.body.querySelector('.tooltip')).toBeNull())
})
describe('when a keyBindingCommand is specified', () => {
describe('when a title is specified', () =>
it('appends the key binding corresponding to the command to the title', () => {
atom.keymaps.add('test', {
'.foo': { 'ctrl-x ctrl-y': 'test-command'
},
'.bar': { 'ctrl-x ctrl-z': 'test-command'
}
}
)
manager.add(element, {title: 'Title', keyBindingCommand: 'test-command'})
hover(element, function () {
const tooltipElement = document.body.querySelector('.tooltip')
expect(tooltipElement).toHaveText(`Title ${ctrlX} ${ctrlY}`)
})
})
)
describe('when no title is specified', () =>
it('shows the key binding corresponding to the command alone', () => {
atom.keymaps.add('test', {'.foo': {'ctrl-x ctrl-y': 'test-command'}})
manager.add(element, {keyBindingCommand: 'test-command'})
hover(element, function () {
const tooltipElement = document.body.querySelector('.tooltip')
expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`)
})
})
)
describe('when a keyBindingTarget is specified', () => {
it('looks up the key binding relative to the target', () => {
atom.keymaps.add('test', {
'.bar': { 'ctrl-x ctrl-z': 'test-command'
},
'.foo': { 'ctrl-x ctrl-y': 'test-command'
}
}
)
manager.add(element, {keyBindingCommand: 'test-command', keyBindingTarget: element})
hover(element, function () {
const tooltipElement = document.body.querySelector('.tooltip')
expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`)
})
})
it('does not display the keybinding if there is nothing mapped to the specified keyBindingCommand', () => {
manager.add(element, {title: 'A Title', keyBindingCommand: 'test-command', keyBindingTarget: element})
hover(element, function () {
const tooltipElement = document.body.querySelector('.tooltip')
expect(tooltipElement.textContent).toBe('A Title')
})
})
})
})
describe('when .dispose() is called on the returned disposable', () =>
it('no longer displays the tooltip on hover', () => {
const disposable = manager.add(element, {title: 'Title'})
hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title'))
disposable.dispose()
hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull())
})
)
describe('when the window is resized', () =>
it('hides the tooltips', () => {
const disposable = manager.add(element, {title: 'Title'})
hover(element, function () {
expect(document.body.querySelector('.tooltip')).not.toBeNull()
window.dispatchEvent(new CustomEvent('resize'))
expect(document.body.querySelector('.tooltip')).toBeNull()
disposable.dispose()
})
})
)
describe('findTooltips', () => {
it('adds and remove tooltips correctly', () => {
expect(manager.findTooltips(element).length).toBe(0)
const disposable1 = manager.add(element, {title: 'elem1'})
expect(manager.findTooltips(element).length).toBe(1)
const disposable2 = manager.add(element, {title: 'elem2'})
expect(manager.findTooltips(element).length).toBe(2)
disposable1.dispose()
expect(manager.findTooltips(element).length).toBe(1)
disposable2.dispose()
expect(manager.findTooltips(element).length).toBe(0)
})
it('lets us hide tooltips programmatically', () => {
const disposable = manager.add(element, {title: 'Title'})
hover(element, function () {
expect(document.body.querySelector('.tooltip')).not.toBeNull()
manager.findTooltips(element)[0].hide()
expect(document.body.querySelector('.tooltip')).toBeNull()
disposable.dispose()
})
})
})
})
})
function createElement (className) {
const el = document.createElement('div')
el.classList.add(className)
jasmine.attachToDOM(el)
return el
}
function mouseEnter (element) {
element.dispatchEvent(new CustomEvent('mouseenter', {bubbles: false}))
element.dispatchEvent(new CustomEvent('mouseover', {bubbles: true}))
}
function mouseLeave (element) {
element.dispatchEvent(new CustomEvent('mouseleave', {bubbles: false}))
element.dispatchEvent(new CustomEvent('mouseout', {bubbles: true}))
}

View File

@@ -0,0 +1,75 @@
/** @babel */
import url from 'url'
import {it} from './async-spec-helpers'
import URIHandlerRegistry from '../src/uri-handler-registry'
describe('URIHandlerRegistry', () => {
let registry
beforeEach(() => {
registry = new URIHandlerRegistry(5)
})
it('handles URIs on a per-host basis', () => {
const testPackageSpy = jasmine.createSpy()
const otherPackageSpy = jasmine.createSpy()
registry.registerHostHandler('test-package', testPackageSpy)
registry.registerHostHandler('other-package', otherPackageSpy)
registry.handleURI('atom://yet-another-package/path')
expect(testPackageSpy).not.toHaveBeenCalled()
expect(otherPackageSpy).not.toHaveBeenCalled()
registry.handleURI('atom://test-package/path')
expect(testPackageSpy).toHaveBeenCalledWith(url.parse('atom://test-package/path', true), 'atom://test-package/path')
expect(otherPackageSpy).not.toHaveBeenCalled()
registry.handleURI('atom://other-package/path')
expect(otherPackageSpy).toHaveBeenCalledWith(url.parse('atom://other-package/path', true), 'atom://other-package/path')
})
it('keeps track of the most recent URIs', () => {
const spy1 = jasmine.createSpy()
const spy2 = jasmine.createSpy()
const changeSpy = jasmine.createSpy()
registry.registerHostHandler('one', spy1)
registry.registerHostHandler('two', spy2)
registry.onHistoryChange(changeSpy)
const uris = [
'atom://one/something?asdf=1',
'atom://fake/nothing',
'atom://two/other/stuff',
'atom://one/more/thing',
'atom://two/more/stuff'
]
uris.forEach(u => registry.handleURI(u))
expect(changeSpy.callCount).toBe(5)
expect(registry.getRecentlyHandledURIs()).toEqual(uris.map((u, idx) => {
return {id: idx + 1, uri: u, handled: !u.match(/fake/), host: url.parse(u).host}
}).reverse())
registry.handleURI('atom://another/url')
expect(changeSpy.callCount).toBe(6)
const history = registry.getRecentlyHandledURIs()
expect(history.length).toBe(5)
expect(history[0].uri).toBe('atom://another/url')
expect(history[4].uri).toBe(uris[1])
})
it('refuses to handle bad URLs', () => {
[
'atom:package/path',
'atom:8080://package/path',
'user:pass@atom://package/path',
'smth://package/path'
].forEach(uri => {
expect(() => registry.handleURI(uri)).toThrow()
})
})
})

View File

@@ -1,163 +0,0 @@
ViewRegistry = require '../src/view-registry'
describe "ViewRegistry", ->
registry = null
beforeEach ->
registry = new ViewRegistry
afterEach ->
registry.clearDocumentRequests()
describe "::getView(object)", ->
describe "when passed a DOM node", ->
it "returns the given DOM node", ->
node = document.createElement('div')
expect(registry.getView(node)).toBe node
describe "when passed an object with an element property", ->
it "returns the element property if it's an instance of HTMLElement", ->
class TestComponent
constructor: -> @element = document.createElement('div')
component = new TestComponent
expect(registry.getView(component)).toBe component.element
describe "when passed an object with a getElement function", ->
it "returns the return value of getElement if it's an instance of HTMLElement", ->
class TestComponent
getElement: ->
@myElement ?= document.createElement('div')
component = new TestComponent
expect(registry.getView(component)).toBe component.myElement
describe "when passed a model object", ->
describe "when a view provider is registered matching the object's constructor", ->
it "constructs a view element and assigns the model on it", ->
class TestModel
class TestModelSubclass extends TestModel
class TestView
initialize: (@model) -> this
model = new TestModel
registry.addViewProvider TestModel, (model) ->
new TestView().initialize(model)
view = registry.getView(model)
expect(view instanceof TestView).toBe true
expect(view.model).toBe model
subclassModel = new TestModelSubclass
view2 = registry.getView(subclassModel)
expect(view2 instanceof TestView).toBe true
expect(view2.model).toBe subclassModel
describe "when a view provider is registered generically, and works with the object", ->
it "constructs a view element and assigns the model on it", ->
model = {a: 'b'}
registry.addViewProvider (model) ->
if model.a is 'b'
element = document.createElement('div')
element.className = 'test-element'
element
view = registry.getView({a: 'b'})
expect(view.className).toBe 'test-element'
expect(-> registry.getView({a: 'c'})).toThrow()
describe "when no view provider is registered for the object's constructor", ->
it "throws an exception", ->
expect(-> registry.getView(new Object)).toThrow()
describe "::addViewProvider(providerSpec)", ->
it "returns a disposable that can be used to remove the provider", ->
class TestModel
class TestView
initialize: (@model) -> this
disposable = registry.addViewProvider TestModel, (model) ->
new TestView().initialize(model)
expect(registry.getView(new TestModel) instanceof TestView).toBe true
disposable.dispose()
expect(-> registry.getView(new TestModel)).toThrow()
describe "::updateDocument(fn) and ::readDocument(fn)", ->
frameRequests = null
beforeEach ->
frameRequests = []
spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> frameRequests.push(fn)
it "performs all pending writes before all pending reads on the next animation frame", ->
events = []
registry.updateDocument -> events.push('write 1')
registry.readDocument -> events.push('read 1')
registry.readDocument -> events.push('read 2')
registry.updateDocument -> events.push('write 2')
expect(events).toEqual []
expect(frameRequests.length).toBe 1
frameRequests[0]()
expect(events).toEqual ['write 1', 'write 2', 'read 1', 'read 2']
frameRequests = []
events = []
disposable = registry.updateDocument -> events.push('write 3')
registry.updateDocument -> events.push('write 4')
registry.readDocument -> events.push('read 3')
disposable.dispose()
expect(frameRequests.length).toBe 1
frameRequests[0]()
expect(events).toEqual ['write 4', 'read 3']
it "performs writes requested from read callbacks in the same animation frame", ->
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
events = []
registry.updateDocument -> events.push('write 1')
registry.readDocument ->
registry.updateDocument -> events.push('write from read 1')
events.push('read 1')
registry.readDocument ->
registry.updateDocument -> events.push('write from read 2')
events.push('read 2')
registry.updateDocument -> events.push('write 2')
expect(frameRequests.length).toBe 1
frameRequests[0]()
expect(frameRequests.length).toBe 1
expect(events).toEqual [
'write 1'
'write 2'
'read 1'
'read 2'
'write from read 1'
'write from read 2'
]
describe "::getNextUpdatePromise()", ->
it "returns a promise that resolves at the end of the next update cycle", ->
updateCalled = false
readCalled = false
waitsFor 'getNextUpdatePromise to resolve', (done) ->
registry.getNextUpdatePromise().then ->
expect(updateCalled).toBe true
expect(readCalled).toBe true
done()
registry.updateDocument -> updateCalled = true
registry.readDocument -> readCalled = true

216
spec/view-registry-spec.js Normal file
View File

@@ -0,0 +1,216 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const ViewRegistry = require('../src/view-registry')
describe('ViewRegistry', () => {
let registry = null
beforeEach(() => {
registry = new ViewRegistry()
})
afterEach(() => {
registry.clearDocumentRequests()
})
describe('::getView(object)', () => {
describe('when passed a DOM node', () =>
it('returns the given DOM node', () => {
const node = document.createElement('div')
expect(registry.getView(node)).toBe(node)
})
)
describe('when passed an object with an element property', () =>
it("returns the element property if it's an instance of HTMLElement", () => {
class TestComponent {
constructor () {
this.element = document.createElement('div')
}
}
const component = new TestComponent()
expect(registry.getView(component)).toBe(component.element)
})
)
describe('when passed an object with a getElement function', () =>
it("returns the return value of getElement if it's an instance of HTMLElement", () => {
class TestComponent {
getElement () {
if (this.myElement == null) {
this.myElement = document.createElement('div')
}
return this.myElement
}
}
const component = new TestComponent()
expect(registry.getView(component)).toBe(component.myElement)
})
)
describe('when passed a model object', () => {
describe("when a view provider is registered matching the object's constructor", () =>
it('constructs a view element and assigns the model on it', () => {
class TestModel {}
class TestModelSubclass extends TestModel {}
class TestView {
initialize (model) {
this.model = model
return this
}
}
const model = new TestModel()
registry.addViewProvider(TestModel, (model) =>
new TestView().initialize(model)
)
const view = registry.getView(model)
expect(view instanceof TestView).toBe(true)
expect(view.model).toBe(model)
const subclassModel = new TestModelSubclass()
const view2 = registry.getView(subclassModel)
expect(view2 instanceof TestView).toBe(true)
expect(view2.model).toBe(subclassModel)
})
)
describe('when a view provider is registered generically, and works with the object', () =>
it('constructs a view element and assigns the model on it', () => {
registry.addViewProvider((model) => {
if (model.a === 'b') {
const element = document.createElement('div')
element.className = 'test-element'
return element
}
})
const view = registry.getView({a: 'b'})
expect(view.className).toBe('test-element')
expect(() => registry.getView({a: 'c'})).toThrow()
})
)
describe("when no view provider is registered for the object's constructor", () =>
it('throws an exception', () => {
expect(() => registry.getView({})).toThrow()
})
)
})
})
describe('::addViewProvider(providerSpec)', () =>
it('returns a disposable that can be used to remove the provider', () => {
class TestModel {}
class TestView {
initialize (model) {
this.model = model
return this
}
}
const disposable = registry.addViewProvider(TestModel, (model) =>
new TestView().initialize(model)
)
expect(registry.getView(new TestModel()) instanceof TestView).toBe(true)
disposable.dispose()
expect(() => registry.getView(new TestModel())).toThrow()
})
)
describe('::updateDocument(fn) and ::readDocument(fn)', () => {
let frameRequests = null
beforeEach(() => {
frameRequests = []
spyOn(window, 'requestAnimationFrame').andCallFake(fn => frameRequests.push(fn))
})
it('performs all pending writes before all pending reads on the next animation frame', () => {
let events = []
registry.updateDocument(() => events.push('write 1'))
registry.readDocument(() => events.push('read 1'))
registry.readDocument(() => events.push('read 2'))
registry.updateDocument(() => events.push('write 2'))
expect(events).toEqual([])
expect(frameRequests.length).toBe(1)
frameRequests[0]()
expect(events).toEqual(['write 1', 'write 2', 'read 1', 'read 2'])
frameRequests = []
events = []
const disposable = registry.updateDocument(() => events.push('write 3'))
registry.updateDocument(() => events.push('write 4'))
registry.readDocument(() => events.push('read 3'))
disposable.dispose()
expect(frameRequests.length).toBe(1)
frameRequests[0]()
expect(events).toEqual(['write 4', 'read 3'])
})
it('performs writes requested from read callbacks in the same animation frame', () => {
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
const events = []
registry.updateDocument(() => events.push('write 1'))
registry.readDocument(() => {
registry.updateDocument(() => events.push('write from read 1'))
events.push('read 1')
})
registry.readDocument(() => {
registry.updateDocument(() => events.push('write from read 2'))
events.push('read 2')
})
registry.updateDocument(() => events.push('write 2'))
expect(frameRequests.length).toBe(1)
frameRequests[0]()
expect(frameRequests.length).toBe(1)
expect(events).toEqual([
'write 1',
'write 2',
'read 1',
'read 2',
'write from read 1',
'write from read 2'
])
})
})
describe('::getNextUpdatePromise()', () =>
it('returns a promise that resolves at the end of the next update cycle', () => {
let updateCalled = false
let readCalled = false
waitsFor('getNextUpdatePromise to resolve', (done) => {
registry.getNextUpdatePromise().then(() => {
expect(updateCalled).toBe(true)
expect(readCalled).toBe(true)
done()
})
registry.updateDocument(() => { updateCalled = true })
registry.readDocument(() => { readCalled = true })
})
})
)
})

View File

@@ -1,209 +0,0 @@
KeymapManager = require 'atom-keymap'
TextEditor = require '../src/text-editor'
WindowEventHandler = require '../src/window-event-handler'
{ipcRenderer} = require 'electron'
describe "WindowEventHandler", ->
[windowEventHandler] = []
beforeEach ->
atom.uninstallWindowEventHandler()
spyOn(atom, 'hide')
initialPath = atom.project.getPaths()[0]
spyOn(atom, 'getLoadSettings').andCallFake ->
loadSettings = atom.getLoadSettings.originalValue.call(atom)
loadSettings.initialPath = initialPath
loadSettings
atom.project.destroy()
windowEventHandler = new WindowEventHandler({atomEnvironment: atom, applicationDelegate: atom.applicationDelegate})
windowEventHandler.initialize(window, document)
afterEach ->
windowEventHandler.unsubscribe()
atom.installWindowEventHandler()
describe "when the window is loaded", ->
it "doesn't have .is-blurred on the body tag", ->
return if process.platform is 'win32' #Win32TestFailures - can not steal focus
expect(document.body.className).not.toMatch("is-blurred")
describe "when the window is blurred", ->
beforeEach ->
window.dispatchEvent(new CustomEvent('blur'))
afterEach ->
document.body.classList.remove('is-blurred')
it "adds the .is-blurred class on the body", ->
expect(document.body.className).toMatch("is-blurred")
describe "when the window is focused again", ->
it "removes the .is-blurred class from the body", ->
window.dispatchEvent(new CustomEvent('focus'))
expect(document.body.className).not.toMatch("is-blurred")
describe "window:close event", ->
it "closes the window", ->
spyOn(atom, 'close')
window.dispatchEvent(new CustomEvent('window:close'))
expect(atom.close).toHaveBeenCalled()
describe "when a link is clicked", ->
it "opens the http/https links in an external application", ->
{shell} = require 'electron'
spyOn(shell, 'openExternal')
link = document.createElement('a')
linkChild = document.createElement('span')
link.appendChild(linkChild)
link.href = 'http://github.com'
jasmine.attachToDOM(link)
fakeEvent = {target: linkChild, currentTarget: link, preventDefault: (->)}
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).toHaveBeenCalled()
expect(shell.openExternal.argsForCall[0][0]).toBe "http://github.com"
shell.openExternal.reset()
link.href = 'https://github.com'
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).toHaveBeenCalled()
expect(shell.openExternal.argsForCall[0][0]).toBe "https://github.com"
shell.openExternal.reset()
link.href = ''
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).not.toHaveBeenCalled()
shell.openExternal.reset()
link.href = '#scroll-me'
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).not.toHaveBeenCalled()
describe "when a form is submitted", ->
it "prevents the default so that the window's URL isn't changed", ->
form = document.createElement('form')
jasmine.attachToDOM(form)
defaultPrevented = false
event = new CustomEvent('submit', bubbles: true)
event.preventDefault = -> defaultPrevented = true
form.dispatchEvent(event)
expect(defaultPrevented).toBe(true)
describe "core:focus-next and core:focus-previous", ->
describe "when there is no currently focused element", ->
it "focuses the element with the lowest/highest tabindex", ->
wrapperDiv = document.createElement('div')
wrapperDiv.innerHTML = """
<div>
<button tabindex="2"></button>
<input tabindex="1">
</div>
"""
elements = wrapperDiv.firstChild
jasmine.attachToDOM(elements)
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
expect(document.activeElement.tabIndex).toBe 1
document.body.focus()
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 2
describe "when a tabindex is set on the currently focused element", ->
it "focuses the element with the next highest/lowest tabindex, skipping disabled elements", ->
wrapperDiv = document.createElement('div')
wrapperDiv.innerHTML = """
<div>
<input tabindex="1">
<button tabindex="2"></button>
<button tabindex="5"></button>
<input tabindex="-1">
<input tabindex="3">
<button tabindex="7"></button>
<input tabindex="9" disabled>
</div>
"""
elements = wrapperDiv.firstChild
jasmine.attachToDOM(elements)
elements.querySelector('[tabindex="1"]').focus()
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
expect(document.activeElement.tabIndex).toBe 2
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
expect(document.activeElement.tabIndex).toBe 3
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
expect(document.activeElement.tabIndex).toBe 5
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
expect(document.activeElement.tabIndex).toBe 7
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
expect(document.activeElement.tabIndex).toBe 1
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 7
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 5
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 3
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 2
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 1
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 7
describe "when keydown events occur on the document", ->
it "dispatches the event via the KeymapManager and CommandRegistry", ->
dispatchedCommands = []
atom.commands.onWillDispatch (command) -> dispatchedCommands.push(command)
atom.commands.add '*', 'foo-command': ->
atom.keymaps.add 'source-name', '*': {'x': 'foo-command'}
event = KeymapManager.buildKeydownEvent('x', target: document.createElement('div'))
document.dispatchEvent(event)
expect(dispatchedCommands.length).toBe 1
expect(dispatchedCommands[0].type).toBe 'foo-command'
describe "native key bindings", ->
it "correctly dispatches them to active elements with the '.native-key-bindings' class", ->
webContentsSpy = jasmine.createSpyObj("webContents", ["copy", "paste"])
spyOn(atom.applicationDelegate, "getCurrentWindow").andReturn({
webContents: webContentsSpy
on: ->
})
nativeKeyBindingsInput = document.createElement("input")
nativeKeyBindingsInput.classList.add("native-key-bindings")
jasmine.attachToDOM(nativeKeyBindingsInput)
nativeKeyBindingsInput.focus()
atom.dispatchApplicationMenuCommand("core:copy")
atom.dispatchApplicationMenuCommand("core:paste")
expect(webContentsSpy.copy).toHaveBeenCalled()
expect(webContentsSpy.paste).toHaveBeenCalled()
webContentsSpy.copy.reset()
webContentsSpy.paste.reset()
normalInput = document.createElement("input")
jasmine.attachToDOM(normalInput)
normalInput.focus()
atom.dispatchApplicationMenuCommand("core:copy")
atom.dispatchApplicationMenuCommand("core:paste")
expect(webContentsSpy.copy).not.toHaveBeenCalled()
expect(webContentsSpy.paste).not.toHaveBeenCalled()

View File

@@ -0,0 +1,228 @@
const KeymapManager = require('atom-keymap')
const WindowEventHandler = require('../src/window-event-handler')
describe('WindowEventHandler', () => {
let windowEventHandler
beforeEach(() => {
atom.uninstallWindowEventHandler()
spyOn(atom, 'hide')
const initialPath = atom.project.getPaths()[0]
spyOn(atom, 'getLoadSettings').andCallFake(() => {
const loadSettings = atom.getLoadSettings.originalValue.call(atom)
loadSettings.initialPath = initialPath
return loadSettings
})
atom.project.destroy()
windowEventHandler = new WindowEventHandler({atomEnvironment: atom, applicationDelegate: atom.applicationDelegate})
windowEventHandler.initialize(window, document)
})
afterEach(() => {
windowEventHandler.unsubscribe()
atom.installWindowEventHandler()
})
describe('when the window is loaded', () =>
it("doesn't have .is-blurred on the body tag", () => {
if (process.platform === 'win32') { return } // Win32TestFailures - can not steal focus
expect(document.body.className).not.toMatch('is-blurred')
})
)
describe('when the window is blurred', () => {
beforeEach(() => window.dispatchEvent(new CustomEvent('blur')))
afterEach(() => document.body.classList.remove('is-blurred'))
it('adds the .is-blurred class on the body', () => expect(document.body.className).toMatch('is-blurred'))
describe('when the window is focused again', () =>
it('removes the .is-blurred class from the body', () => {
window.dispatchEvent(new CustomEvent('focus'))
expect(document.body.className).not.toMatch('is-blurred')
})
)
})
describe('window:close event', () =>
it('closes the window', () => {
spyOn(atom, 'close')
window.dispatchEvent(new CustomEvent('window:close'))
expect(atom.close).toHaveBeenCalled()
})
)
describe('when a link is clicked', () =>
it('opens the http/https links in an external application', () => {
const {shell} = require('electron')
spyOn(shell, 'openExternal')
const link = document.createElement('a')
const linkChild = document.createElement('span')
link.appendChild(linkChild)
link.href = 'http://github.com'
jasmine.attachToDOM(link)
const fakeEvent = {target: linkChild, currentTarget: link, preventDefault: () => {}}
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).toHaveBeenCalled()
expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com')
shell.openExternal.reset()
link.href = 'https://github.com'
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).toHaveBeenCalled()
expect(shell.openExternal.argsForCall[0][0]).toBe('https://github.com')
shell.openExternal.reset()
link.href = ''
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).not.toHaveBeenCalled()
shell.openExternal.reset()
link.href = '#scroll-me'
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).not.toHaveBeenCalled()
})
)
describe('when a form is submitted', () =>
it("prevents the default so that the window's URL isn't changed", () => {
const form = document.createElement('form')
jasmine.attachToDOM(form)
let defaultPrevented = false
const event = new CustomEvent('submit', {bubbles: true})
event.preventDefault = () => { defaultPrevented = true }
form.dispatchEvent(event)
expect(defaultPrevented).toBe(true)
})
)
describe('core:focus-next and core:focus-previous', () => {
describe('when there is no currently focused element', () =>
it('focuses the element with the lowest/highest tabindex', () => {
const wrapperDiv = document.createElement('div')
wrapperDiv.innerHTML = `
<div>
<button tabindex="2"></button>
<input tabindex="1">
</div>
`.trim()
const elements = wrapperDiv.firstChild
jasmine.attachToDOM(elements)
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(1)
document.body.focus()
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(2)
})
)
describe('when a tabindex is set on the currently focused element', () =>
it('focuses the element with the next highest/lowest tabindex, skipping disabled elements', () => {
const wrapperDiv = document.createElement('div')
wrapperDiv.innerHTML = `
<div>
<input tabindex="1">
<button tabindex="2"></button>
<button tabindex="5"></button>
<input tabindex="-1">
<input tabindex="3">
<button tabindex="7"></button>
<input tabindex="9" disabled>
</div>
`.trim()
const elements = wrapperDiv.firstChild
jasmine.attachToDOM(elements)
elements.querySelector('[tabindex="1"]').focus()
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(2)
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(3)
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(5)
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(7)
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(1)
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(7)
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(5)
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(3)
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(2)
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(1)
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(7)
})
)
})
describe('when keydown events occur on the document', () =>
it('dispatches the event via the KeymapManager and CommandRegistry', () => {
const dispatchedCommands = []
atom.commands.onWillDispatch(command => dispatchedCommands.push(command))
atom.commands.add('*', {'foo-command': () => {}})
atom.keymaps.add('source-name', {'*': {'x': 'foo-command'}})
const event = KeymapManager.buildKeydownEvent('x', {target: document.createElement('div')})
document.dispatchEvent(event)
expect(dispatchedCommands.length).toBe(1)
expect(dispatchedCommands[0].type).toBe('foo-command')
})
)
describe('native key bindings', () =>
it("correctly dispatches them to active elements with the '.native-key-bindings' class", () => {
const webContentsSpy = jasmine.createSpyObj('webContents', ['copy', 'paste'])
spyOn(atom.applicationDelegate, 'getCurrentWindow').andReturn({
webContents: webContentsSpy,
on: () => {}
})
const nativeKeyBindingsInput = document.createElement('input')
nativeKeyBindingsInput.classList.add('native-key-bindings')
jasmine.attachToDOM(nativeKeyBindingsInput)
nativeKeyBindingsInput.focus()
atom.dispatchApplicationMenuCommand('core:copy')
atom.dispatchApplicationMenuCommand('core:paste')
expect(webContentsSpy.copy).toHaveBeenCalled()
expect(webContentsSpy.paste).toHaveBeenCalled()
webContentsSpy.copy.reset()
webContentsSpy.paste.reset()
const normalInput = document.createElement('input')
jasmine.attachToDOM(normalInput)
normalInput.focus()
atom.dispatchApplicationMenuCommand('core:copy')
atom.dispatchApplicationMenuCommand('core:paste')
expect(webContentsSpy.copy).not.toHaveBeenCalled()
expect(webContentsSpy.paste).not.toHaveBeenCalled()
})
)
})

View File

@@ -1585,15 +1585,15 @@ i = /test/; #FIXME\
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)',
'JSDoc',
'JavaScript',
'Null Grammar',
'Regular Expression Replacement (JavaScript)',
'Regular Expressions (JavaScript)',
'TODO'
expect(atom2.grammars.getGrammars().map(grammar => grammar.scopeName).sort()).toEqual([
'source.coffee',
'source.js',
'source.js.regexp',
'source.js.regexp.replacement',
'source.jsdoc',
'source.litcoffee',
'text.plain.null-grammar',
'text.todo'
])
atom2.destroy()