mirror of https://github.com/atom/atom.git
1749 lines
58 KiB
JavaScript
1749 lines
58 KiB
JavaScript
/* globals assert */
|
|
|
|
const path = require('path');
|
|
const { EventEmitter } = require('events');
|
|
const temp = require('temp').track();
|
|
const fs = require('fs-plus');
|
|
const electron = require('electron');
|
|
const { sandbox } = require('sinon');
|
|
|
|
const AtomApplication = require('../../src/main-process/atom-application');
|
|
const parseCommandLine = require('../../src/main-process/parse-command-line');
|
|
const {
|
|
emitterEventPromise,
|
|
conditionPromise
|
|
} = require('../async-spec-helpers');
|
|
|
|
// These tests use a utility class called LaunchScenario, defined below, to manipulate AtomApplication instances that
|
|
// (1) are stubbed to only simulate AtomWindow creation and (2) allow you to use a shorthand notation to assert the
|
|
// application state after certain launch actions.
|
|
//
|
|
// Each scenario instance has access to a small set of directories and files created within a dedicated temporary
|
|
// directory. For convenience, you may use short names to refer to any of its contents (their basenames, basically).
|
|
// Check `LaunchScenario::init()` to see what directories and files are available.
|
|
//
|
|
// To create an application and its first window, call `await scenario.launch({})`. "Launch" may open multiple windows,
|
|
// so it returns a Promise that resolves to an array of StubWindows. Its options argument may be created by
|
|
// `parseCommandLine()` from a simulated argv string, or built by hand to include `{pathsToOpen}` and so on.
|
|
//
|
|
// To create additional windows, call `await scenario.open({})` with similar arguments. `LaunchScenario::open()` returns
|
|
// a Promise that resolves to the opened or re-used StubWindows. The one exception is if `urlsToOpen` are provided in the open
|
|
// arguments; then it resolves to an Array of StubWindows, because AtomApplication processes each URL individually.
|
|
//
|
|
// To ensure that the expected windows have been created, call `await scenario.assert('')` with a string specifying the
|
|
// expected window contents. The specification shorthand language is as follows:
|
|
//
|
|
// * '[_ _]' describes a single window with no project roots and no open editors.
|
|
// * '[_ 1.md]' describes a single window with no project roots and a single editor open on the file `./a/1.md` within
|
|
// the LaunchScenario temporary directory.
|
|
// * '[a _]' describes a single window with one project root - the directory `./a` within the LaunchScenario temporary
|
|
// directory - and no open editors.
|
|
// * '[a,b 1.md,2.md]' describes a single window with two project roots - the directories `./a` and `./b` - and two
|
|
// open editors - `./a/1.md` and `./b/2.md`.
|
|
// * '[a _] [b,c 2.md]' describes two windows, one with a project root of `./a` and no open editors, and another with
|
|
// two project roots, `./b` and `./c`, and one open editor on `./b/2.md`. The windows are listed in their expected
|
|
// creation order.
|
|
|
|
describe('AtomApplication', function() {
|
|
let scenario, sinon;
|
|
|
|
if (process.env.CI) {
|
|
this.timeout(10 * 1000);
|
|
}
|
|
|
|
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 selects the application from the OS shell
|
|
it('opens an empty window', async function() {
|
|
await scenario.launch(parseCommandLine([]));
|
|
await scenario.assert('[_ _]');
|
|
});
|
|
|
|
// 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: {
|
|
version: '1',
|
|
windows: [
|
|
{ projectRoots: [scenario.convertRootPath('b')] },
|
|
{ projectRoots: [scenario.convertRootPath('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 a project path to open', async function() {
|
|
await scenario.launch({ app, pathsToOpen: ['a'] });
|
|
await scenario.assert('[b _] [c _] [a _]');
|
|
});
|
|
|
|
it('restores windows when launched with a file path to open', async function() {
|
|
await scenario.launch({ app, pathsToOpen: ['a/1.md'] });
|
|
await scenario.assert('[b _] [c 1.md]');
|
|
});
|
|
|
|
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 unversioned application state', function() {
|
|
it('reads "initialPaths" as project roots', async function() {
|
|
const app = scenario.addApplication({
|
|
applicationJson: [
|
|
{ initialPaths: [scenario.convertRootPath('a')] },
|
|
{
|
|
initialPaths: [
|
|
scenario.convertRootPath('b'),
|
|
scenario.convertRootPath('c')
|
|
]
|
|
}
|
|
]
|
|
});
|
|
app.config.set('core.restorePreviousWindowsOnStart', 'always');
|
|
|
|
await scenario.launch({ app });
|
|
await scenario.assert('[a _] [b,c _]');
|
|
});
|
|
|
|
it('filters file paths from project root lists', async function() {
|
|
const app = scenario.addApplication({
|
|
applicationJson: [
|
|
{
|
|
initialPaths: [
|
|
scenario.convertRootPath('b'),
|
|
scenario.convertEditorPath('a/1.md')
|
|
]
|
|
}
|
|
]
|
|
});
|
|
app.config.set('core.restorePreviousWindowsOnStart', 'always');
|
|
|
|
await scenario.launch({ app });
|
|
await scenario.assert('[b _]');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('with one empty window', function() {
|
|
beforeEach(async function() {
|
|
await scenario.preconditions('[_ _]');
|
|
});
|
|
|
|
// This is also the case when a user selects the application from the OS shell
|
|
it('opens a new, empty window', async function() {
|
|
await scenario.open(parseCommandLine([]));
|
|
await scenario.assert('[_ _] [_ _]');
|
|
});
|
|
|
|
// 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 selects the application from the OS shell
|
|
it('opens a new, empty window', async function() {
|
|
await scenario.open(parseCommandLine([]));
|
|
await scenario.assert('[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 selects the application from the OS shell
|
|
it('opens a new, empty window', async function() {
|
|
await scenario.open(parseCommandLine([]));
|
|
await scenario.assert('[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 selects the application from the OS shell
|
|
it('opens a new, empty window', async function() {
|
|
await scenario.open(parseCommandLine([]));
|
|
await scenario.assert('[_ _] [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 _]');
|
|
});
|
|
});
|
|
|
|
describe('--wait', function() {
|
|
it('kills the specified pid after a newly-opened window is closed', async function() {
|
|
const [w0] = await scenario.launch(
|
|
parseCommandLine(['--new-window', '--wait', '--pid', '101'])
|
|
);
|
|
const w1 = await scenario.open(
|
|
parseCommandLine(['--new-window', '--wait', '--pid', '202'])
|
|
);
|
|
|
|
assert.lengthOf(scenario.killedPids, 0);
|
|
|
|
w0.browserWindow.emit('closed');
|
|
assert.deepEqual(scenario.killedPids, [101]);
|
|
|
|
w1.browserWindow.emit('closed');
|
|
assert.deepEqual(scenario.killedPids, [101, 202]);
|
|
});
|
|
|
|
it('kills the specified pid after all newly-opened files in an existing window are closed', async function() {
|
|
const [w] = await scenario.launch(
|
|
parseCommandLine(['--new-window', 'a'])
|
|
);
|
|
await scenario.open(
|
|
parseCommandLine([
|
|
'--add',
|
|
'--wait',
|
|
'--pid',
|
|
'303',
|
|
'a/1.md',
|
|
'b/2.md'
|
|
])
|
|
);
|
|
await scenario.assert('[a 1.md,2.md]');
|
|
|
|
assert.lengthOf(scenario.killedPids, 0);
|
|
|
|
scenario
|
|
.getApplication(0)
|
|
.windowDidClosePathWithWaitSession(
|
|
w,
|
|
scenario.convertEditorPath('b/2.md')
|
|
);
|
|
assert.lengthOf(scenario.killedPids, 0);
|
|
scenario
|
|
.getApplication(0)
|
|
.windowDidClosePathWithWaitSession(
|
|
w,
|
|
scenario.convertEditorPath('a/1.md')
|
|
);
|
|
assert.deepEqual(scenario.killedPids, [303]);
|
|
});
|
|
|
|
it('kills the specified pid after a newly-opened directory in an existing window is closed', async function() {
|
|
const [w] = await scenario.launch(
|
|
parseCommandLine(['--new-window', 'a'])
|
|
);
|
|
await scenario.open(
|
|
parseCommandLine(['--add', '--wait', '--pid', '404', 'b'])
|
|
);
|
|
await scenario.assert('[a,b _]');
|
|
|
|
assert.lengthOf(scenario.killedPids, 0);
|
|
|
|
scenario
|
|
.getApplication(0)
|
|
.windowDidClosePathWithWaitSession(w, scenario.convertRootPath('b'));
|
|
assert.deepEqual(scenario.killedPids, [404]);
|
|
});
|
|
});
|
|
|
|
describe('atom:// URLs', function() {
|
|
describe('with a package-name host', function() {
|
|
it("loads the package's urlMain in a new window", async function() {
|
|
await scenario.launch({});
|
|
|
|
const app = scenario.getApplication(0);
|
|
app.packages = {
|
|
getAvailablePackageMetadata: () => [
|
|
{ name: 'package-with-url-main', urlMain: 'some/url-main' }
|
|
],
|
|
resolvePackagePath: () =>
|
|
path.resolve('dot-atom/package-with-url-main')
|
|
};
|
|
|
|
const [w1, w2] = await scenario.open(
|
|
parseCommandLine([
|
|
'atom://package-with-url-main/test1',
|
|
'atom://package-with-url-main/test2'
|
|
])
|
|
);
|
|
|
|
assert.strictEqual(
|
|
w1.loadSettings.windowInitializationScript,
|
|
path.resolve('dot-atom/package-with-url-main/some/url-main')
|
|
);
|
|
assert.strictEqual(
|
|
w1.loadSettings.urlToOpen,
|
|
'atom://package-with-url-main/test1'
|
|
);
|
|
|
|
assert.strictEqual(
|
|
w2.loadSettings.windowInitializationScript,
|
|
path.resolve('dot-atom/package-with-url-main/some/url-main')
|
|
);
|
|
assert.strictEqual(
|
|
w2.loadSettings.urlToOpen,
|
|
'atom://package-with-url-main/test2'
|
|
);
|
|
});
|
|
|
|
it('sends a URI message to the most recently focused non-spec window', async function() {
|
|
const [w0] = await scenario.launch({});
|
|
const w1 = await scenario.open(parseCommandLine(['--new-window']));
|
|
const w2 = await scenario.open(parseCommandLine(['--new-window']));
|
|
const w3 = await scenario.open(
|
|
parseCommandLine(['--test', 'a/1.md'])
|
|
);
|
|
|
|
const app = scenario.getApplication(0);
|
|
app.packages = {
|
|
getAvailablePackageMetadata: () => []
|
|
};
|
|
|
|
const [uw] = await scenario.open(
|
|
parseCommandLine(['atom://package-without-url-main/test'])
|
|
);
|
|
assert.strictEqual(uw, w2);
|
|
|
|
assert.isTrue(
|
|
w2.sendURIMessage.calledWith('atom://package-without-url-main/test')
|
|
);
|
|
assert.strictEqual(w2.focus.callCount, 2);
|
|
|
|
for (const other of [w0, w1, w3]) {
|
|
assert.isFalse(other.sendURIMessage.called);
|
|
}
|
|
});
|
|
|
|
it('creates a new window and sends a URI message to it once it loads', async function() {
|
|
const [w0] = await scenario.launch(
|
|
parseCommandLine(['--test', 'a/1.md'])
|
|
);
|
|
|
|
const app = scenario.getApplication(0);
|
|
app.packages = {
|
|
getAvailablePackageMetadata: () => []
|
|
};
|
|
|
|
const [uw] = await scenario.open(
|
|
parseCommandLine(['atom://package-without-url-main/test'])
|
|
);
|
|
assert.notStrictEqual(uw, w0);
|
|
assert.strictEqual(
|
|
uw.loadSettings.windowInitializationScript,
|
|
path.resolve(
|
|
__dirname,
|
|
'../../src/initialize-application-window.js'
|
|
)
|
|
);
|
|
|
|
uw.emit('window:loaded');
|
|
assert.isTrue(
|
|
uw.sendURIMessage.calledWith('atom://package-without-url-main/test')
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('with a "core" host', function() {
|
|
it('sends a URI message to the most recently focused non-spec window that owns the open locations', async function() {
|
|
const [w0] = await scenario.launch(parseCommandLine(['a']));
|
|
const w1 = await scenario.open(
|
|
parseCommandLine(['--new-window', 'a'])
|
|
);
|
|
const w2 = await scenario.open(
|
|
parseCommandLine(['--new-window', 'b'])
|
|
);
|
|
|
|
const uri = `atom://core/open/file?filename=${encodeURIComponent(
|
|
scenario.convertEditorPath('a/1.md')
|
|
)}`;
|
|
const [uw] = await scenario.open(parseCommandLine([uri]));
|
|
assert.strictEqual(uw, w1);
|
|
assert.isTrue(w1.sendURIMessage.calledWith(uri));
|
|
|
|
for (const other of [w0, w2]) {
|
|
assert.isFalse(other.sendURIMessage.called);
|
|
}
|
|
});
|
|
|
|
it('creates a new window and sends a URI message to it once it loads', async function() {
|
|
const [w0] = await scenario.launch(
|
|
parseCommandLine(['--test', 'a/1.md'])
|
|
);
|
|
|
|
const uri = `atom://core/open/file?filename=${encodeURIComponent(
|
|
scenario.convertEditorPath('b/2.md')
|
|
)}`;
|
|
const [uw] = await scenario.open(parseCommandLine([uri]));
|
|
assert.notStrictEqual(uw, w0);
|
|
|
|
uw.emit('window:loaded');
|
|
assert.isTrue(uw.sendURIMessage.calledWith(uri));
|
|
});
|
|
});
|
|
});
|
|
|
|
it('opens a file to a specific line number', async function() {
|
|
await scenario.open(parseCommandLine(['a/1.md:10']));
|
|
await scenario.assert('[_ 1.md]');
|
|
|
|
const w = scenario.getWindow(0);
|
|
assert.lengthOf(w._locations, 1);
|
|
assert.strictEqual(w._locations[0].initialLine, 9);
|
|
assert.isNull(w._locations[0].initialColumn);
|
|
});
|
|
|
|
it('opens a file to a specific line number and column', async function() {
|
|
await scenario.open(parseCommandLine('b/2.md:12:5'));
|
|
await scenario.assert('[_ 2.md]');
|
|
|
|
const w = scenario.getWindow(0);
|
|
assert.lengthOf(w._locations, 1);
|
|
assert.strictEqual(w._locations[0].initialLine, 11);
|
|
assert.strictEqual(w._locations[0].initialColumn, 4);
|
|
});
|
|
|
|
it('opens a directory with a non-file protocol', async function() {
|
|
await scenario.open(
|
|
parseCommandLine(['remote://server:3437/some/directory/path'])
|
|
);
|
|
|
|
const w = scenario.getWindow(0);
|
|
assert.lengthOf(w._locations, 1);
|
|
assert.strictEqual(
|
|
w._locations[0].pathToOpen,
|
|
'remote://server:3437/some/directory/path'
|
|
);
|
|
assert.isFalse(w._locations[0].exists);
|
|
assert.isFalse(w._locations[0].isDirectory);
|
|
assert.isFalse(w._locations[0].isFile);
|
|
});
|
|
|
|
it('truncates trailing whitespace and colons', async function() {
|
|
await scenario.open(parseCommandLine('b/2.md:: '));
|
|
await scenario.assert('[_ 2.md]');
|
|
|
|
const w = scenario.getWindow(0);
|
|
assert.lengthOf(w._locations, 1);
|
|
assert.isNull(w._locations[0].initialLine);
|
|
assert.isNull(w._locations[0].initialColumn);
|
|
});
|
|
|
|
it('disregards test and benchmark windows', async function() {
|
|
await scenario.launch(parseCommandLine(['--test', 'b']));
|
|
await scenario.open(parseCommandLine(['--new-window']));
|
|
await scenario.open(parseCommandLine(['--test', 'c']));
|
|
await scenario.open(parseCommandLine(['--benchmark', 'b']));
|
|
|
|
await scenario.open(parseCommandLine(['a/1.md']));
|
|
|
|
// Test and benchmark StubWindows are visible as empty editor windows here
|
|
await scenario.assert('[_ _] [_ 1.md] [_ _] [_ _]');
|
|
});
|
|
});
|
|
|
|
if (process.platform === 'darwin' || process.platform === 'win32') {
|
|
it('positions new windows at an offset from the previous window', async function() {
|
|
const [w0] = await scenario.launch(parseCommandLine(['a']));
|
|
w0.setSize(400, 400);
|
|
const d0 = w0.getDimensions();
|
|
|
|
const w1 = await scenario.open(parseCommandLine(['b']));
|
|
const d1 = w1.getDimensions();
|
|
|
|
assert.isAbove(d1.x, d0.x);
|
|
assert.isAbove(d1.y, d0.y);
|
|
});
|
|
}
|
|
|
|
if (process.platform === 'darwin') {
|
|
describe('with no windows open', function() {
|
|
let app;
|
|
|
|
beforeEach(async function() {
|
|
const [w] = await scenario.launch(parseCommandLine([]));
|
|
|
|
app = scenario.getApplication(0);
|
|
app.removeWindow(w);
|
|
sinon.stub(app, 'promptForPathToOpen');
|
|
});
|
|
|
|
it('opens a new file', function() {
|
|
app.emit('application:open-file');
|
|
assert.isTrue(
|
|
app.promptForPathToOpen.calledWith('file', {
|
|
devMode: false,
|
|
safeMode: false,
|
|
window: null
|
|
})
|
|
);
|
|
});
|
|
|
|
it('opens a new directory', function() {
|
|
app.emit('application:open-folder');
|
|
assert.isTrue(
|
|
app.promptForPathToOpen.calledWith('folder', {
|
|
devMode: false,
|
|
safeMode: false,
|
|
window: null
|
|
})
|
|
);
|
|
});
|
|
|
|
it('opens a new file or directory', function() {
|
|
app.emit('application:open');
|
|
assert.isTrue(
|
|
app.promptForPathToOpen.calledWith('all', {
|
|
devMode: false,
|
|
safeMode: false,
|
|
window: null
|
|
})
|
|
);
|
|
});
|
|
|
|
it('reopens a project in a new window', async function() {
|
|
const paths = scenario.convertPaths(['a', 'b']);
|
|
app.emit('application:reopen-project', { paths });
|
|
|
|
await conditionPromise(() => app.getAllWindows().length > 0);
|
|
|
|
assert.deepEqual(
|
|
app.getAllWindows().map(w => Array.from(w._rootPaths)),
|
|
[paths]
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
describe('existing application re-use', function() {
|
|
let createApplication;
|
|
|
|
const version = electron.app.getVersion();
|
|
|
|
beforeEach(function() {
|
|
createApplication = async options => {
|
|
options.version = version;
|
|
|
|
const app = scenario.addApplication(options);
|
|
await app.listenForArgumentsFromNewProcess(options);
|
|
await app.launch(options);
|
|
return app;
|
|
};
|
|
});
|
|
|
|
it('creates a new application when no socket is present', async function() {
|
|
const app0 = await AtomApplication.open({ createApplication, version });
|
|
await app0.deleteSocketSecretFile();
|
|
|
|
const app1 = await AtomApplication.open({ createApplication, version });
|
|
assert.isNotNull(app1);
|
|
assert.notStrictEqual(app0, app1);
|
|
});
|
|
|
|
it('creates a new application for spec windows', async function() {
|
|
const app0 = await AtomApplication.open({ createApplication, version });
|
|
|
|
const app1 = await AtomApplication.open({
|
|
createApplication,
|
|
version,
|
|
...parseCommandLine(['--test', 'a'])
|
|
});
|
|
assert.isNotNull(app1);
|
|
assert.notStrictEqual(app0, app1);
|
|
});
|
|
|
|
it('sends a request to an existing application when a socket is present', async function() {
|
|
const app0 = await AtomApplication.open({ createApplication, version });
|
|
assert.lengthOf(app0.getAllWindows(), 1);
|
|
|
|
const app1 = await AtomApplication.open({
|
|
createApplication,
|
|
version,
|
|
...parseCommandLine(['--new-window'])
|
|
});
|
|
assert.isNull(app1);
|
|
assert.isTrue(electron.app.quit.called);
|
|
|
|
await conditionPromise(() => app0.getAllWindows().length === 2);
|
|
await scenario.assert('[_ _] [_ _]');
|
|
});
|
|
});
|
|
|
|
describe('IPC handling', function() {
|
|
let w0, w1, w2, app;
|
|
|
|
beforeEach(async function() {
|
|
w0 = (await scenario.launch(parseCommandLine(['a'])))[0];
|
|
w1 = await scenario.open(parseCommandLine(['--new-window']));
|
|
w2 = await scenario.open(parseCommandLine(['--new-window', 'b']));
|
|
|
|
app = scenario.getApplication(0);
|
|
sinon.spy(app, 'openPaths');
|
|
sinon.stub(app, 'promptForPath', (_type, callback, defaultPath) =>
|
|
callback([defaultPath])
|
|
);
|
|
});
|
|
|
|
// This is the IPC message used to handle:
|
|
// * application:reopen-project
|
|
// * choosing "open in new window" when adding a folder that has previously saved state
|
|
// * drag and drop
|
|
// * deprecated call links in deprecation-cop
|
|
// * other direct callers of `atom.open()`
|
|
it('"open" opens a fixed path by the standard opening rules', async function() {
|
|
sinon.stub(app, 'atomWindowForEvent', () => w1);
|
|
|
|
electron.ipcMain.emit(
|
|
'open',
|
|
{},
|
|
{ pathsToOpen: [scenario.convertEditorPath('a/1.md')] }
|
|
);
|
|
await app.openPaths.lastCall.returnValue;
|
|
await scenario.assert('[a 1.md] [_ _] [b _]');
|
|
|
|
electron.ipcMain.emit(
|
|
'open',
|
|
{},
|
|
{ pathsToOpen: [scenario.convertRootPath('c')] }
|
|
);
|
|
await app.openPaths.lastCall.returnValue;
|
|
await scenario.assert('[a 1.md] [c _] [b _]');
|
|
|
|
electron.ipcMain.emit(
|
|
'open',
|
|
{},
|
|
{ pathsToOpen: [scenario.convertRootPath('d')], here: true }
|
|
);
|
|
await app.openPaths.lastCall.returnValue;
|
|
await scenario.assert('[a 1.md] [c,d _] [b _]');
|
|
});
|
|
|
|
it('"open" without any option open the prompt for selecting a path', async function() {
|
|
sinon.stub(app, 'atomWindowForEvent', () => w1);
|
|
|
|
electron.ipcMain.emit('open', {});
|
|
assert.strictEqual(app.promptForPath.lastCall.args[0], 'all');
|
|
});
|
|
|
|
it('"open-chosen-any" opens a file in the sending window', async function() {
|
|
sinon.stub(app, 'atomWindowForEvent', () => w2);
|
|
|
|
electron.ipcMain.emit(
|
|
'open-chosen-any',
|
|
{},
|
|
scenario.convertEditorPath('a/1.md')
|
|
);
|
|
await conditionPromise(() => app.openPaths.called);
|
|
await app.openPaths.lastCall.returnValue;
|
|
await scenario.assert('[a _] [_ _] [b 1.md]');
|
|
|
|
assert.isTrue(app.promptForPath.called);
|
|
assert.strictEqual(app.promptForPath.lastCall.args[0], 'all');
|
|
});
|
|
|
|
it('"open-chosen-any" opens a directory by the standard opening rules', async function() {
|
|
sinon.stub(app, 'atomWindowForEvent', () => w1);
|
|
|
|
// Open unrecognized directory in empty window
|
|
electron.ipcMain.emit(
|
|
'open-chosen-any',
|
|
{},
|
|
scenario.convertRootPath('c')
|
|
);
|
|
await conditionPromise(() => app.openPaths.callCount > 0);
|
|
await app.openPaths.lastCall.returnValue;
|
|
await scenario.assert('[a _] [c _] [b _]');
|
|
|
|
assert.strictEqual(app.promptForPath.callCount, 1);
|
|
assert.strictEqual(app.promptForPath.lastCall.args[0], 'all');
|
|
|
|
// Open unrecognized directory in new window
|
|
electron.ipcMain.emit(
|
|
'open-chosen-any',
|
|
{},
|
|
scenario.convertRootPath('d')
|
|
);
|
|
await conditionPromise(() => app.openPaths.callCount > 1);
|
|
await app.openPaths.lastCall.returnValue;
|
|
await scenario.assert('[a _] [c _] [b _] [d _]');
|
|
|
|
assert.strictEqual(app.promptForPath.callCount, 2);
|
|
assert.strictEqual(app.promptForPath.lastCall.args[0], 'all');
|
|
|
|
// Open recognized directory in existing window
|
|
electron.ipcMain.emit(
|
|
'open-chosen-any',
|
|
{},
|
|
scenario.convertRootPath('a')
|
|
);
|
|
await conditionPromise(() => app.openPaths.callCount > 2);
|
|
await app.openPaths.lastCall.returnValue;
|
|
await scenario.assert('[a _] [c _] [b _] [d _]');
|
|
|
|
assert.strictEqual(app.promptForPath.callCount, 3);
|
|
assert.strictEqual(app.promptForPath.lastCall.args[0], 'all');
|
|
});
|
|
|
|
it('"open-chosen-file" opens a file chooser and opens the chosen file in the sending window', async function() {
|
|
sinon.stub(app, 'atomWindowForEvent', () => w0);
|
|
|
|
electron.ipcMain.emit(
|
|
'open-chosen-file',
|
|
{},
|
|
scenario.convertEditorPath('b/2.md')
|
|
);
|
|
await app.openPaths.lastCall.returnValue;
|
|
await scenario.assert('[a 2.md] [_ _] [b _]');
|
|
|
|
assert.isTrue(app.promptForPath.called);
|
|
assert.strictEqual(app.promptForPath.lastCall.args[0], 'file');
|
|
});
|
|
|
|
it('"open-chosen-folder" opens a directory chooser and opens the chosen directory', async function() {
|
|
sinon.stub(app, 'atomWindowForEvent', () => w0);
|
|
|
|
electron.ipcMain.emit(
|
|
'open-chosen-folder',
|
|
{},
|
|
scenario.convertRootPath('c')
|
|
);
|
|
await app.openPaths.lastCall.returnValue;
|
|
await scenario.assert('[a _] [c _] [b _]');
|
|
|
|
assert.isTrue(app.promptForPath.called);
|
|
assert.strictEqual(app.promptForPath.lastCall.args[0], 'folder');
|
|
});
|
|
});
|
|
|
|
describe('window state serialization', function() {
|
|
it('occurs immediately when adding a window', async function() {
|
|
await scenario.launch(parseCommandLine(['a']));
|
|
|
|
const promise = emitterEventPromise(
|
|
scenario.getApplication(0),
|
|
'application:did-save-state'
|
|
);
|
|
await scenario.open(parseCommandLine(['c', 'b']));
|
|
await promise;
|
|
|
|
assert.isTrue(
|
|
scenario
|
|
.getApplication(0)
|
|
.storageFolder.store.calledWith('application.json', {
|
|
version: '1',
|
|
windows: [
|
|
{ projectRoots: [scenario.convertRootPath('a')] },
|
|
{
|
|
projectRoots: [
|
|
scenario.convertRootPath('b'),
|
|
scenario.convertRootPath('c')
|
|
]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
});
|
|
|
|
it('occurs immediately when removing a window', async function() {
|
|
await scenario.launch(parseCommandLine(['a']));
|
|
const w = await scenario.open(parseCommandLine(['b']));
|
|
|
|
const promise = emitterEventPromise(
|
|
scenario.getApplication(0),
|
|
'application:did-save-state'
|
|
);
|
|
scenario.getApplication(0).removeWindow(w);
|
|
await promise;
|
|
|
|
assert.isTrue(
|
|
scenario
|
|
.getApplication(0)
|
|
.storageFolder.store.calledWith('application.json', {
|
|
version: '1',
|
|
windows: [{ projectRoots: [scenario.convertRootPath('a')] }]
|
|
})
|
|
);
|
|
});
|
|
|
|
it('occurs when the window is blurred', async function() {
|
|
const [w] = await scenario.launch(parseCommandLine(['a']));
|
|
const promise = emitterEventPromise(
|
|
scenario.getApplication(0),
|
|
'application:did-save-state'
|
|
);
|
|
w.browserWindow.emit('blur');
|
|
await promise;
|
|
});
|
|
});
|
|
|
|
describe('when closing the last window', function() {
|
|
if (process.platform === 'linux' || process.platform === 'win32') {
|
|
it('quits the application', async function() {
|
|
const [w] = await scenario.launch(parseCommandLine(['a']));
|
|
scenario.getApplication(0).removeWindow(w);
|
|
assert.isTrue(electron.app.quit.called);
|
|
});
|
|
} else if (process.platform === 'darwin') {
|
|
it('leaves the application open', async function() {
|
|
const [w] = await scenario.launch(parseCommandLine(['a']));
|
|
scenario.getApplication(0).removeWindow(w);
|
|
assert.isFalse(electron.app.quit.called);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('quitting', function() {
|
|
it('waits until all windows have saved their state before quitting', async function() {
|
|
const [w0] = await scenario.launch(parseCommandLine(['a']));
|
|
const w1 = await scenario.open(parseCommandLine(['b']));
|
|
assert.notStrictEqual(w0, w1);
|
|
|
|
sinon.spy(w0, 'close');
|
|
let resolveUnload0;
|
|
w0.prepareToUnload = () =>
|
|
new Promise(resolve => {
|
|
resolveUnload0 = resolve;
|
|
});
|
|
|
|
sinon.spy(w1, 'close');
|
|
let resolveUnload1;
|
|
w1.prepareToUnload = () =>
|
|
new Promise(resolve => {
|
|
resolveUnload1 = resolve;
|
|
});
|
|
|
|
const evt = { preventDefault: sinon.spy() };
|
|
electron.app.emit('before-quit', evt);
|
|
await new Promise(process.nextTick);
|
|
assert.isTrue(evt.preventDefault.called);
|
|
assert.isFalse(electron.app.quit.called);
|
|
|
|
resolveUnload1(true);
|
|
await new Promise(process.nextTick);
|
|
assert.isFalse(electron.app.quit.called);
|
|
|
|
resolveUnload0(true);
|
|
await scenario.getApplication(0).lastBeforeQuitPromise;
|
|
assert.isTrue(electron.app.quit.called);
|
|
|
|
assert.isTrue(w0.close.called);
|
|
assert.isTrue(w1.close.called);
|
|
});
|
|
|
|
it('prevents a quit if a user cancels when prompted to save', async function() {
|
|
const [w] = await scenario.launch(parseCommandLine(['a']));
|
|
let resolveUnload;
|
|
w.prepareToUnload = () =>
|
|
new Promise(resolve => {
|
|
resolveUnload = resolve;
|
|
});
|
|
|
|
const evt = { preventDefault: sinon.spy() };
|
|
electron.app.emit('before-quit', evt);
|
|
await new Promise(process.nextTick);
|
|
assert.isTrue(evt.preventDefault.called);
|
|
|
|
resolveUnload(false);
|
|
await scenario.getApplication(0).lastBeforeQuitPromise;
|
|
|
|
assert.isFalse(electron.app.quit.called);
|
|
});
|
|
|
|
it('closes successfully unloaded windows', async function() {
|
|
const [w0] = await scenario.launch(parseCommandLine(['a']));
|
|
const w1 = await scenario.open(parseCommandLine(['b']));
|
|
|
|
sinon.spy(w0, 'close');
|
|
let resolveUnload0;
|
|
w0.prepareToUnload = () =>
|
|
new Promise(resolve => {
|
|
resolveUnload0 = resolve;
|
|
});
|
|
|
|
sinon.spy(w1, 'close');
|
|
let resolveUnload1;
|
|
w1.prepareToUnload = () =>
|
|
new Promise(resolve => {
|
|
resolveUnload1 = resolve;
|
|
});
|
|
|
|
const evt = { preventDefault() {} };
|
|
electron.app.emit('before-quit', evt);
|
|
|
|
resolveUnload0(false);
|
|
resolveUnload1(true);
|
|
|
|
await scenario.getApplication(0).lastBeforeQuitPromise;
|
|
|
|
assert.isFalse(electron.app.quit.called);
|
|
assert.isFalse(w0.close.called);
|
|
assert.isTrue(w1.close.called);
|
|
});
|
|
});
|
|
});
|
|
|
|
class StubWindow extends EventEmitter {
|
|
constructor(sinon, loadSettings, options) {
|
|
super();
|
|
|
|
this.loadSettings = loadSettings;
|
|
|
|
this._dimensions = Object.assign({}, loadSettings.windowDimensions) || {
|
|
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.locationsToOpen || [];
|
|
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.projectRoots = Array.from(this._rootPaths);
|
|
this.projectRoots.sort();
|
|
|
|
this.emit('window:locations-opened');
|
|
}
|
|
|
|
setSize(x, y) {
|
|
this._dimensions = { x, y };
|
|
}
|
|
|
|
setPosition(x, y) {
|
|
this._position = { x, y };
|
|
}
|
|
|
|
isSpecWindow() {
|
|
return this.isSpec;
|
|
}
|
|
|
|
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 Object.assign({}, 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.atomHome = null;
|
|
this.projectRootPool = new Map();
|
|
this.filePathPool = new Map();
|
|
|
|
this.killedPids = [];
|
|
this.originalAtomHome = null;
|
|
}
|
|
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
this.atomHome = path.join(this.root, '.atom');
|
|
await new Promise((resolve, reject) => {
|
|
fs.makeTree(this.atomHome, err => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
this.originalAtomHome = process.env.ATOM_HOME;
|
|
process.env.ATOM_HOME = this.atomHome;
|
|
|
|
await Promise.all(
|
|
['a', 'b', 'c', 'd'].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();
|
|
}
|
|
}
|
|
);
|
|
})
|
|
)
|
|
);
|
|
|
|
this.sinon.stub(electron.app, 'quit');
|
|
}
|
|
|
|
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(
|
|
((theApp, foldersToOpen, pathsToOpen) => {
|
|
return theApp.openPaths({
|
|
newWindow: true,
|
|
foldersToOpen,
|
|
pathsToOpen
|
|
});
|
|
})(app, windowSpec.roots, windowSpec.editors)
|
|
);
|
|
}
|
|
await Promise.all(windowPromises);
|
|
}
|
|
|
|
launch(options) {
|
|
const app = options.app || this.addApplication();
|
|
delete options.app;
|
|
|
|
if (options.pathsToOpen) {
|
|
options.pathsToOpen = this.convertPaths(options.pathsToOpen);
|
|
}
|
|
|
|
return app.launch(options);
|
|
}
|
|
|
|
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;
|
|
|
|
return app.openWithOptions(options);
|
|
}
|
|
|
|
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: null,
|
|
editors: null
|
|
});
|
|
}
|
|
|
|
const shorthandParts = [];
|
|
const descriptionParts = [];
|
|
for (const comparison of comparisons) {
|
|
if (comparison.roots !== null && comparison.editors !== null) {
|
|
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()));
|
|
|
|
if (this.originalAtomHome) {
|
|
process.env.ATOM_HOME = this.originalAtomHome;
|
|
}
|
|
}
|
|
|
|
addApplication(options = {}) {
|
|
const app = new AtomApplication({
|
|
resourcePath: path.resolve(__dirname, '../..'),
|
|
atomHomeDirPath: this.atomHome,
|
|
preserveFocus: true,
|
|
killProcess: pid => {
|
|
this.killedPids.push(pid);
|
|
},
|
|
...options
|
|
});
|
|
this.sinon.stub(app, 'createWindow', loadSettings => {
|
|
const newWindow = new StubWindow(this.sinon, loadSettings, options);
|
|
this.windows.add(newWindow);
|
|
return newWindow;
|
|
});
|
|
this.sinon.stub(app.storageFolder, 'load', () =>
|
|
Promise.resolve(options.applicationJson || { version: '1', windows: [] })
|
|
);
|
|
this.sinon.stub(app.storageFolder, 'store', () => Promise.resolve());
|
|
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) {
|
|
if (
|
|
shortRootPath.startsWith('atom://') ||
|
|
shortRootPath.startsWith('remote://')
|
|
) {
|
|
return shortRootPath;
|
|
}
|
|
|
|
const fullRootPath = this.projectRootPool.get(shortRootPath);
|
|
if (!fullRootPath) {
|
|
throw new Error(`Unexpected short project root path: ${shortRootPath}`);
|
|
}
|
|
return fullRootPath;
|
|
}
|
|
|
|
convertEditorPath(shortEditorPath) {
|
|
const [truncatedPath, ...suffix] = shortEditorPath.split(/(?=:)/);
|
|
const fullEditorPath = this.filePathPool.get(truncatedPath);
|
|
if (!fullEditorPath) {
|
|
throw new Error(`Unexpected short editor path: ${shortEditorPath}`);
|
|
}
|
|
return fullEditorPath + suffix.join('');
|
|
}
|
|
|
|
convertPaths(paths) {
|
|
return paths.map(shortPath => {
|
|
if (
|
|
shortPath.startsWith('atom://') ||
|
|
shortPath.startsWith('remote://')
|
|
) {
|
|
return shortPath;
|
|
}
|
|
|
|
const fullRoot = this.projectRootPool.get(shortPath);
|
|
if (fullRoot) {
|
|
return fullRoot;
|
|
}
|
|
|
|
const [truncatedPath, ...suffix] = shortPath.split(/(?=:)/);
|
|
const fullEditor = this.filePathPool.get(truncatedPath);
|
|
if (fullEditor) {
|
|
return fullEditor + suffix.join('');
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|