mirror of
https://github.com/atom/atom.git
synced 2026-02-08 05:35:04 -05:00
Merge branch 'master' into b3-failing-seed
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
770
spec/atom-environment-spec.js
Normal file
770
spec/atom-environment-spec.js
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
'name': 'Test Ruby'
|
||||
'scopeName': 'test.rb'
|
||||
'firstLineMatch': '^\\#!.*(?:\\s|\\/)(?:testruby)(?:$|\\s)'
|
||||
'fileTypes': [
|
||||
'rb'
|
||||
]
|
||||
|
||||
5
spec/fixtures/packages/package-with-uri-handler/index.js
vendored
Normal file
5
spec/fixtures/packages/package-with-uri-handler/index.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
activate: () => null,
|
||||
deactivate: () => null,
|
||||
handleURI: () => null,
|
||||
}
|
||||
6
spec/fixtures/packages/package-with-uri-handler/package.json
vendored
Normal file
6
spec/fixtures/packages/package-with-uri-handler/package.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "package-with-uri-handler",
|
||||
"uriHandler": {
|
||||
"method": "handleURI"
|
||||
}
|
||||
}
|
||||
@@ -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
393
spec/git-repository-spec.js
Normal 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
|
||||
}
|
||||
@@ -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", ->
|
||||
|
||||
@@ -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 []
|
||||
77
spec/gutter-container-spec.js
Normal file
77
spec/gutter-container-spec.js
Normal 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([])
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -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
82
spec/gutter-spec.js
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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 () {
|
||||
|
||||
27
spec/main-process/parse-command-line.test.js
Normal file
27
spec/main-process/parse-command-line.test.js
Normal 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, [])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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"}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
955
spec/project-spec.js
Normal 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')
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -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
157
spec/selection-spec.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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])
|
||||
|
||||
@@ -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
@@ -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("B")).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, "B")).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
110
spec/text-utils-spec.js
Normal 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('B')).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, 'B')).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)
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -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
503
spec/theme-manager-spec.js
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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']
|
||||
43
spec/token-iterator-spec.js
Normal file
43
spec/token-iterator-spec.js
Normal 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'])
|
||||
})
|
||||
)
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
257
spec/tooltip-manager-spec.js
Normal file
257
spec/tooltip-manager-spec.js
Normal 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}))
|
||||
}
|
||||
75
spec/uri-handler-registry-spec.js
Normal file
75
spec/uri-handler-registry-spec.js
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
216
spec/view-registry-spec.js
Normal 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 })
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -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()
|
||||
228
spec/window-event-handler-spec.js
Normal file
228
spec/window-event-handler-spec.js
Normal 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()
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user