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