Merge remote-tracking branch 'origin/master' into mkt-core-uri-handlers

This commit is contained in:
Michelle Tilley
2017-11-02 16:30:29 -07:00
53 changed files with 15685 additions and 13574 deletions

View File

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

View File

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

View File

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

View File

@@ -1,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"

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

@@ -0,0 +1,927 @@
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) }
}
let serviceDisposable = 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
}
}
})
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)
})
})
describe('.open(path)', () => {
let absolutePath, newBufferHandler
beforeEach(() => {
absolutePath = require.resolve('./fixtures/dir/a')
newBufferHandler = jasmine.createSpy('newBufferHandler')
atom.project.onDidAddBuffer(newBufferHandler)
})
describe("when given an absolute path that isn't currently open", () =>
it("returns a new edit session for the given path and emits 'buffer-created'", () => {
let editor = null
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
runs(() => {
expect(editor.buffer.getPath()).toBe(absolutePath)
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
})
})
)
describe("when given a relative path that isn't currently opened", () =>
it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", () => {
let editor = null
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
runs(() => {
expect(editor.buffer.getPath()).toBe(absolutePath)
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
})
})
)
describe('when passed the path to a buffer that is currently opened', () =>
it('returns a new edit session containing currently opened buffer', () => {
let editor = null
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
runs(() => newBufferHandler.reset())
waitsForPromise(() =>
atom.workspace.open(absolutePath).then(({buffer}) => expect(buffer).toBe(editor.buffer))
)
waitsForPromise(() =>
atom.workspace.open('a').then(({buffer}) => {
expect(buffer).toBe(editor.buffer)
expect(newBufferHandler).not.toHaveBeenCalled()
})
)
})
)
describe('when not passed a path', () =>
it("returns a new edit session and emits 'buffer-created'", () => {
let editor = null
waitsForPromise(() => atom.workspace.open().then(o => { editor = o }))
runs(() => {
expect(editor.buffer.getPath()).toBeUndefined()
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
})
})
)
})
describe('.bufferForPath(path)', () => {
let buffer = null
beforeEach(() =>
waitsForPromise(() =>
atom.project.bufferForPath('a').then((o) => {
buffer = o
buffer.retain()
})
)
)
afterEach(() => buffer.release())
describe('when opening a previously opened path', () => {
it('does not create a new buffer', () => {
waitsForPromise(() =>
atom.project.bufferForPath('a').then(anotherBuffer => expect(anotherBuffer).toBe(buffer))
)
waitsForPromise(() =>
atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer))
)
waitsForPromise(() =>
Promise.all([
atom.project.bufferForPath('c'),
atom.project.bufferForPath('c')
]).then(([buffer1, buffer2]) => {
expect(buffer1).toBe(buffer2)
})
)
})
it('retries loading the buffer if it previously failed', () => {
waitsForPromise({shouldReject: true}, () => {
spyOn(TextBuffer, 'load').andCallFake(() => Promise.reject(new Error('Could not open file')))
return atom.project.bufferForPath('b')
})
waitsForPromise({shouldReject: false}, () => {
TextBuffer.load.andCallThrough()
return atom.project.bufferForPath('b')
})
})
it('creates a new buffer if the previous buffer was destroyed', () => {
buffer.release()
waitsForPromise(() =>
atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer))
)
})
})
})
describe('.repositoryForDirectory(directory)', () => {
it('resolves to null when the directory does not have a repository', () =>
waitsForPromise(() => {
const directory = new Directory('/tmp')
return atom.project.repositoryForDirectory(directory).then((result) => {
expect(result).toBeNull()
expect(atom.project.repositoryProviders.length).toBeGreaterThan(0)
expect(atom.project.repositoryPromisesByPath.size).toBe(0)
})
})
)
it('resolves to a GitRepository and is cached when the given directory is a Git repo', () =>
waitsForPromise(() => {
const directory = new Directory(path.join(__dirname, '..'))
const promise = atom.project.repositoryForDirectory(directory)
return promise.then((result) => {
expect(result).toBeInstanceOf(GitRepository)
const dirPath = directory.getRealPathSync()
expect(result.getPath()).toBe(path.join(dirPath, '.git'))
// Verify that the result is cached.
expect(atom.project.repositoryForDirectory(directory)).toBe(promise)
})
})
)
it('creates a new repository if a previous one with the same directory had been destroyed', () => {
let repository = null
const directory = new Directory(path.join(__dirname, '..'))
waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => { repository = repo }))
runs(() => {
expect(repository.isDestroyed()).toBe(false)
repository.destroy()
expect(repository.isDestroyed()).toBe(true)
})
waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => { repository = repo }))
runs(() => expect(repository.isDestroyed()).toBe(false))
})
})
describe('.setPaths(paths, options)', () => {
describe('when path is a file', () =>
it("sets its path to the file's parent directory and updates the root directory", () => {
const filePath = require.resolve('./fixtures/dir/a')
atom.project.setPaths([filePath])
expect(atom.project.getPaths()[0]).toEqual(path.dirname(filePath))
expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(filePath))
})
)
describe('when path is a directory', () => {
it('assigns the directories and repositories', () => {
const directory1 = temp.mkdirSync('non-git-repo')
const directory2 = temp.mkdirSync('git-repo1')
const directory3 = temp.mkdirSync('git-repo2')
const gitDirPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git'))
fs.copySync(gitDirPath, path.join(directory2, '.git'))
fs.copySync(gitDirPath, path.join(directory3, '.git'))
atom.project.setPaths([directory1, directory2, directory3])
const [repo1, repo2, repo3] = atom.project.getRepositories()
expect(repo1).toBeNull()
expect(repo2.getShortHead()).toBe('master')
expect(repo2.getPath()).toBe(fs.realpathSync(path.join(directory2, '.git')))
expect(repo3.getShortHead()).toBe('master')
expect(repo3.getPath()).toBe(fs.realpathSync(path.join(directory3, '.git')))
})
it('calls callbacks registered with ::onDidChangePaths', () => {
const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
atom.project.onDidChangePaths(onDidChangePathsSpy)
const paths = [ temp.mkdirSync('dir1'), temp.mkdirSync('dir2') ]
atom.project.setPaths(paths)
expect(onDidChangePathsSpy.callCount).toBe(1)
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths)
})
it('optionally throws an error with any paths that did not exist', () => {
const paths = [temp.mkdirSync('exists0'), '/doesnt-exists/0', temp.mkdirSync('exists1'), '/doesnt-exists/1']
try {
atom.project.setPaths(paths, {mustExist: true})
expect('no exception thrown').toBeUndefined()
} catch (e) {
expect(e.missingProjectPaths).toEqual([paths[1], paths[3]])
}
expect(atom.project.getPaths()).toEqual([paths[0], paths[2]])
})
})
describe('when no paths are given', () =>
it('clears its path', () => {
atom.project.setPaths([])
expect(atom.project.getPaths()).toEqual([])
expect(atom.project.getDirectories()).toEqual([])
})
)
it('normalizes the path to remove consecutive slashes, ., and .. segments', () => {
atom.project.setPaths([`${require.resolve('./fixtures/dir/a')}${path.sep}b${path.sep}${path.sep}..`])
expect(atom.project.getPaths()[0]).toEqual(path.dirname(require.resolve('./fixtures/dir/a')))
expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(require.resolve('./fixtures/dir/a')))
})
})
describe('.addPath(path, options)', () => {
it('calls callbacks registered with ::onDidChangePaths', () => {
const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
atom.project.onDidChangePaths(onDidChangePathsSpy)
const [oldPath] = atom.project.getPaths()
const newPath = temp.mkdirSync('dir')
atom.project.addPath(newPath)
expect(onDidChangePathsSpy.callCount).toBe(1)
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath])
})
it("doesn't add redundant paths", () => {
const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
atom.project.onDidChangePaths(onDidChangePathsSpy)
const [oldPath] = atom.project.getPaths()
// Doesn't re-add an existing root directory
atom.project.addPath(oldPath)
expect(atom.project.getPaths()).toEqual([oldPath])
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
// Doesn't add an entry for a file-path within an existing root directory
atom.project.addPath(path.join(oldPath, 'some-file.txt'))
expect(atom.project.getPaths()).toEqual([oldPath])
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
// Does add an entry for a directory within an existing directory
const newPath = path.join(oldPath, 'a-dir')
atom.project.addPath(newPath)
expect(atom.project.getPaths()).toEqual([oldPath, newPath])
expect(onDidChangePathsSpy).toHaveBeenCalled()
})
it("doesn't add non-existent directories", () => {
const previousPaths = atom.project.getPaths()
atom.project.addPath('/this-definitely/does-not-exist')
expect(atom.project.getPaths()).toEqual(previousPaths)
})
it('optionally throws on non-existent directories', () =>
expect(() => atom.project.addPath('/this-definitely/does-not-exist', {mustExist: true})).toThrow()
)
})
describe('.removePath(path)', () => {
let onDidChangePathsSpy = null
beforeEach(() => {
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener')
atom.project.onDidChangePaths(onDidChangePathsSpy)
})
it('removes the directory and repository for the path', () => {
const result = atom.project.removePath(atom.project.getPaths()[0])
expect(atom.project.getDirectories()).toEqual([])
expect(atom.project.getRepositories()).toEqual([])
expect(atom.project.getPaths()).toEqual([])
expect(result).toBe(true)
expect(onDidChangePathsSpy).toHaveBeenCalled()
})
it("does nothing if the path is not one of the project's root paths", () => {
const originalPaths = atom.project.getPaths()
const result = atom.project.removePath(originalPaths[0] + 'xyz')
expect(result).toBe(false)
expect(atom.project.getPaths()).toEqual(originalPaths)
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
})
it("doesn't destroy the repository if it is shared by another root directory", () => {
atom.project.setPaths([__dirname, path.join(__dirname, '..', 'src')])
atom.project.removePath(__dirname)
expect(atom.project.getPaths()).toEqual([path.join(__dirname, '..', 'src')])
expect(atom.project.getRepositories()[0].isSubmodule('src')).toBe(false)
})
it('removes a path that is represented as a URI', () => {
atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', {
directoryForURISync (uri) {
return {
getPath () { return uri },
getSubdirectory () { return {} },
isRoot () { return true },
existsSync () { return true },
off () {}
}
}
})
const ftpURI = 'ftp://example.com/some/folder'
atom.project.setPaths([ftpURI])
expect(atom.project.getPaths()).toEqual([ftpURI])
atom.project.removePath(ftpURI)
expect(atom.project.getPaths()).toEqual([])
})
})
describe('.onDidChangeFiles()', () => {
let sub = []
const events = []
let checkCallback = () => {}
beforeEach(() => {
sub = atom.project.onDidChangeFiles((incoming) => {
events.push(...incoming)
checkCallback()
})
})
afterEach(() => sub.dispose())
const waitForEvents = (paths) => {
const remaining = new Set(paths.map((p) => fs.realpathSync(p)))
return new Promise((resolve, reject) => {
checkCallback = () => {
for (let event of events) { remaining.delete(event.path) }
if (remaining.size === 0) { resolve() }
}
const expire = () => {
checkCallback = () => {}
console.error('Paths not seen:', remaining)
reject(new Error('Expired before all expected events were delivered.'))
}
checkCallback()
setTimeout(expire, 2000)
})
}
it('reports filesystem changes within project paths', () => {
const dirOne = temp.mkdirSync('atom-spec-project-one')
const fileOne = path.join(dirOne, 'file-one.txt')
const fileTwo = path.join(dirOne, 'file-two.txt')
const dirTwo = temp.mkdirSync('atom-spec-project-two')
const fileThree = path.join(dirTwo, 'file-three.txt')
// Ensure that all preexisting watchers are stopped
waitsForPromise(() => stopAllWatchers())
runs(() => atom.project.setPaths([dirOne]))
waitsForPromise(() => atom.project.getWatcherPromise(dirOne))
runs(() => {
expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual(undefined)
fs.writeFileSync(fileThree, 'three\n')
fs.writeFileSync(fileTwo, 'two\n')
fs.writeFileSync(fileOne, 'one\n')
})
waitsForPromise(() => waitForEvents([fileOne, fileTwo]))
runs(() => expect(events.some(event => event.path === fileThree)).toBeFalsy())
})
})
describe('.onDidAddBuffer()', () =>
it('invokes the callback with added text buffers', () => {
const buffers = []
const added = []
waitsForPromise(() =>
atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
.then(o => buffers.push(o))
)
runs(() => {
expect(buffers.length).toBe(1)
atom.project.onDidAddBuffer(buffer => added.push(buffer))
})
waitsForPromise(() =>
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
.then(o => buffers.push(o))
)
runs(() => {
expect(buffers.length).toBe(2)
expect(added).toEqual([buffers[1]])
})
})
)
describe('.observeBuffers()', () =>
it('invokes the observer with current and future text buffers', () => {
const buffers = []
const observed = []
waitsForPromise(() =>
atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
.then(o => buffers.push(o))
)
waitsForPromise(() =>
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
.then(o => buffers.push(o))
)
runs(() => {
expect(buffers.length).toBe(2)
atom.project.observeBuffers(buffer => observed.push(buffer))
expect(observed).toEqual(buffers)
})
waitsForPromise(() =>
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
.then(o => buffers.push(o))
)
runs(() => {
expect(observed.length).toBe(3)
expect(buffers.length).toBe(3)
expect(observed).toEqual(buffers)
})
})
)
describe('.relativize(path)', () => {
it('returns the path, relative to whichever root directory it is inside of', () => {
atom.project.addPath(temp.mkdirSync('another-path'))
let rootPath = atom.project.getPaths()[0]
let childPath = path.join(rootPath, 'some', 'child', 'directory')
expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory'))
rootPath = atom.project.getPaths()[1]
childPath = path.join(rootPath, 'some', 'child', 'directory')
expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory'))
})
it('returns the given path if it is not in any of the root directories', () => {
const randomPath = path.join('some', 'random', 'path')
expect(atom.project.relativize(randomPath)).toBe(randomPath)
})
})
describe('.relativizePath(path)', () => {
it('returns the root path that contains the given path, and the path relativized to that root path', () => {
atom.project.addPath(temp.mkdirSync('another-path'))
let rootPath = atom.project.getPaths()[0]
let childPath = path.join(rootPath, 'some', 'child', 'directory')
expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')])
rootPath = atom.project.getPaths()[1]
childPath = path.join(rootPath, 'some', 'child', 'directory')
expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')])
})
describe("when the given path isn't inside of any of the project's path", () =>
it('returns null for the root path, and the given path unchanged', () => {
const randomPath = path.join('some', 'random', 'path')
expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath])
})
)
describe('when the given path is a URL', () =>
it('returns null for the root path, and the given path unchanged', () => {
const url = 'http://the-path'
expect(atom.project.relativizePath(url)).toEqual([null, url])
})
)
describe('when the given path is inside more than one root folder', () =>
it('uses the root folder that is closest to the given path', () => {
atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir'))
const inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt')
expect(atom.project.getDirectories()[0].contains(inputPath)).toBe(true)
expect(atom.project.getDirectories()[1].contains(inputPath)).toBe(true)
expect(atom.project.relativizePath(inputPath)).toEqual([
atom.project.getPaths()[1],
path.join('somewhere', 'something.txt')
])
})
)
})
describe('.contains(path)', () =>
it('returns whether or not the given path is in one of the root directories', () => {
const rootPath = atom.project.getPaths()[0]
const childPath = path.join(rootPath, 'some', 'child', 'directory')
expect(atom.project.contains(childPath)).toBe(true)
const randomPath = path.join('some', 'random', 'path')
expect(atom.project.contains(randomPath)).toBe(false)
})
)
describe('.resolvePath(uri)', () =>
it('normalizes disk drive letter in passed path on #win32', () => {
expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')
})
)
})

