Merge branch 'master' into mb-use-language-mode-api

This commit is contained in:
Max Brunsfeld
2017-11-06 11:32:52 -08:00
23 changed files with 1244 additions and 989 deletions

View File

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

View File

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

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

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

View File

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

View File

@@ -385,9 +385,14 @@ describe('Project', () => {
isRoot () { return true }
existsSync () { return this.path.endsWith('does-exist') }
contains (filePath) { return filePath.startsWith(this.path) }
onDidChangeFiles (callback) {
onDidChangeFilesCallback = callback
return {dispose: () => {}}
}
}
let serviceDisposable = null
let onDidChangeFilesCallback = null
beforeEach(() => {
serviceDisposable = atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', {
@@ -399,6 +404,7 @@ describe('Project', () => {
}
}
})
onDidChangeFilesCallback = null
waitsFor(() => atom.project.directoryProviders.length > 0)
})
@@ -433,6 +439,28 @@ describe('Project', () => {
atom.project.setPaths(['ssh://foreign-directory:8080/does-exist'])
expect(atom.project.getDirectories().length).toBe(0)
})
it('uses the custom onDidChangeFiles as the watcher if available', () => {
// Ensure that all preexisting watchers are stopped
waitsForPromise(() => stopAllWatchers())
const remotePath = 'ssh://another-directory:8080/does-exist'
runs(() => atom.project.setPaths([remotePath]))
waitsForPromise(() => atom.project.getWatcherPromise(remotePath))
runs(() => {
expect(onDidChangeFilesCallback).not.toBeNull()
const changeSpy = jasmine.createSpy('atom.project.onDidChangeFiles')
const disposable = atom.project.onDidChangeFiles(changeSpy)
const events = [{action: 'created', path: remotePath + '/test.txt'}]
onDidChangeFilesCallback(events)
expect(changeSpy).toHaveBeenCalledWith(events)
disposable.dispose()
})
})
})
describe('.open(path)', () => {

View File

@@ -2007,13 +2007,17 @@ describe('TextEditor', () => {
describe('when the cursor is between two words', () => {
it('selects the word the cursor is on', () => {
editor.setCursorScreenPosition([0, 4])
editor.setCursorBufferPosition([0, 4])
editor.selectWordsContainingCursors()
expect(editor.getSelectedText()).toBe('quicksort')
editor.setCursorScreenPosition([0, 3])
editor.setCursorBufferPosition([0, 3])
editor.selectWordsContainingCursors()
expect(editor.getSelectedText()).toBe('var')
editor.setCursorBufferPosition([1, 22])
editor.selectWordsContainingCursors()
expect(editor.getSelectedText()).toBe('items')
})
})

View File

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

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

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

View File

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

View File

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