/* globals assert */ const path = require('path') const {EventEmitter} = require('events') const temp = require('temp').track() const fs = require('fs-plus') const {sandbox} = require('sinon') const AtomApplication = require('../../src/main-process/atom-application') const parseCommandLine = require('../../src/main-process/parse-command-line') describe('AtomApplication', function () { let scenario, sinon beforeEach(async function () { sinon = sandbox.create() scenario = await LaunchScenario.create(sinon) }) afterEach(async function () { await scenario.destroy() sinon.restore() }) describe('command-line interface behavior', function () { describe('with no open windows', function () { // This is also the case when a user clicks on a file in their file manager it('opens a file', async function () { await scenario.open(parseCommandLine(['a/1.md'])) await scenario.assert('[_ 1.md]') }) // This is also the case when a user clicks on a folder in their file manager // (or, on macOS, drags the folder to Atom in their doc) it('opens a directory', async function () { await scenario.open(parseCommandLine(['a'])) await scenario.assert('[a _]') }) it('opens a file with --add', async function () { await scenario.open(parseCommandLine(['--add', 'a/1.md'])) await scenario.assert('[_ 1.md]') }) it('opens a directory with --add', async function () { await scenario.open(parseCommandLine(['--add', 'a'])) await scenario.assert('[a _]') }) it('opens a file with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'a/1.md'])) await scenario.assert('[_ 1.md]') }) it('opens a directory with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'a'])) await scenario.assert('[a _]') }) describe('with previous window state', function () { let app beforeEach(function () { app = scenario.addApplication({ applicationJson: [ { initialPaths: ['b'] }, { initialPaths: ['c'] } ] }) }) describe('with core.restorePreviousWindowsOnStart set to "no"', function () { beforeEach(function () { app.config.set('core.restorePreviousWindowsOnStart', 'no') }) it("doesn't restore windows when launched with no arguments", async function () { await scenario.launch({app}) await scenario.assert('[_ _]') }) it("doesn't restore windows when launched with paths to open", async function () { await scenario.launch({app, pathsToOpen: ['a/1.md']}) await scenario.assert('[_ 1.md]') }) it("doesn't restore windows when --new-window is provided", async function () { await scenario.launch({app, newWindow: true}) await scenario.assert('[_ _]') }) }) describe('with core.restorePreviousWindowsOnStart set to "yes"', function () { beforeEach(function () { app.config.set('core.restorePreviousWindowsOnStart', 'yes') }) it('restores windows when launched with no arguments', async function () { await scenario.launch({app}) await scenario.assert('[b _] [c _]') }) it("doesn't restore windows when launched with paths to open", async function () { await scenario.launch({app, pathsToOpen: ['a/1.md']}) await scenario.assert('[_ 1.md]') }) it("doesn't restore windows when --new-window is provided", async function () { await scenario.launch({app, newWindow: true}) await scenario.assert('[_ _]') }) }) describe('with core.restorePreviousWindowsOnStart set to "always"', function () { beforeEach(function () { app.config.set('core.restorePreviousWindowsOnStart', 'always') }) it('restores windows when launched with no arguments', async function () { await scenario.launch({app}) await scenario.assert('[b _] [c _]') }) it('restores windows when launched with paths to open', async function () { await scenario.launch({app, pathsToOpen: ['a']}) await scenario.assert('[a _] [b _] [c _]') }) it('collapses new paths into restored windows when appropriate', async function () { await scenario.launch({app, pathsToOpen: ['b/2.md']}) await scenario.assert('[b 2.md] [c _]') }) it("doesn't restore windows when --new-window is provided", async function () { await scenario.launch({app, newWindow: true}) await scenario.assert('[_ _]') }) it("doesn't restore windows on open, just launch", async function () { await scenario.launch({app, pathsToOpen: ['a'], newWindow: true}) await scenario.open(parseCommandLine(['b'])) await scenario.assert('[a _] [b _]') }) }) }) }) describe('with one empty window', function () { beforeEach(async function () { await scenario.preconditions('[_ _]') }) // This is also the case when a user clicks on a file in their file manager it('opens a file', async function () { await scenario.open(parseCommandLine(['a/1.md'])) await scenario.assert('[_ 1.md]') }) // This is also the case when a user clicks on a folder in their file manager it('opens a directory', async function () { await scenario.open(parseCommandLine(['a'])) await scenario.assert('[a _]') }) it('opens a file with --add', async function () { await scenario.open(parseCommandLine(['--add', 'a/1.md'])) await scenario.assert('[_ 1.md]') }) it('opens a directory with --add', async function () { await scenario.open(parseCommandLine(['--add', 'a'])) await scenario.assert('[a _]') }) it('opens a file with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'a/1.md'])) await scenario.assert('[_ _] [_ 1.md]') }) it('opens a directory with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'a'])) await scenario.assert('[_ _] [a _]') }) }) describe('with one window that has a project root', function () { beforeEach(async function () { await scenario.preconditions('[a _]') }) // This is also the case when a user clicks on a file within the project root in their file manager it('opens a file within the project root', async function () { await scenario.open(parseCommandLine(['a/1.md'])) await scenario.assert('[a 1.md]') }) // This is also the case when a user clicks on a project root folder in their file manager it('opens a directory that matches the project root', async function () { await scenario.open(parseCommandLine(['a'])) await scenario.assert('[a _]') }) // This is also the case when a user clicks on a file outside the project root in their file manager it('opens a file outside the project root', async function () { await scenario.open(parseCommandLine(['b/2.md'])) await scenario.assert('[a 2.md]') }) // This is also the case when a user clicks on a new folder in their file manager it('opens a directory other than the project root', async function () { await scenario.open(parseCommandLine(['b'])) await scenario.assert('[a _] [b _]') }) it('opens a file within the project root with --add', async function () { await scenario.open(parseCommandLine(['--add', 'a/1.md'])) await scenario.assert('[a 1.md]') }) it('opens a directory that matches the project root with --add', async function () { await scenario.open(parseCommandLine(['--add', 'a'])) await scenario.assert('[a _]') }) it('opens a file outside the project root with --add', async function () { await scenario.open(parseCommandLine(['--add', 'b/2.md'])) await scenario.assert('[a 2.md]') }) it('opens a directory other than the project root with --add', async function () { await scenario.open(parseCommandLine(['--add', 'b'])) await scenario.assert('[a,b _]') }) it('opens a file within the project root with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'a/1.md'])) await scenario.assert('[a _] [_ 1.md]') }) it('opens a directory that matches the project root with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'a'])) await scenario.assert('[a _] [a _]') }) it('opens a file outside the project root with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'b/2.md'])) await scenario.assert('[a _] [_ 2.md]') }) it('opens a directory other than the project root with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'b'])) await scenario.assert('[a _] [b _]') }) }) describe('with two windows, one with a project root and one empty', function () { beforeEach(async function () { await scenario.preconditions('[a _] [_ _]') }) // This is also the case when a user clicks on a file within the project root in their file manager it('opens a file within the project root', async function () { await scenario.open(parseCommandLine(['a/1.md'])) await scenario.assert('[a 1.md] [_ _]') }) // This is also the case when a user clicks on a project root folder in their file manager it('opens a directory that matches the project root', async function () { await scenario.open(parseCommandLine(['a'])) await scenario.assert('[a _] [_ _]') }) // This is also the case when a user clicks on a file outside the project root in their file manager it('opens a file outside the project root', async function () { await scenario.open(parseCommandLine(['b/2.md'])) await scenario.assert('[a _] [_ 2.md]') }) // This is also the case when a user clicks on a new folder in their file manager it('opens a directory other than the project root', async function () { await scenario.open(parseCommandLine(['b'])) await scenario.assert('[a _] [b _]') }) it('opens a file within the project root with --add', async function () { await scenario.open(parseCommandLine(['--add', 'a/1.md'])) await scenario.assert('[a 1.md] [_ _]') }) it('opens a directory that matches the project root with --add', async function () { await scenario.open(parseCommandLine(['--add', 'a'])) await scenario.assert('[a _] [_ _]') }) it('opens a file outside the project root with --add', async function () { await scenario.open(parseCommandLine(['--add', 'b/2.md'])) await scenario.assert('[a _] [_ 2.md]') }) it('opens a directory other than the project root with --add', async function () { await scenario.open(parseCommandLine(['--add', 'b'])) await scenario.assert('[a _] [b _]') }) it('opens a file within the project root with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'a/1.md'])) await scenario.assert('[a _] [_ _] [_ 1.md]') }) it('opens a directory that matches the project root with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'a'])) await scenario.assert('[a _] [_ _] [a _]') }) it('opens a file outside the project root with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'b/2.md'])) await scenario.assert('[a _] [_ _] [_ 2.md]') }) it('opens a directory other than the project root with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'b'])) await scenario.assert('[a _] [_ _] [b _]') }) }) describe('with two windows, one empty and one with a project root', function () { beforeEach(async function () { await scenario.preconditions('[_ _] [a _]') }) // This is also the case when a user clicks on a file within the project root in their file manager it('opens a file within the project root', async function () { await scenario.open(parseCommandLine(['a/1.md'])) await scenario.assert('[_ _] [a 1.md]') }) // This is also the case when a user clicks on a project root folder in their file manager it('opens a directory that matches the project root', async function () { await scenario.open(parseCommandLine(['a'])) await scenario.assert('[_ _] [a _]') }) // This is also the case when a user clicks on a file outside the project root in their file manager it('opens a file outside the project root', async function () { await scenario.open(parseCommandLine(['b/2.md'])) await scenario.assert('[_ 2.md] [a _]') }) // This is also the case when a user clicks on a new folder in their file manager it('opens a directory other than the project root', async function () { await scenario.open(parseCommandLine(['b'])) await scenario.assert('[b _] [a _]') }) it('opens a file within the project root with --add', async function () { await scenario.open(parseCommandLine(['--add', 'a/1.md'])) await scenario.assert('[_ _] [a 1.md]') }) it('opens a directory that matches the project root with --add', async function () { await scenario.open(parseCommandLine(['--add', 'a'])) await scenario.assert('[_ _] [a _]') }) it('opens a file outside the project root with --add', async function () { await scenario.open(parseCommandLine(['--add', 'b/2.md'])) await scenario.assert('[_ _] [a 2.md]') }) it('opens a directory other than the project root with --add', async function () { await scenario.open(parseCommandLine(['--add', 'b'])) await scenario.assert('[_ _] [a,b _]') }) it('opens a file within the project root with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'a/1.md'])) await scenario.assert('[_ _] [a _] [_ 1.md]') }) it('opens a directory that matches the project root with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'a'])) await scenario.assert('[_ _] [a _] [a _]') }) it('opens a file outside the project root with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'b/2.md'])) await scenario.assert('[_ _] [a _] [_ 2.md]') }) it('opens a directory other than the project root with --new-window', async function () { await scenario.open(parseCommandLine(['--new-window', 'b'])) await scenario.assert('[_ _] [a _] [b _]') }) }) }) }) class StubWindow extends EventEmitter { constructor (sinon, loadSettings, options) { super() this.loadSettings = loadSettings this._dimensions = {x: 100, y: 100} this._position = {x: 0, y: 0} this._locations = [] this._rootPaths = new Set() this._editorPaths = new Set() let resolveClosePromise this.closedPromise = new Promise(resolve => { resolveClosePromise = resolve }) this.minimize = sinon.spy() this.maximize = sinon.spy() this.center = sinon.spy() this.focus = sinon.spy() this.show = sinon.spy() this.hide = sinon.spy() this.prepareToUnload = sinon.spy() this.close = resolveClosePromise this.replaceEnvironment = sinon.spy() this.disableZoom = sinon.spy() this.isFocused = sinon.stub().returns(options.isFocused !== undefined ? options.isFocused : false) this.isMinimized = sinon.stub().returns(options.isMinimized !== undefined ? options.isMinimized : false) this.isMaximized = sinon.stub().returns(options.isMaximized !== undefined ? options.isMaximized : false) this.sendURIMessage = sinon.spy() this.didChangeUserSettings = sinon.spy() this.didFailToReadUserSettings = sinon.spy() this.isSpec = loadSettings.isSpec !== undefined ? loadSettings.isSpec : false this.devMode = loadSettings.devMode !== undefined ? loadSettings.devMode : false this.safeMode = loadSettings.safeMode !== undefined ? loadSettings.safeMode : false this.browserWindow = new EventEmitter() this.browserWindow.webContents = new EventEmitter() const {locationsToOpen} = this.loadSettings if (!(locationsToOpen.length === 1 && locationsToOpen[0].pathToOpen == null) && !this.isSpec) { this.openLocations(locationsToOpen) } } openPath (pathToOpen, initialLine, initialColumn) { return this.openLocations([{pathToOpen, initialLine, initialColumn}]) } openLocations (locations) { this._locations.push(...locations) for (const location of locations) { if (location.pathToOpen) { if (location.isDirectory) { this._rootPaths.add(location.pathToOpen) } else if (location.isFile) { this._editorPaths.add(location.pathToOpen) } } } this.emit('window:locations-opened') } setSize (x, y) { this._dimensions = {x, y} } setPosition (x, y) { this._position = {x, y} } hasProjectPaths () { return this._rootPaths.size > 0 } containsLocations (locations) { return locations.every(location => this.containsLocation(location)) } containsLocation (location) { if (!location.pathToOpen) return false return Array.from(this._rootPaths).some(projectPath => { if (location.pathToOpen === projectPath) return true if (location.pathToOpen.startsWith(path.join(projectPath, path.sep))) { if (!location.exists) return true if (!location.isDirectory) return true } return false }) } getDimensions () { return this._dimensions } } class LaunchScenario { static async create (sandbox) { const scenario = new this(sandbox) await scenario.init() return scenario } constructor (sandbox) { this.sinon = sandbox this.applications = new Set() this.windows = new Set() this.root = null this.projectRootPool = new Map() this.filePathPool = new Map() } async init () { if (this.root !== null) { return this.root } this.root = await new Promise((resolve, reject) => { temp.mkdir('launch-', (err, rootPath) => { if (err) { reject(err) } else { resolve(rootPath) } }) }) await Promise.all( ['a', 'b', 'c'].map(dirPath => new Promise((resolve, reject) => { const fullDirPath = path.join(this.root, dirPath) fs.makeTree(fullDirPath, err => { if (err) { reject(err) } else { this.projectRootPool.set(dirPath, fullDirPath) resolve() } }) })) ) await Promise.all( ['a/1.md', 'b/2.md'].map(filePath => new Promise((resolve, reject) => { const fullFilePath = path.join(this.root, filePath) fs.writeFile(fullFilePath, `file: ${filePath}\n`, {encoding: 'utf8'}, err => { if (err) { reject(err) } else { this.filePathPool.set(filePath, fullFilePath) this.filePathPool.set(path.basename(filePath), fullFilePath) resolve() } }) })) ) } async preconditions (source) { const app = this.addApplication() const windowPromises = [] for (const windowSpec of this.parseWindowSpecs(source)) { if (windowSpec.editors.length === 0) { windowSpec.editors.push(null) } windowPromises.push((async (theApp, foldersToOpen, pathsToOpen) => { const window = await theApp.openPaths({ newWindow: true, foldersToOpen, pathsToOpen }) this.windows.add(window) return window })(app, windowSpec.roots, windowSpec.editors)) } await Promise.all(windowPromises) } async launch (options) { const app = options.app || this.addApplication() delete options.app if (options.pathsToOpen) { options.pathsToOpen = this.convertPaths(options.pathsToOpen) } const windows = await app.launch(options) for (const window of windows) { this.windows.add(window) } return windows } async open (options) { if (this.applications.size === 0) { return this.launch(options) } let app = options.app if (!app) { const apps = Array.from(this.applications) app = apps[apps.length - 1] } else { delete options.app } if (options.pathsToOpen) { options.pathsToOpen = this.convertPaths(options.pathsToOpen) } options.preserveFocus = true const window = await app.openWithOptions(options) this.windows.add(window) return window } async assert (source) { const windowSpecs = this.parseWindowSpecs(source) let specIndex = 0 const windowPromises = [] for (const window of this.windows) { windowPromises.push((async (theWindow, theSpec) => { const {_rootPaths: rootPaths, _editorPaths: editorPaths} = theWindow const comparison = { ok: true, extraWindow: false, missingWindow: false, extraRoots: [], missingRoots: [], extraEditors: [], missingEditors: [], roots: rootPaths, editors: editorPaths } if (!theSpec) { comparison.ok = false comparison.extraWindow = true comparison.extraRoots = rootPaths comparison.extraEditors = editorPaths } else { const [missingRoots, extraRoots] = this.compareSets(theSpec.roots, rootPaths) const [missingEditors, extraEditors] = this.compareSets(theSpec.editors, editorPaths) comparison.ok = missingRoots.length === 0 && extraRoots.length === 0 && missingEditors.length === 0 && extraEditors.length === 0 comparison.extraRoots = extraRoots comparison.missingRoots = missingRoots comparison.extraEditors = extraEditors comparison.missingEditors = missingEditors } return comparison })(window, windowSpecs[specIndex++])) } const comparisons = await Promise.all(windowPromises) for (; specIndex < windowSpecs.length; specIndex++) { const spec = windowSpecs[specIndex] comparisons.push({ ok: false, extraWindow: false, missingWindow: true, extraRoots: [], missingRoots: spec.roots, extraEditors: [], missingEditors: spec.editors, roots: [], editors: [] }) } const shorthandParts = [] const descriptionParts = [] for (const comparison of comparisons) { const shortRoots = Array.from(comparison.roots, r => path.basename(r)).join(',') const shortPaths = Array.from(comparison.editors, e => path.basename(e)).join(',') shorthandParts.push(`[${shortRoots} ${shortPaths}]`) if (comparison.ok) { continue } let parts = [] if (comparison.extraWindow) { parts.push('extra window\n') } else if (comparison.missingWindow) { parts.push('missing window\n') } else { parts.push('incorrect window\n') } const shorten = fullPaths => fullPaths.map(fullPath => path.basename(fullPath)).join(', ') if (comparison.extraRoots.length > 0) { parts.push(`* extra roots ${shorten(comparison.extraRoots)}\n`) } if (comparison.missingRoots.length > 0) { parts.push(`* missing roots ${shorten(comparison.missingRoots)}\n`) } if (comparison.extraEditors.length > 0) { parts.push(`* extra editors ${shorten(comparison.extraEditors)}\n`) } if (comparison.missingEditors.length > 0) { parts.push(`* missing editors ${shorten(comparison.missingEditors)}\n`) } descriptionParts.push(parts.join('')) } if (descriptionParts.length !== 0) { descriptionParts.unshift(shorthandParts.join(' ') + '\n') descriptionParts.unshift('Launched windows did not match spec\n') } assert.isTrue(descriptionParts.length === 0, descriptionParts.join('')) } async destroy () { await Promise.all( Array.from(this.applications, app => app.destroy()) ) } addApplication (options = {}) { const app = new AtomApplication({ resourcePath: path.resolve(__dirname, '../..'), atomHomeDirPath: this.atomHome, preserveFocus: true, ...options }) this.sinon.stub(app, 'createWindow', loadSettings => new StubWindow(this.sinon, loadSettings, options)) this.sinon.stub(app.storageFolder, 'load', () => Promise.resolve( (options.applicationJson || []).map(each => ({ initialPaths: this.convertPaths(each.initialPaths) })) )) this.applications.add(app) return app } getApplication (index) { const app = Array.from(this.applications)[index] if (!app) { throw new Error(`Application ${index} does not exist`) } return app } getWindow (index) { const window = Array.from(this.windows)[index] if (!window) { throw new Error(`Window ${index} does not exist`) } return window } compareSets (expected, actual) { const expectedItems = new Set(expected) const extra = [] const missing = [] for (const actualItem of actual) { if (!expectedItems.delete(actualItem)) { // actualItem was present, but not expected extra.push(actualItem) } } for (const remainingItem of expectedItems) { // remainingItem was expected, but not present missing.push(remainingItem) } return [missing, extra] } convertRootPath (shortRootPath) { const fullRootPath = this.projectRootPool.get(shortRootPath) if (!fullRootPath) { throw new Error(`Unexpected short project root path: ${shortRootPath}`) } return fullRootPath } convertEditorPath (shortEditorPath) { const fullEditorPath = this.filePathPool.get(shortEditorPath) if (!fullEditorPath) { throw new Error(`Unexpected short editor path: ${shortEditorPath}`) } return fullEditorPath } convertPaths (paths) { return paths.map(shortPath => { const fullRoot = this.projectRootPool.get(shortPath) if (fullRoot) { return fullRoot } const fullEditor = this.filePathPool.get(shortPath) if (fullEditor) { return fullEditor } throw new Error(`Unexpected short path: ${shortPath}`) }) } parseWindowSpecs (source) { const specs = [] const rx = /\s*\[(?:_|(\S+)) (?:_|(\S+))\]/g let match = rx.exec(source) while (match) { const roots = match[1] ? match[1].split(',').map(shortPath => this.convertRootPath(shortPath)) : [] const editors = match[2] ? match[2].split(',').map(shortPath => this.convertEditorPath(shortPath)) : [] specs.push({ roots, editors }) match = rx.exec(source) } return specs } }