View File

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

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

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

View File

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

View File

@@ -1896,6 +1896,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))
@@ -1926,12 +1928,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
@@ -4426,11 +4428,14 @@ describe('TextEditorComponent', () => {
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)
@@ -4446,6 +4451,17 @@ describe('TextEditorComponent', () => {
window.dispatchEvent(new MouseEvent('mousemove'))
await getNextAnimationFramePromise()
expect(dragging).toBe(false)
// Pressing a modifier key does not terminate dragging, (to ensure we can add new selections with the mouse)
startDragging()
window.dispatchEvent(new MouseEvent('mousemove'))
await getNextAnimationFramePromise()
expect(dragging).toBe(true)
component.didKeydown({key: 'Control'})
component.didKeydown({key: 'Alt'})
component.didKeydown({key: 'Shift'})
component.didKeydown({key: 'Meta'})
expect(dragging).toBe(true)
})
function getNextAnimationFramePromise () {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -0,0 +1,253 @@
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 = [element, element2]
fakeJqueryWrapper.jquery = 'any-version'
const disposable = manager.add(fakeJqueryWrapper, {title: 'Title'})
hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title'))
expect(document.body.querySelector('.tooltip')).toBeNull()
hover(element2, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title'))
expect(document.body.querySelector('.tooltip')).toBeNull()
disposable.dispose()
hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull())
hover(element2, () => expect(document.body.querySelector('.tooltip')).toBeNull())
})
describe('when a keyBindingCommand is specified', () => {
describe('when a title is specified', () =>
it('appends the key binding corresponding to the command to the title', () => {
atom.keymaps.add('test', {
'.foo': { 'ctrl-x ctrl-y': 'test-command'
},
'.bar': { 'ctrl-x ctrl-z': 'test-command'
}
}
)
manager.add(element, {title: 'Title', keyBindingCommand: 'test-command'})
hover(element, function () {
const tooltipElement = document.body.querySelector('.tooltip')
expect(tooltipElement).toHaveText(`Title ${ctrlX} ${ctrlY}`)
})
})
)
describe('when no title is specified', () =>
it('shows the key binding corresponding to the command alone', () => {
atom.keymaps.add('test', {'.foo': {'ctrl-x ctrl-y': 'test-command'}})
manager.add(element, {keyBindingCommand: 'test-command'})
hover(element, function () {
const tooltipElement = document.body.querySelector('.tooltip')
expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`)
})
})
)
describe('when a keyBindingTarget is specified', () => {
it('looks up the key binding relative to the target', () => {
atom.keymaps.add('test', {
'.bar': { 'ctrl-x ctrl-z': 'test-command'
},
'.foo': { 'ctrl-x ctrl-y': 'test-command'
}
}
)
manager.add(element, {keyBindingCommand: 'test-command', keyBindingTarget: element})
hover(element, function () {
const tooltipElement = document.body.querySelector('.tooltip')
expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`)
})
})
it('does not display the keybinding if there is nothing mapped to the specified keyBindingCommand', () => {
manager.add(element, {title: 'A Title', keyBindingCommand: 'test-command', keyBindingTarget: element})
hover(element, function () {
const tooltipElement = document.body.querySelector('.tooltip')
expect(tooltipElement.textContent).toBe('A Title')
})
})
})
})
describe('when .dispose() is called on the returned disposable', () =>
it('no longer displays the tooltip on hover', () => {
const disposable = manager.add(element, {title: 'Title'})
hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title'))
disposable.dispose()
hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull())
})
)
describe('when the window is resized', () =>
it('hides the tooltips', () => {
const disposable = manager.add(element, {title: 'Title'})
hover(element, function () {
expect(document.body.querySelector('.tooltip')).not.toBeNull()
window.dispatchEvent(new CustomEvent('resize'))
expect(document.body.querySelector('.tooltip')).toBeNull()
disposable.dispose()
})
})
)
describe('findTooltips', () => {
it('adds and remove tooltips correctly', () => {
expect(manager.findTooltips(element).length).toBe(0)
const disposable1 = manager.add(element, {title: 'elem1'})
expect(manager.findTooltips(element).length).toBe(1)
const disposable2 = manager.add(element, {title: 'elem2'})
expect(manager.findTooltips(element).length).toBe(2)
disposable1.dispose()
expect(manager.findTooltips(element).length).toBe(1)
disposable2.dispose()
expect(manager.findTooltips(element).length).toBe(0)
})
it('lets us hide tooltips programmatically', () => {
const disposable = manager.add(element, {title: 'Title'})
hover(element, function () {
expect(document.body.querySelector('.tooltip')).not.toBeNull()
manager.findTooltips(element)[0].hide()
expect(document.body.querySelector('.tooltip')).toBeNull()
disposable.dispose()
})
})
})
})
})
function createElement (className) {
const el = document.createElement('div')
el.classList.add(className)
jasmine.attachToDOM(el)
return el
}
function mouseEnter (element) {
element.dispatchEvent(new CustomEvent('mouseenter', {bubbles: false}))
element.dispatchEvent(new CustomEvent('mouseover', {bubbles: true}))
}
function mouseLeave (element) {
element.dispatchEvent(new CustomEvent('mouseleave', {bubbles: false}))
element.dispatchEvent(new CustomEvent('mouseout', {bubbles: true}))
}

View File

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

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

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

View File

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

View File

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

View File

@@ -1585,15 +1585,15 @@ i = /test/; #FIXME\
atom2.project.deserialize(atom.project.serialize())
atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers)
expect(atom2.grammars.getGrammars().map(grammar => grammar.name).sort()).toEqual([
'CoffeeScript',
'CoffeeScript (Literate)',
'JSDoc',
'JavaScript',
'Null Grammar',
'Regular Expression Replacement (JavaScript)',
'Regular Expressions (JavaScript)',
'TODO'
expect(atom2.grammars.getGrammars().map(grammar => grammar.scopeName).sort()).toEqual([
'source.coffee',
'source.js',
'source.js.regexp',
'source.js.regexp.replacement',
'source.jsdoc',
'source.litcoffee',
'text.plain.null-grammar',
'text.todo'
])
atom2.destroy()