atom-application.test.js 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756
  1. /* globals assert */
  2. const path = require('path');
  3. const { EventEmitter } = require('events');
  4. const temp = require('temp').track();
  5. const fs = require('fs-plus');
  6. const electron = require('electron');
  7. const sandbox = require('sinon').createSandbox();
  8. const AtomApplication = require('../../src/main-process/atom-application');
  9. const parseCommandLine = require('../../src/main-process/parse-command-line');
  10. const {
  11. emitterEventPromise,
  12. conditionPromise
  13. } = require('../async-spec-helpers');
  14. // These tests use a utility class called LaunchScenario, defined below, to manipulate AtomApplication instances that
  15. // (1) are stubbed to only simulate AtomWindow creation and (2) allow you to use a shorthand notation to assert the
  16. // application state after certain launch actions.
  17. //
  18. // Each scenario instance has access to a small set of directories and files created within a dedicated temporary
  19. // directory. For convenience, you may use short names to refer to any of its contents (their basenames, basically).
  20. // Check `LaunchScenario::init()` to see what directories and files are available.
  21. //
  22. // To create an application and its first window, call `await scenario.launch({})`. "Launch" may open multiple windows,
  23. // so it returns a Promise that resolves to an array of StubWindows. Its options argument may be created by
  24. // `parseCommandLine()` from a simulated argv string, or built by hand to include `{pathsToOpen}` and so on.
  25. //
  26. // To create additional windows, call `await scenario.open({})` with similar arguments. `LaunchScenario::open()` returns
  27. // a Promise that resolves to the opened or re-used StubWindows. The one exception is if `urlsToOpen` are provided in the open
  28. // arguments; then it resolves to an Array of StubWindows, because AtomApplication processes each URL individually.
  29. //
  30. // To ensure that the expected windows have been created, call `await scenario.assert('')` with a string specifying the
  31. // expected window contents. The specification shorthand language is as follows:
  32. //
  33. // * '[_ _]' describes a single window with no project roots and no open editors.
  34. // * '[_ 1.md]' describes a single window with no project roots and a single editor open on the file `./a/1.md` within
  35. // the LaunchScenario temporary directory.
  36. // * '[a _]' describes a single window with one project root - the directory `./a` within the LaunchScenario temporary
  37. // directory - and no open editors.
  38. // * '[a,b 1.md,2.md]' describes a single window with two project roots - the directories `./a` and `./b` - and two
  39. // open editors - `./a/1.md` and `./b/2.md`.
  40. // * '[a _] [b,c 2.md]' describes two windows, one with a project root of `./a` and no open editors, and another with
  41. // two project roots, `./b` and `./c`, and one open editor on `./b/2.md`. The windows are listed in their expected
  42. // creation order.
  43. describe('AtomApplication', function() {
  44. let scenario, sinon;
  45. if (process.env.CI) {
  46. this.timeout(10 * 1000);
  47. }
  48. beforeEach(async function() {
  49. sinon = sandbox;
  50. scenario = await LaunchScenario.create(sinon);
  51. });
  52. afterEach(async function() {
  53. await scenario.destroy();
  54. sinon.restore();
  55. });
  56. describe('command-line interface behavior', function() {
  57. describe('with no open windows', function() {
  58. // This is also the case when a user selects the application from the OS shell
  59. it('opens an empty window', async function() {
  60. await scenario.launch(parseCommandLine([]));
  61. await scenario.assert('[_ _]');
  62. });
  63. // This is also the case when a user clicks on a file in their file manager
  64. it('opens a file', async function() {
  65. await scenario.open(parseCommandLine(['a/1.md']));
  66. await scenario.assert('[_ 1.md]');
  67. });
  68. // This is also the case when a user clicks on a folder in their file manager
  69. // (or, on macOS, drags the folder to Atom in their doc)
  70. it('opens a directory', async function() {
  71. await scenario.open(parseCommandLine(['a']));
  72. await scenario.assert('[a _]');
  73. });
  74. it('opens a file with --add', async function() {
  75. await scenario.open(parseCommandLine(['--add', 'a/1.md']));
  76. await scenario.assert('[_ 1.md]');
  77. });
  78. it('opens a directory with --add', async function() {
  79. await scenario.open(parseCommandLine(['--add', 'a']));
  80. await scenario.assert('[a _]');
  81. });
  82. it('opens a file with --new-window', async function() {
  83. await scenario.open(parseCommandLine(['--new-window', 'a/1.md']));
  84. await scenario.assert('[_ 1.md]');
  85. });
  86. it('opens a directory with --new-window', async function() {
  87. await scenario.open(parseCommandLine(['--new-window', 'a']));
  88. await scenario.assert('[a _]');
  89. });
  90. describe('with previous window state', function() {
  91. let app;
  92. beforeEach(function() {
  93. app = scenario.addApplication({
  94. applicationJson: {
  95. version: '1',
  96. windows: [
  97. { projectRoots: [scenario.convertRootPath('b')] },
  98. { projectRoots: [scenario.convertRootPath('c')] }
  99. ]
  100. }
  101. });
  102. });
  103. describe('with core.restorePreviousWindowsOnStart set to "no"', function() {
  104. beforeEach(function() {
  105. app.config.set('core.restorePreviousWindowsOnStart', 'no');
  106. });
  107. it("doesn't restore windows when launched with no arguments", async function() {
  108. await scenario.launch({ app });
  109. await scenario.assert('[_ _]');
  110. });
  111. it("doesn't restore windows when launched with paths to open", async function() {
  112. await scenario.launch({ app, pathsToOpen: ['a/1.md'] });
  113. await scenario.assert('[_ 1.md]');
  114. });
  115. it("doesn't restore windows when --new-window is provided", async function() {
  116. await scenario.launch({ app, newWindow: true });
  117. await scenario.assert('[_ _]');
  118. });
  119. });
  120. describe('with core.restorePreviousWindowsOnStart set to "yes"', function() {
  121. beforeEach(function() {
  122. app.config.set('core.restorePreviousWindowsOnStart', 'yes');
  123. });
  124. it('restores windows when launched with no arguments', async function() {
  125. await scenario.launch({ app });
  126. await scenario.assert('[b _] [c _]');
  127. });
  128. it("doesn't restore windows when launched with paths to open", async function() {
  129. await scenario.launch({ app, pathsToOpen: ['a/1.md'] });
  130. await scenario.assert('[_ 1.md]');
  131. });
  132. it("doesn't restore windows when --new-window is provided", async function() {
  133. await scenario.launch({ app, newWindow: true });
  134. await scenario.assert('[_ _]');
  135. });
  136. });
  137. describe('with core.restorePreviousWindowsOnStart set to "always"', function() {
  138. beforeEach(function() {
  139. app.config.set('core.restorePreviousWindowsOnStart', 'always');
  140. });
  141. it('restores windows when launched with no arguments', async function() {
  142. await scenario.launch({ app });
  143. await scenario.assert('[b _] [c _]');
  144. });
  145. it('restores windows when launched with a project path to open', async function() {
  146. await scenario.launch({ app, pathsToOpen: ['a'] });
  147. await scenario.assert('[b _] [c _] [a _]');
  148. });
  149. it('restores windows when launched with a file path to open', async function() {
  150. await scenario.launch({ app, pathsToOpen: ['a/1.md'] });
  151. await scenario.assert('[b _] [c 1.md]');
  152. });
  153. it('collapses new paths into restored windows when appropriate', async function() {
  154. await scenario.launch({ app, pathsToOpen: ['b/2.md'] });
  155. await scenario.assert('[b 2.md] [c _]');
  156. });
  157. it("doesn't restore windows when --new-window is provided", async function() {
  158. await scenario.launch({ app, newWindow: true });
  159. await scenario.assert('[_ _]');
  160. });
  161. it("doesn't restore windows on open, just launch", async function() {
  162. await scenario.launch({ app, pathsToOpen: ['a'], newWindow: true });
  163. await scenario.open(parseCommandLine(['b']));
  164. await scenario.assert('[a _] [b _]');
  165. });
  166. });
  167. });
  168. describe('with unversioned application state', function() {
  169. it('reads "initialPaths" as project roots', async function() {
  170. const app = scenario.addApplication({
  171. applicationJson: [
  172. { initialPaths: [scenario.convertRootPath('a')] },
  173. {
  174. initialPaths: [
  175. scenario.convertRootPath('b'),
  176. scenario.convertRootPath('c')
  177. ]
  178. }
  179. ]
  180. });
  181. app.config.set('core.restorePreviousWindowsOnStart', 'always');
  182. await scenario.launch({ app });
  183. await scenario.assert('[a _] [b,c _]');
  184. });
  185. it('filters file paths from project root lists', async function() {
  186. const app = scenario.addApplication({
  187. applicationJson: [
  188. {
  189. initialPaths: [
  190. scenario.convertRootPath('b'),
  191. scenario.convertEditorPath('a/1.md')
  192. ]
  193. }
  194. ]
  195. });
  196. app.config.set('core.restorePreviousWindowsOnStart', 'always');
  197. await scenario.launch({ app });
  198. await scenario.assert('[b _]');
  199. });
  200. });
  201. });
  202. describe('with one empty window', function() {
  203. beforeEach(async function() {
  204. await scenario.preconditions('[_ _]');
  205. });
  206. // This is also the case when a user selects the application from the OS shell
  207. it('opens a new, empty window', async function() {
  208. await scenario.open(parseCommandLine([]));
  209. await scenario.assert('[_ _] [_ _]');
  210. });
  211. // This is also the case when a user clicks on a file in their file manager
  212. it('opens a file', async function() {
  213. await scenario.open(parseCommandLine(['a/1.md']));
  214. await scenario.assert('[_ 1.md]');
  215. });
  216. // This is also the case when a user clicks on a folder in their file manager
  217. it('opens a directory', async function() {
  218. await scenario.open(parseCommandLine(['a']));
  219. await scenario.assert('[a _]');
  220. });
  221. it('opens a file with --add', async function() {
  222. await scenario.open(parseCommandLine(['--add', 'a/1.md']));
  223. await scenario.assert('[_ 1.md]');
  224. });
  225. it('opens a directory with --add', async function() {
  226. await scenario.open(parseCommandLine(['--add', 'a']));
  227. await scenario.assert('[a _]');
  228. });
  229. it('opens a file with --new-window', async function() {
  230. await scenario.open(parseCommandLine(['--new-window', 'a/1.md']));
  231. await scenario.assert('[_ _] [_ 1.md]');
  232. });
  233. it('opens a directory with --new-window', async function() {
  234. await scenario.open(parseCommandLine(['--new-window', 'a']));
  235. await scenario.assert('[_ _] [a _]');
  236. });
  237. });
  238. describe('with one window that has a project root', function() {
  239. beforeEach(async function() {
  240. await scenario.preconditions('[a _]');
  241. });
  242. // This is also the case when a user selects the application from the OS shell
  243. it('opens a new, empty window', async function() {
  244. await scenario.open(parseCommandLine([]));
  245. await scenario.assert('[a _] [_ _]');
  246. });
  247. // This is also the case when a user clicks on a file within the project root in their file manager
  248. it('opens a file within the project root', async function() {
  249. await scenario.open(parseCommandLine(['a/1.md']));
  250. await scenario.assert('[a 1.md]');
  251. });
  252. // This is also the case when a user clicks on a project root folder in their file manager
  253. it('opens a directory that matches the project root', async function() {
  254. await scenario.open(parseCommandLine(['a']));
  255. await scenario.assert('[a _]');
  256. });
  257. // This is also the case when a user clicks on a file outside the project root in their file manager
  258. it('opens a file outside the project root', async function() {
  259. await scenario.open(parseCommandLine(['b/2.md']));
  260. await scenario.assert('[a 2.md]');
  261. });
  262. // This is also the case when a user clicks on a new folder in their file manager
  263. it('opens a directory other than the project root', async function() {
  264. await scenario.open(parseCommandLine(['b']));
  265. await scenario.assert('[a _] [b _]');
  266. });
  267. it('opens a file within the project root with --add', async function() {
  268. await scenario.open(parseCommandLine(['--add', 'a/1.md']));
  269. await scenario.assert('[a 1.md]');
  270. });
  271. it('opens a directory that matches the project root with --add', async function() {
  272. await scenario.open(parseCommandLine(['--add', 'a']));
  273. await scenario.assert('[a _]');
  274. });
  275. it('opens a file outside the project root with --add', async function() {
  276. await scenario.open(parseCommandLine(['--add', 'b/2.md']));
  277. await scenario.assert('[a 2.md]');
  278. });
  279. it('opens a directory other than the project root with --add', async function() {
  280. await scenario.open(parseCommandLine(['--add', 'b']));
  281. await scenario.assert('[a,b _]');
  282. });
  283. it('opens a file within the project root with --new-window', async function() {
  284. await scenario.open(parseCommandLine(['--new-window', 'a/1.md']));
  285. await scenario.assert('[a _] [_ 1.md]');
  286. });
  287. it('opens a directory that matches the project root with --new-window', async function() {
  288. await scenario.open(parseCommandLine(['--new-window', 'a']));
  289. await scenario.assert('[a _] [a _]');
  290. });
  291. it('opens a file outside the project root with --new-window', async function() {
  292. await scenario.open(parseCommandLine(['--new-window', 'b/2.md']));
  293. await scenario.assert('[a _] [_ 2.md]');
  294. });
  295. it('opens a directory other than the project root with --new-window', async function() {
  296. await scenario.open(parseCommandLine(['--new-window', 'b']));
  297. await scenario.assert('[a _] [b _]');
  298. });
  299. });
  300. describe('with two windows, one with a project root and one empty', function() {
  301. beforeEach(async function() {
  302. await scenario.preconditions('[a _] [_ _]');
  303. });
  304. // This is also the case when a user selects the application from the OS shell
  305. it('opens a new, empty window', async function() {
  306. await scenario.open(parseCommandLine([]));
  307. await scenario.assert('[a _] [_ _] [_ _]');
  308. });
  309. // This is also the case when a user clicks on a file within the project root in their file manager
  310. it('opens a file within the project root', async function() {
  311. await scenario.open(parseCommandLine(['a/1.md']));
  312. await scenario.assert('[a 1.md] [_ _]');
  313. });
  314. // This is also the case when a user clicks on a project root folder in their file manager
  315. it('opens a directory that matches the project root', async function() {
  316. await scenario.open(parseCommandLine(['a']));
  317. await scenario.assert('[a _] [_ _]');
  318. });
  319. // This is also the case when a user clicks on a file outside the project root in their file manager
  320. it('opens a file outside the project root', async function() {
  321. await scenario.open(parseCommandLine(['b/2.md']));
  322. await scenario.assert('[a _] [_ 2.md]');
  323. });
  324. // This is also the case when a user clicks on a new folder in their file manager
  325. it('opens a directory other than the project root', async function() {
  326. await scenario.open(parseCommandLine(['b']));
  327. await scenario.assert('[a _] [b _]');
  328. });
  329. it('opens a file within the project root with --add', async function() {
  330. await scenario.open(parseCommandLine(['--add', 'a/1.md']));
  331. await scenario.assert('[a 1.md] [_ _]');
  332. });
  333. it('opens a directory that matches the project root with --add', async function() {
  334. await scenario.open(parseCommandLine(['--add', 'a']));
  335. await scenario.assert('[a _] [_ _]');
  336. });
  337. it('opens a file outside the project root with --add', async function() {
  338. await scenario.open(parseCommandLine(['--add', 'b/2.md']));
  339. await scenario.assert('[a _] [_ 2.md]');
  340. });
  341. it('opens a directory other than the project root with --add', async function() {
  342. await scenario.open(parseCommandLine(['--add', 'b']));
  343. await scenario.assert('[a _] [b _]');
  344. });
  345. it('opens a file within the project root with --new-window', async function() {
  346. await scenario.open(parseCommandLine(['--new-window', 'a/1.md']));
  347. await scenario.assert('[a _] [_ _] [_ 1.md]');
  348. });
  349. it('opens a directory that matches the project root with --new-window', async function() {
  350. await scenario.open(parseCommandLine(['--new-window', 'a']));
  351. await scenario.assert('[a _] [_ _] [a _]');
  352. });
  353. it('opens a file outside the project root with --new-window', async function() {
  354. await scenario.open(parseCommandLine(['--new-window', 'b/2.md']));
  355. await scenario.assert('[a _] [_ _] [_ 2.md]');
  356. });
  357. it('opens a directory other than the project root with --new-window', async function() {
  358. await scenario.open(parseCommandLine(['--new-window', 'b']));
  359. await scenario.assert('[a _] [_ _] [b _]');
  360. });
  361. });
  362. describe('with two windows, one empty and one with a project root', function() {
  363. beforeEach(async function() {
  364. await scenario.preconditions('[_ _] [a _]');
  365. });
  366. // This is also the case when a user selects the application from the OS shell
  367. it('opens a new, empty window', async function() {
  368. await scenario.open(parseCommandLine([]));
  369. await scenario.assert('[_ _] [a _] [_ _]');
  370. });
  371. // This is also the case when a user clicks on a file within the project root in their file manager
  372. it('opens a file within the project root', async function() {
  373. await scenario.open(parseCommandLine(['a/1.md']));
  374. await scenario.assert('[_ _] [a 1.md]');
  375. });
  376. // This is also the case when a user clicks on a project root folder in their file manager
  377. it('opens a directory that matches the project root', async function() {
  378. await scenario.open(parseCommandLine(['a']));
  379. await scenario.assert('[_ _] [a _]');
  380. });
  381. // This is also the case when a user clicks on a file outside the project root in their file manager
  382. it('opens a file outside the project root', async function() {
  383. await scenario.open(parseCommandLine(['b/2.md']));
  384. await scenario.assert('[_ 2.md] [a _]');
  385. });
  386. // This is also the case when a user clicks on a new folder in their file manager
  387. it('opens a directory other than the project root', async function() {
  388. await scenario.open(parseCommandLine(['b']));
  389. await scenario.assert('[b _] [a _]');
  390. });
  391. it('opens a file within the project root with --add', async function() {
  392. await scenario.open(parseCommandLine(['--add', 'a/1.md']));
  393. await scenario.assert('[_ _] [a 1.md]');
  394. });
  395. it('opens a directory that matches the project root with --add', async function() {
  396. await scenario.open(parseCommandLine(['--add', 'a']));
  397. await scenario.assert('[_ _] [a _]');
  398. });
  399. it('opens a file outside the project root with --add', async function() {
  400. await scenario.open(parseCommandLine(['--add', 'b/2.md']));
  401. await scenario.assert('[_ _] [a 2.md]');
  402. });
  403. it('opens a directory other than the project root with --add', async function() {
  404. await scenario.open(parseCommandLine(['--add', 'b']));
  405. await scenario.assert('[_ _] [a,b _]');
  406. });
  407. it('opens a file within the project root with --new-window', async function() {
  408. await scenario.open(parseCommandLine(['--new-window', 'a/1.md']));
  409. await scenario.assert('[_ _] [a _] [_ 1.md]');
  410. });
  411. it('opens a directory that matches the project root with --new-window', async function() {
  412. await scenario.open(parseCommandLine(['--new-window', 'a']));
  413. await scenario.assert('[_ _] [a _] [a _]');
  414. });
  415. it('opens a file outside the project root with --new-window', async function() {
  416. await scenario.open(parseCommandLine(['--new-window', 'b/2.md']));
  417. await scenario.assert('[_ _] [a _] [_ 2.md]');
  418. });
  419. it('opens a directory other than the project root with --new-window', async function() {
  420. await scenario.open(parseCommandLine(['--new-window', 'b']));
  421. await scenario.assert('[_ _] [a _] [b _]');
  422. });
  423. });
  424. describe('--wait', function() {
  425. it('kills the specified pid after a newly-opened window is closed', async function() {
  426. const [w0] = await scenario.launch(
  427. parseCommandLine(['--new-window', '--wait', '--pid', '101'])
  428. );
  429. const w1 = await scenario.open(
  430. parseCommandLine(['--new-window', '--wait', '--pid', '202'])
  431. );
  432. assert.lengthOf(scenario.killedPids, 0);
  433. w0.browserWindow.emit('closed');
  434. assert.deepEqual(scenario.killedPids, [101]);
  435. w1.browserWindow.emit('closed');
  436. assert.deepEqual(scenario.killedPids, [101, 202]);
  437. });
  438. it('kills the specified pid after all newly-opened files in an existing window are closed', async function() {
  439. const [w] = await scenario.launch(
  440. parseCommandLine(['--new-window', 'a'])
  441. );
  442. await scenario.open(
  443. parseCommandLine([
  444. '--add',
  445. '--wait',
  446. '--pid',
  447. '303',
  448. 'a/1.md',
  449. 'b/2.md'
  450. ])
  451. );
  452. await scenario.assert('[a 1.md,2.md]');
  453. assert.lengthOf(scenario.killedPids, 0);
  454. scenario
  455. .getApplication(0)
  456. .windowDidClosePathWithWaitSession(
  457. w,
  458. scenario.convertEditorPath('b/2.md')
  459. );
  460. assert.lengthOf(scenario.killedPids, 0);
  461. scenario
  462. .getApplication(0)
  463. .windowDidClosePathWithWaitSession(
  464. w,
  465. scenario.convertEditorPath('a/1.md')
  466. );
  467. assert.deepEqual(scenario.killedPids, [303]);
  468. });
  469. it('kills the specified pid after a newly-opened directory in an existing window is closed', async function() {
  470. const [w] = await scenario.launch(
  471. parseCommandLine(['--new-window', 'a'])
  472. );
  473. await scenario.open(
  474. parseCommandLine(['--add', '--wait', '--pid', '404', 'b'])
  475. );
  476. await scenario.assert('[a,b _]');
  477. assert.lengthOf(scenario.killedPids, 0);
  478. scenario
  479. .getApplication(0)
  480. .windowDidClosePathWithWaitSession(w, scenario.convertRootPath('b'));
  481. assert.deepEqual(scenario.killedPids, [404]);
  482. });
  483. });
  484. describe('atom:// URLs', function() {
  485. describe('with a package-name host', function() {
  486. it("loads the package's urlMain in a new window", async function() {
  487. await scenario.launch({});
  488. const app = scenario.getApplication(0);
  489. app.packages = {
  490. getAvailablePackageMetadata: () => [
  491. { name: 'package-with-url-main', urlMain: 'some/url-main' }
  492. ],
  493. resolvePackagePath: () =>
  494. path.resolve('dot-atom/package-with-url-main')
  495. };
  496. const [w1, w2] = await scenario.open(
  497. parseCommandLine([
  498. 'atom://package-with-url-main/test1',
  499. 'atom://package-with-url-main/test2'
  500. ])
  501. );
  502. assert.strictEqual(
  503. w1.loadSettings.windowInitializationScript,
  504. path.resolve('dot-atom/package-with-url-main/some/url-main')
  505. );
  506. assert.strictEqual(
  507. w1.loadSettings.urlToOpen,
  508. 'atom://package-with-url-main/test1'
  509. );
  510. assert.strictEqual(
  511. w2.loadSettings.windowInitializationScript,
  512. path.resolve('dot-atom/package-with-url-main/some/url-main')
  513. );
  514. assert.strictEqual(
  515. w2.loadSettings.urlToOpen,
  516. 'atom://package-with-url-main/test2'
  517. );
  518. });
  519. it('sends a URI message to the most recently focused non-spec window', async function() {
  520. const [w0] = await scenario.launch({});
  521. const w1 = await scenario.open(parseCommandLine(['--new-window']));
  522. const w2 = await scenario.open(parseCommandLine(['--new-window']));
  523. const w3 = await scenario.open(
  524. parseCommandLine(['--test', 'a/1.md'])
  525. );
  526. const app = scenario.getApplication(0);
  527. app.packages = {
  528. getAvailablePackageMetadata: () => []
  529. };
  530. const [uw] = await scenario.open(
  531. parseCommandLine(['atom://package-without-url-main/test'])
  532. );
  533. assert.strictEqual(uw, w2);
  534. assert.isTrue(
  535. w2.sendURIMessage.calledWith('atom://package-without-url-main/test')
  536. );
  537. assert.strictEqual(w2.focus.callCount, 2);
  538. for (const other of [w0, w1, w3]) {
  539. assert.isFalse(other.sendURIMessage.called);
  540. }
  541. });
  542. it('creates a new window and sends a URI message to it once it loads', async function() {
  543. const [w0] = await scenario.launch(
  544. parseCommandLine(['--test', 'a/1.md'])
  545. );
  546. const app = scenario.getApplication(0);
  547. app.packages = {
  548. getAvailablePackageMetadata: () => []
  549. };
  550. const [uw] = await scenario.open(
  551. parseCommandLine(['atom://package-without-url-main/test'])
  552. );
  553. assert.notStrictEqual(uw, w0);
  554. assert.strictEqual(
  555. uw.loadSettings.windowInitializationScript,
  556. path.resolve(
  557. __dirname,
  558. '../../src/initialize-application-window.js'
  559. )
  560. );
  561. uw.emit('window:loaded');
  562. assert.isTrue(
  563. uw.sendURIMessage.calledWith('atom://package-without-url-main/test')
  564. );
  565. });
  566. });
  567. describe('with a "core" host', function() {
  568. it('sends a URI message to the most recently focused non-spec window that owns the open locations', async function() {
  569. const [w0] = await scenario.launch(parseCommandLine(['a']));
  570. const w1 = await scenario.open(
  571. parseCommandLine(['--new-window', 'a'])
  572. );
  573. const w2 = await scenario.open(
  574. parseCommandLine(['--new-window', 'b'])
  575. );
  576. const uri = `atom://core/open/file?filename=${encodeURIComponent(
  577. scenario.convertEditorPath('a/1.md')
  578. )}`;
  579. const [uw] = await scenario.open(parseCommandLine([uri]));
  580. assert.strictEqual(uw, w1);
  581. assert.isTrue(w1.sendURIMessage.calledWith(uri));
  582. for (const other of [w0, w2]) {
  583. assert.isFalse(other.sendURIMessage.called);
  584. }
  585. });
  586. it('creates a new window and sends a URI message to it once it loads', async function() {
  587. const [w0] = await scenario.launch(
  588. parseCommandLine(['--test', 'a/1.md'])
  589. );
  590. const uri = `atom://core/open/file?filename=${encodeURIComponent(
  591. scenario.convertEditorPath('b/2.md')
  592. )}`;
  593. const [uw] = await scenario.open(parseCommandLine([uri]));
  594. assert.notStrictEqual(uw, w0);
  595. uw.emit('window:loaded');
  596. assert.isTrue(uw.sendURIMessage.calledWith(uri));
  597. });
  598. });
  599. });
  600. it('opens a file to a specific line number', async function() {
  601. await scenario.open(parseCommandLine(['a/1.md:10']));
  602. await scenario.assert('[_ 1.md]');
  603. const w = scenario.getWindow(0);
  604. assert.lengthOf(w._locations, 1);
  605. assert.strictEqual(w._locations[0].initialLine, 9);
  606. assert.isNull(w._locations[0].initialColumn);
  607. });
  608. it('opens a file to a specific line number and column', async function() {
  609. await scenario.open(parseCommandLine(['b/2.md:12:5']));
  610. await scenario.assert('[_ 2.md]');
  611. const w = scenario.getWindow(0);
  612. assert.lengthOf(w._locations, 1);
  613. assert.strictEqual(w._locations[0].initialLine, 11);
  614. assert.strictEqual(w._locations[0].initialColumn, 4);
  615. });
  616. it('opens a directory with a non-file protocol', async function() {
  617. await scenario.open(
  618. parseCommandLine(['remote://server:3437/some/directory/path'])
  619. );
  620. const w = scenario.getWindow(0);
  621. assert.lengthOf(w._locations, 1);
  622. assert.strictEqual(
  623. w._locations[0].pathToOpen,
  624. 'remote://server:3437/some/directory/path'
  625. );
  626. assert.isFalse(w._locations[0].exists);
  627. assert.isFalse(w._locations[0].isDirectory);
  628. assert.isFalse(w._locations[0].isFile);
  629. });
  630. it('truncates trailing whitespace and colons', async function() {
  631. await scenario.open(parseCommandLine(['b/2.md:: ']));
  632. await scenario.assert('[_ 2.md]');
  633. const w = scenario.getWindow(0);
  634. assert.lengthOf(w._locations, 1);
  635. assert.isNull(w._locations[0].initialLine);
  636. assert.isNull(w._locations[0].initialColumn);
  637. });
  638. it('disregards test and benchmark windows', async function() {
  639. await scenario.launch(parseCommandLine(['--test', 'b']));
  640. await scenario.open(parseCommandLine(['--new-window']));
  641. await scenario.open(parseCommandLine(['--test', 'c']));
  642. await scenario.open(parseCommandLine(['--benchmark', 'b']));
  643. await scenario.open(parseCommandLine(['a/1.md']));
  644. // Test StubWindows are visible as empty editor windows here.
  645. // (Benchmark mode has been removed, and will no-longer open new windows.)
  646. await scenario.assert('[_ _] [_ 1.md] [_ _]');
  647. });
  648. });
  649. if (process.platform === 'darwin' || process.platform === 'win32') {
  650. it('positions new windows at an offset from the previous window', async function() {
  651. const [w0] = await scenario.launch(parseCommandLine(['a']));
  652. w0.setSize(400, 400);
  653. const d0 = w0.getDimensions();
  654. const w1 = await scenario.open(parseCommandLine(['b']));
  655. const d1 = w1.getDimensions();
  656. assert.isAbove(d1.x, d0.x);
  657. assert.isAbove(d1.y, d0.y);
  658. });
  659. }
  660. if (process.platform === 'darwin') {
  661. describe('with no windows open', function() {
  662. let app;
  663. beforeEach(async function() {
  664. const [w] = await scenario.launch(parseCommandLine([]));
  665. app = scenario.getApplication(0);
  666. app.removeWindow(w);
  667. sinon.stub(app, 'promptForPathToOpen');
  668. global.atom = { workspace: { getActiveTextEditor() {} } };
  669. });
  670. it('opens a new file', function() {
  671. app.emit('application:open-file');
  672. assert.isTrue(
  673. app.promptForPathToOpen.calledWith('file', {
  674. devMode: false,
  675. safeMode: false,
  676. window: null
  677. })
  678. );
  679. });
  680. it('opens a new directory', function() {
  681. app.emit('application:open-folder');
  682. assert.isTrue(
  683. app.promptForPathToOpen.calledWith('folder', {
  684. devMode: false,
  685. safeMode: false,
  686. window: null
  687. })
  688. );
  689. });
  690. it('opens a new file or directory', function() {
  691. app.emit('application:open');
  692. assert.isTrue(
  693. app.promptForPathToOpen.calledWith('all', {
  694. devMode: false,
  695. safeMode: false,
  696. window: null
  697. })
  698. );
  699. });
  700. it('reopens a project in a new window', async function() {
  701. const paths = scenario.convertPaths(['a', 'b']);
  702. app.emit('application:reopen-project', { paths });
  703. await conditionPromise(() => app.getAllWindows().length > 0);
  704. assert.deepEqual(
  705. app.getAllWindows().map(w => Array.from(w._rootPaths)),
  706. [paths]
  707. );
  708. });
  709. });
  710. }
  711. describe('existing application re-use', function() {
  712. let createApplication;
  713. const version = electron.app.getVersion();
  714. beforeEach(function() {
  715. createApplication = async options => {
  716. options.version = version;
  717. const app = scenario.addApplication(options);
  718. await app.listenForArgumentsFromNewProcess(options);
  719. await app.launch(options);
  720. return app;
  721. };
  722. });
  723. it('creates a new application when no socket is present', async function() {
  724. const app0 = await AtomApplication.open({ createApplication, version });
  725. await app0.deleteSocketSecretFile();
  726. const app1 = await AtomApplication.open({ createApplication, version });
  727. assert.isNotNull(app1);
  728. assert.notStrictEqual(app0, app1);
  729. });
  730. it('creates a new application for spec windows', async function() {
  731. const app0 = await AtomApplication.open({ createApplication, version });
  732. const app1 = await AtomApplication.open({
  733. createApplication,
  734. version,
  735. ...parseCommandLine(['--test', 'a'])
  736. });
  737. assert.isNotNull(app1);
  738. assert.notStrictEqual(app0, app1);
  739. });
  740. it('sends a request to an existing application when a socket is present', async function() {
  741. const app0 = await AtomApplication.open({ createApplication, version });
  742. assert.lengthOf(app0.getAllWindows(), 1);
  743. const app1 = await AtomApplication.open({
  744. createApplication,
  745. version,
  746. ...parseCommandLine(['--new-window'])
  747. });
  748. assert.isNull(app1);
  749. assert.isTrue(electron.app.quit.called);
  750. await conditionPromise(() => app0.getAllWindows().length === 2);
  751. await scenario.assert('[_ _] [_ _]');
  752. });
  753. });
  754. describe('IPC handling', function() {
  755. let w0, w1, w2, app;
  756. beforeEach(async function() {
  757. w0 = (await scenario.launch(parseCommandLine(['a'])))[0];
  758. w1 = await scenario.open(parseCommandLine(['--new-window']));
  759. w2 = await scenario.open(parseCommandLine(['--new-window', 'b']));
  760. app = scenario.getApplication(0);
  761. sinon.spy(app, 'openPaths');
  762. sinon
  763. .stub(app, 'promptForPath')
  764. .callsFake((_type, callback, defaultPath) => callback([defaultPath]));
  765. });
  766. // This is the IPC message used to handle:
  767. // * application:reopen-project
  768. // * choosing "open in new window" when adding a folder that has previously saved state
  769. // * drag and drop
  770. // * deprecated call links in deprecation-cop
  771. // * other direct callers of `atom.open()`
  772. it('"open" opens a fixed path by the standard opening rules', async function() {
  773. sinon.stub(app, 'atomWindowForEvent').callsFake(() => w1);
  774. electron.ipcMain.emit(
  775. 'open',
  776. {},
  777. { pathsToOpen: [scenario.convertEditorPath('a/1.md')] }
  778. );
  779. await app.openPaths.lastCall.returnValue;
  780. await scenario.assert('[a 1.md] [_ _] [b _]');
  781. electron.ipcMain.emit(
  782. 'open',
  783. {},
  784. { pathsToOpen: [scenario.convertRootPath('c')] }
  785. );
  786. await app.openPaths.lastCall.returnValue;
  787. await scenario.assert('[a 1.md] [c _] [b _]');
  788. electron.ipcMain.emit(
  789. 'open',
  790. {},
  791. { pathsToOpen: [scenario.convertRootPath('d')], here: true }
  792. );
  793. await app.openPaths.lastCall.returnValue;
  794. await scenario.assert('[a 1.md] [c,d _] [b _]');
  795. });
  796. it('"open" without any option open the prompt for selecting a path', async function() {
  797. sinon.stub(app, 'atomWindowForEvent').callsFake(() => w1);
  798. electron.ipcMain.emit('open', {});
  799. assert.strictEqual(app.promptForPath.lastCall.args[0], 'all');
  800. });
  801. it('"open-chosen-any" opens a file in the sending window', async function() {
  802. sinon.stub(app, 'atomWindowForEvent').callsFake(() => w2);
  803. electron.ipcMain.emit(
  804. 'open-chosen-any',
  805. {},
  806. scenario.convertEditorPath('a/1.md')
  807. );
  808. await conditionPromise(() => app.openPaths.called);
  809. await app.openPaths.lastCall.returnValue;
  810. await scenario.assert('[a _] [_ _] [b 1.md]');
  811. assert.isTrue(app.promptForPath.called);
  812. assert.strictEqual(app.promptForPath.lastCall.args[0], 'all');
  813. });
  814. it('"open-chosen-any" opens a directory by the standard opening rules', async function() {
  815. sinon.stub(app, 'atomWindowForEvent').callsFake(() => w1);
  816. // Open unrecognized directory in empty window
  817. electron.ipcMain.emit(
  818. 'open-chosen-any',
  819. {},
  820. scenario.convertRootPath('c')
  821. );
  822. await conditionPromise(() => app.openPaths.callCount > 0);
  823. await app.openPaths.lastCall.returnValue;
  824. await scenario.assert('[a _] [c _] [b _]');
  825. assert.strictEqual(app.promptForPath.callCount, 1);
  826. assert.strictEqual(app.promptForPath.lastCall.args[0], 'all');
  827. // Open unrecognized directory in new window
  828. electron.ipcMain.emit(
  829. 'open-chosen-any',
  830. {},
  831. scenario.convertRootPath('d')
  832. );
  833. await conditionPromise(() => app.openPaths.callCount > 1);
  834. await app.openPaths.lastCall.returnValue;
  835. await scenario.assert('[a _] [c _] [b _] [d _]');
  836. assert.strictEqual(app.promptForPath.callCount, 2);
  837. assert.strictEqual(app.promptForPath.lastCall.args[0], 'all');
  838. // Open recognized directory in existing window
  839. electron.ipcMain.emit(
  840. 'open-chosen-any',
  841. {},
  842. scenario.convertRootPath('a')
  843. );
  844. await conditionPromise(() => app.openPaths.callCount > 2);
  845. await app.openPaths.lastCall.returnValue;
  846. await scenario.assert('[a _] [c _] [b _] [d _]');
  847. assert.strictEqual(app.promptForPath.callCount, 3);
  848. assert.strictEqual(app.promptForPath.lastCall.args[0], 'all');
  849. });
  850. it('"open-chosen-file" opens a file chooser and opens the chosen file in the sending window', async function() {
  851. sinon.stub(app, 'atomWindowForEvent').callsFake(() => w0);
  852. electron.ipcMain.emit(
  853. 'open-chosen-file',
  854. {},
  855. scenario.convertEditorPath('b/2.md')
  856. );
  857. await app.openPaths.lastCall.returnValue;
  858. await scenario.assert('[a 2.md] [_ _] [b _]');
  859. assert.isTrue(app.promptForPath.called);
  860. assert.strictEqual(app.promptForPath.lastCall.args[0], 'file');
  861. });
  862. it('"open-chosen-folder" opens a directory chooser and opens the chosen directory', async function() {
  863. sinon.stub(app, 'atomWindowForEvent').callsFake(() => w0);
  864. electron.ipcMain.emit(
  865. 'open-chosen-folder',
  866. {},
  867. scenario.convertRootPath('c')
  868. );
  869. await app.openPaths.lastCall.returnValue;
  870. await scenario.assert('[a _] [c _] [b _]');
  871. assert.isTrue(app.promptForPath.called);
  872. assert.strictEqual(app.promptForPath.lastCall.args[0], 'folder');
  873. });
  874. });
  875. describe('window state serialization', function() {
  876. it('occurs immediately when adding a window', async function() {
  877. await scenario.launch(parseCommandLine(['a']));
  878. const promise = emitterEventPromise(
  879. scenario.getApplication(0),
  880. 'application:did-save-state'
  881. );
  882. await scenario.open(parseCommandLine(['c', 'b']));
  883. await promise;
  884. assert.isTrue(
  885. scenario
  886. .getApplication(0)
  887. .storageFolder.store.calledWith('application.json', {
  888. version: '1',
  889. windows: [
  890. { projectRoots: [scenario.convertRootPath('a')] },
  891. {
  892. projectRoots: [
  893. scenario.convertRootPath('b'),
  894. scenario.convertRootPath('c')
  895. ]
  896. }
  897. ]
  898. })
  899. );
  900. });
  901. it('occurs immediately when removing a window', async function() {
  902. await scenario.launch(parseCommandLine(['a']));
  903. const w = await scenario.open(parseCommandLine(['b']));
  904. const promise = emitterEventPromise(
  905. scenario.getApplication(0),
  906. 'application:did-save-state'
  907. );
  908. scenario.getApplication(0).removeWindow(w);
  909. await promise;
  910. assert.isTrue(
  911. scenario
  912. .getApplication(0)
  913. .storageFolder.store.calledWith('application.json', {
  914. version: '1',
  915. windows: [{ projectRoots: [scenario.convertRootPath('a')] }]
  916. })
  917. );
  918. });
  919. it('occurs when the window is blurred', async function() {
  920. const [w] = await scenario.launch(parseCommandLine(['a']));
  921. const promise = emitterEventPromise(
  922. scenario.getApplication(0),
  923. 'application:did-save-state'
  924. );
  925. w.browserWindow.emit('blur');
  926. await promise;
  927. });
  928. });
  929. describe('when closing the last window', function() {
  930. if (process.platform === 'linux' || process.platform === 'win32') {
  931. it('quits the application', async function() {
  932. const [w] = await scenario.launch(parseCommandLine(['a']));
  933. scenario.getApplication(0).removeWindow(w);
  934. assert.isTrue(electron.app.quit.called);
  935. });
  936. } else if (process.platform === 'darwin') {
  937. it('leaves the application open', async function() {
  938. const [w] = await scenario.launch(parseCommandLine(['a']));
  939. scenario.getApplication(0).removeWindow(w);
  940. assert.isFalse(electron.app.quit.called);
  941. });
  942. }
  943. });
  944. describe('quitting', function() {
  945. it('waits until all windows have saved their state before quitting', async function() {
  946. const [w0] = await scenario.launch(parseCommandLine(['a']));
  947. const w1 = await scenario.open(parseCommandLine(['b']));
  948. assert.notStrictEqual(w0, w1);
  949. sinon.spy(w0, 'close');
  950. let resolveUnload0;
  951. w0.prepareToUnload = () =>
  952. new Promise(resolve => {
  953. resolveUnload0 = resolve;
  954. });
  955. sinon.spy(w1, 'close');
  956. let resolveUnload1;
  957. w1.prepareToUnload = () =>
  958. new Promise(resolve => {
  959. resolveUnload1 = resolve;
  960. });
  961. const evt = { preventDefault: sinon.spy() };
  962. electron.app.emit('before-quit', evt);
  963. await new Promise(process.nextTick);
  964. assert.isTrue(evt.preventDefault.called);
  965. assert.isFalse(electron.app.quit.called);
  966. resolveUnload1(true);
  967. await new Promise(process.nextTick);
  968. assert.isFalse(electron.app.quit.called);
  969. resolveUnload0(true);
  970. await scenario.getApplication(0).lastBeforeQuitPromise;
  971. assert.isTrue(electron.app.quit.called);
  972. assert.isTrue(w0.close.called);
  973. assert.isTrue(w1.close.called);
  974. });
  975. it('prevents a quit if a user cancels when prompted to save', async function() {
  976. const [w] = await scenario.launch(parseCommandLine(['a']));
  977. let resolveUnload;
  978. w.prepareToUnload = () =>
  979. new Promise(resolve => {
  980. resolveUnload = resolve;
  981. });
  982. const evt = { preventDefault: sinon.spy() };
  983. electron.app.emit('before-quit', evt);
  984. await new Promise(process.nextTick);
  985. assert.isTrue(evt.preventDefault.called);
  986. resolveUnload(false);
  987. await scenario.getApplication(0).lastBeforeQuitPromise;
  988. assert.isFalse(electron.app.quit.called);
  989. });
  990. it('closes successfully unloaded windows', async function() {
  991. const [w0] = await scenario.launch(parseCommandLine(['a']));
  992. const w1 = await scenario.open(parseCommandLine(['b']));
  993. sinon.spy(w0, 'close');
  994. let resolveUnload0;
  995. w0.prepareToUnload = () =>
  996. new Promise(resolve => {
  997. resolveUnload0 = resolve;
  998. });
  999. sinon.spy(w1, 'close');
  1000. let resolveUnload1;
  1001. w1.prepareToUnload = () =>
  1002. new Promise(resolve => {
  1003. resolveUnload1 = resolve;
  1004. });
  1005. const evt = { preventDefault() {} };
  1006. electron.app.emit('before-quit', evt);
  1007. resolveUnload0(false);
  1008. resolveUnload1(true);
  1009. await scenario.getApplication(0).lastBeforeQuitPromise;
  1010. assert.isFalse(electron.app.quit.called);
  1011. assert.isFalse(w0.close.called);
  1012. assert.isTrue(w1.close.called);
  1013. });
  1014. });
  1015. });
  1016. class StubWindow extends EventEmitter {
  1017. constructor(sinon, loadSettings, options) {
  1018. super();
  1019. this.loadSettings = loadSettings;
  1020. this._dimensions = Object.assign({}, loadSettings.windowDimensions) || {
  1021. x: 100,
  1022. y: 100
  1023. };
  1024. this._position = { x: 0, y: 0 };
  1025. this._locations = [];
  1026. this._rootPaths = new Set();
  1027. this._editorPaths = new Set();
  1028. let resolveClosePromise;
  1029. this.closedPromise = new Promise(resolve => {
  1030. resolveClosePromise = resolve;
  1031. });
  1032. this.minimize = sinon.spy();
  1033. this.maximize = sinon.spy();
  1034. this.center = sinon.spy();
  1035. this.focus = sinon.spy();
  1036. this.show = sinon.spy();
  1037. this.hide = sinon.spy();
  1038. this.prepareToUnload = sinon.spy();
  1039. this.close = resolveClosePromise;
  1040. this.replaceEnvironment = sinon.spy();
  1041. this.disableZoom = sinon.spy();
  1042. this.isFocused = sinon
  1043. .stub()
  1044. .returns(options.isFocused !== undefined ? options.isFocused : false);
  1045. this.isMinimized = sinon
  1046. .stub()
  1047. .returns(options.isMinimized !== undefined ? options.isMinimized : false);
  1048. this.isMaximized = sinon
  1049. .stub()
  1050. .returns(options.isMaximized !== undefined ? options.isMaximized : false);
  1051. this.sendURIMessage = sinon.spy();
  1052. this.didChangeUserSettings = sinon.spy();
  1053. this.didFailToReadUserSettings = sinon.spy();
  1054. this.isSpec =
  1055. loadSettings.isSpec !== undefined ? loadSettings.isSpec : false;
  1056. this.devMode =
  1057. loadSettings.devMode !== undefined ? loadSettings.devMode : false;
  1058. this.safeMode =
  1059. loadSettings.safeMode !== undefined ? loadSettings.safeMode : false;
  1060. this.browserWindow = new EventEmitter();
  1061. this.browserWindow.webContents = new EventEmitter();
  1062. const locationsToOpen = this.loadSettings.locationsToOpen || [];
  1063. if (
  1064. !(
  1065. locationsToOpen.length === 1 && locationsToOpen[0].pathToOpen == null
  1066. ) &&
  1067. !this.isSpec
  1068. ) {
  1069. this.openLocations(locationsToOpen);
  1070. }
  1071. }
  1072. openPath(pathToOpen, initialLine, initialColumn) {
  1073. return this.openLocations([{ pathToOpen, initialLine, initialColumn }]);
  1074. }
  1075. openLocations(locations) {
  1076. this._locations.push(...locations);
  1077. for (const location of locations) {
  1078. if (location.pathToOpen) {
  1079. if (location.isDirectory) {
  1080. this._rootPaths.add(location.pathToOpen);
  1081. } else if (location.isFile) {
  1082. this._editorPaths.add(location.pathToOpen);
  1083. }
  1084. }
  1085. }
  1086. this.projectRoots = Array.from(this._rootPaths);
  1087. this.projectRoots.sort();
  1088. this.emit('window:locations-opened');
  1089. }
  1090. setSize(x, y) {
  1091. this._dimensions = { x, y };
  1092. }
  1093. setPosition(x, y) {
  1094. this._position = { x, y };
  1095. }
  1096. isSpecWindow() {
  1097. return this.isSpec;
  1098. }
  1099. hasProjectPaths() {
  1100. return this._rootPaths.size > 0;
  1101. }
  1102. containsLocations(locations) {
  1103. return locations.every(location => this.containsLocation(location));
  1104. }
  1105. containsLocation(location) {
  1106. if (!location.pathToOpen) return false;
  1107. return Array.from(this._rootPaths).some(projectPath => {
  1108. if (location.pathToOpen === projectPath) return true;
  1109. if (location.pathToOpen.startsWith(path.join(projectPath, path.sep))) {
  1110. if (!location.exists) return true;
  1111. if (!location.isDirectory) return true;
  1112. }
  1113. return false;
  1114. });
  1115. }
  1116. getDimensions() {
  1117. return Object.assign({}, this._dimensions);
  1118. }
  1119. }
  1120. class LaunchScenario {
  1121. static async create(sandbox) {
  1122. const scenario = new this(sandbox);
  1123. await scenario.init();
  1124. return scenario;
  1125. }
  1126. constructor(sandbox) {
  1127. this.sinon = sandbox;
  1128. this.applications = new Set();
  1129. this.windows = new Set();
  1130. this.root = null;
  1131. this.atomHome = null;
  1132. this.projectRootPool = new Map();
  1133. this.filePathPool = new Map();
  1134. this.killedPids = [];
  1135. this.originalAtomHome = null;
  1136. }
  1137. async init() {
  1138. if (this.root !== null) {
  1139. return this.root;
  1140. }
  1141. this.root = await new Promise((resolve, reject) => {
  1142. temp.mkdir('launch-', (err, rootPath) => {
  1143. if (err) {
  1144. reject(err);
  1145. } else {
  1146. resolve(rootPath);
  1147. }
  1148. });
  1149. });
  1150. this.atomHome = path.join(this.root, '.atom');
  1151. await new Promise((resolve, reject) => {
  1152. fs.makeTree(this.atomHome, err => {
  1153. if (err) {
  1154. reject(err);
  1155. } else {
  1156. resolve();
  1157. }
  1158. });
  1159. });
  1160. this.originalAtomHome = process.env.ATOM_HOME;
  1161. process.env.ATOM_HOME = this.atomHome;
  1162. await Promise.all(
  1163. ['a', 'b', 'c', 'd'].map(
  1164. dirPath =>
  1165. new Promise((resolve, reject) => {
  1166. const fullDirPath = path.join(this.root, dirPath);
  1167. fs.makeTree(fullDirPath, err => {
  1168. if (err) {
  1169. reject(err);
  1170. } else {
  1171. this.projectRootPool.set(dirPath, fullDirPath);
  1172. resolve();
  1173. }
  1174. });
  1175. })
  1176. )
  1177. );
  1178. await Promise.all(
  1179. ['a/1.md', 'b/2.md'].map(
  1180. filePath =>
  1181. new Promise((resolve, reject) => {
  1182. const fullFilePath = path.join(this.root, filePath);
  1183. fs.writeFile(
  1184. fullFilePath,
  1185. `file: ${filePath}\n`,
  1186. { encoding: 'utf8' },
  1187. err => {
  1188. if (err) {
  1189. reject(err);
  1190. } else {
  1191. this.filePathPool.set(filePath, fullFilePath);
  1192. this.filePathPool.set(path.basename(filePath), fullFilePath);
  1193. resolve();
  1194. }
  1195. }
  1196. );
  1197. })
  1198. )
  1199. );
  1200. this.sinon.stub(electron.app, 'quit');
  1201. }
  1202. async preconditions(source) {
  1203. const app = this.addApplication();
  1204. const windowPromises = [];
  1205. for (const windowSpec of this.parseWindowSpecs(source)) {
  1206. if (windowSpec.editors.length === 0) {
  1207. windowSpec.editors.push(null);
  1208. }
  1209. windowPromises.push(
  1210. ((theApp, foldersToOpen, pathsToOpen) => {
  1211. return theApp.openPaths({
  1212. newWindow: true,
  1213. foldersToOpen,
  1214. pathsToOpen
  1215. });
  1216. })(app, windowSpec.roots, windowSpec.editors)
  1217. );
  1218. }
  1219. await Promise.all(windowPromises);
  1220. }
  1221. launch(options) {
  1222. const app = options.app || this.addApplication();
  1223. delete options.app;
  1224. if (options.pathsToOpen) {
  1225. options.pathsToOpen = this.convertPaths(options.pathsToOpen);
  1226. }
  1227. return app.launch(options);
  1228. }
  1229. open(options) {
  1230. if (this.applications.size === 0) {
  1231. return this.launch(options);
  1232. }
  1233. let app = options.app;
  1234. if (!app) {
  1235. const apps = Array.from(this.applications);
  1236. app = apps[apps.length - 1];
  1237. } else {
  1238. delete options.app;
  1239. }
  1240. if (options.pathsToOpen) {
  1241. options.pathsToOpen = this.convertPaths(options.pathsToOpen);
  1242. }
  1243. options.preserveFocus = true;
  1244. return app.openWithOptions(options);
  1245. }
  1246. async assert(source) {
  1247. const windowSpecs = this.parseWindowSpecs(source);
  1248. let specIndex = 0;
  1249. const windowPromises = [];
  1250. for (const window of this.windows) {
  1251. windowPromises.push(
  1252. (async (theWindow, theSpec) => {
  1253. const {
  1254. _rootPaths: rootPaths,
  1255. _editorPaths: editorPaths
  1256. } = theWindow;
  1257. const comparison = {
  1258. ok: true,
  1259. extraWindow: false,
  1260. missingWindow: false,
  1261. extraRoots: [],
  1262. missingRoots: [],
  1263. extraEditors: [],
  1264. missingEditors: [],
  1265. roots: rootPaths,
  1266. editors: editorPaths
  1267. };
  1268. if (!theSpec) {
  1269. comparison.ok = false;
  1270. comparison.extraWindow = true;
  1271. comparison.extraRoots = rootPaths;
  1272. comparison.extraEditors = editorPaths;
  1273. } else {
  1274. const [missingRoots, extraRoots] = this.compareSets(
  1275. theSpec.roots,
  1276. rootPaths
  1277. );
  1278. const [missingEditors, extraEditors] = this.compareSets(
  1279. theSpec.editors,
  1280. editorPaths
  1281. );
  1282. comparison.ok =
  1283. missingRoots.length === 0 &&
  1284. extraRoots.length === 0 &&
  1285. missingEditors.length === 0 &&
  1286. extraEditors.length === 0;
  1287. comparison.extraRoots = extraRoots;
  1288. comparison.missingRoots = missingRoots;
  1289. comparison.extraEditors = extraEditors;
  1290. comparison.missingEditors = missingEditors;
  1291. }
  1292. return comparison;
  1293. })(window, windowSpecs[specIndex++])
  1294. );
  1295. }
  1296. const comparisons = await Promise.all(windowPromises);
  1297. for (; specIndex < windowSpecs.length; specIndex++) {
  1298. const spec = windowSpecs[specIndex];
  1299. comparisons.push({
  1300. ok: false,
  1301. extraWindow: false,
  1302. missingWindow: true,
  1303. extraRoots: [],
  1304. missingRoots: spec.roots,
  1305. extraEditors: [],
  1306. missingEditors: spec.editors,
  1307. roots: null,
  1308. editors: null
  1309. });
  1310. }
  1311. const shorthandParts = [];
  1312. const descriptionParts = [];
  1313. for (const comparison of comparisons) {
  1314. if (comparison.roots !== null && comparison.editors !== null) {
  1315. const shortRoots = Array.from(comparison.roots, r =>
  1316. path.basename(r)
  1317. ).join(',');
  1318. const shortPaths = Array.from(comparison.editors, e =>
  1319. path.basename(e)
  1320. ).join(',');
  1321. shorthandParts.push(`[${shortRoots} ${shortPaths}]`);
  1322. }
  1323. if (comparison.ok) {
  1324. continue;
  1325. }
  1326. let parts = [];
  1327. if (comparison.extraWindow) {
  1328. parts.push('extra window\n');
  1329. } else if (comparison.missingWindow) {
  1330. parts.push('missing window\n');
  1331. } else {
  1332. parts.push('incorrect window\n');
  1333. }
  1334. const shorten = fullPaths =>
  1335. fullPaths.map(fullPath => path.basename(fullPath)).join(', ');
  1336. if (comparison.extraRoots.length > 0) {
  1337. parts.push(`* extra roots ${shorten(comparison.extraRoots)}\n`);
  1338. }
  1339. if (comparison.missingRoots.length > 0) {
  1340. parts.push(`* missing roots ${shorten(comparison.missingRoots)}\n`);
  1341. }
  1342. if (comparison.extraEditors.length > 0) {
  1343. parts.push(`* extra editors ${shorten(comparison.extraEditors)}\n`);
  1344. }
  1345. if (comparison.missingEditors.length > 0) {
  1346. parts.push(`* missing editors ${shorten(comparison.missingEditors)}\n`);
  1347. }
  1348. descriptionParts.push(parts.join(''));
  1349. }
  1350. if (descriptionParts.length !== 0) {
  1351. descriptionParts.unshift(shorthandParts.join(' ') + '\n');
  1352. descriptionParts.unshift('Launched windows did not match spec\n');
  1353. }
  1354. assert.isTrue(descriptionParts.length === 0, descriptionParts.join(''));
  1355. }
  1356. async destroy() {
  1357. await Promise.all(Array.from(this.applications, app => app.destroy()));
  1358. if (this.originalAtomHome) {
  1359. process.env.ATOM_HOME = this.originalAtomHome;
  1360. }
  1361. }
  1362. addApplication(options = {}) {
  1363. const app = new AtomApplication({
  1364. resourcePath: path.resolve(__dirname, '../..'),
  1365. atomHomeDirPath: this.atomHome,
  1366. preserveFocus: true,
  1367. killProcess: pid => {
  1368. this.killedPids.push(pid);
  1369. },
  1370. ...options
  1371. });
  1372. this.sinon.stub(app, 'createWindow').callsFake(loadSettings => {
  1373. const newWindow = new StubWindow(this.sinon, loadSettings, options);
  1374. this.windows.add(newWindow);
  1375. return newWindow;
  1376. });
  1377. this.sinon
  1378. .stub(app.storageFolder, 'load')
  1379. .callsFake(() =>
  1380. Promise.resolve(
  1381. options.applicationJson || { version: '1', windows: [] }
  1382. )
  1383. );
  1384. this.sinon
  1385. .stub(app.storageFolder, 'store')
  1386. .callsFake(() => Promise.resolve());
  1387. this.applications.add(app);
  1388. return app;
  1389. }
  1390. getApplication(index) {
  1391. const app = Array.from(this.applications)[index];
  1392. if (!app) {
  1393. throw new Error(`Application ${index} does not exist`);
  1394. }
  1395. return app;
  1396. }
  1397. getWindow(index) {
  1398. const window = Array.from(this.windows)[index];
  1399. if (!window) {
  1400. throw new Error(`Window ${index} does not exist`);
  1401. }
  1402. return window;
  1403. }
  1404. compareSets(expected, actual) {
  1405. const expectedItems = new Set(expected);
  1406. const extra = [];
  1407. const missing = [];
  1408. for (const actualItem of actual) {
  1409. if (!expectedItems.delete(actualItem)) {
  1410. // actualItem was present, but not expected
  1411. extra.push(actualItem);
  1412. }
  1413. }
  1414. for (const remainingItem of expectedItems) {
  1415. // remainingItem was expected, but not present
  1416. missing.push(remainingItem);
  1417. }
  1418. return [missing, extra];
  1419. }
  1420. convertRootPath(shortRootPath) {
  1421. if (
  1422. shortRootPath.startsWith('atom://') ||
  1423. shortRootPath.startsWith('remote://')
  1424. ) {
  1425. return shortRootPath;
  1426. }
  1427. const fullRootPath = this.projectRootPool.get(shortRootPath);
  1428. if (!fullRootPath) {
  1429. throw new Error(`Unexpected short project root path: ${shortRootPath}`);
  1430. }
  1431. return fullRootPath;
  1432. }
  1433. convertEditorPath(shortEditorPath) {
  1434. const [truncatedPath, ...suffix] = shortEditorPath.split(/(?=:)/);
  1435. const fullEditorPath = this.filePathPool.get(truncatedPath);
  1436. if (!fullEditorPath) {
  1437. throw new Error(`Unexpected short editor path: ${shortEditorPath}`);
  1438. }
  1439. return fullEditorPath + suffix.join('');
  1440. }
  1441. convertPaths(paths) {
  1442. return paths.map(shortPath => {
  1443. if (
  1444. shortPath.startsWith('atom://') ||
  1445. shortPath.startsWith('remote://')
  1446. ) {
  1447. return shortPath;
  1448. }
  1449. const fullRoot = this.projectRootPool.get(shortPath);
  1450. if (fullRoot) {
  1451. return fullRoot;
  1452. }
  1453. const [truncatedPath, ...suffix] = shortPath.split(/(?=:)/);
  1454. const fullEditor = this.filePathPool.get(truncatedPath);
  1455. if (fullEditor) {
  1456. return fullEditor + suffix.join('');
  1457. }
  1458. throw new Error(`Unexpected short path: ${shortPath}`);
  1459. });
  1460. }
  1461. parseWindowSpecs(source) {
  1462. const specs = [];
  1463. const rx = /\s*\[(?:_|(\S+)) (?:_|(\S+))\]/g;
  1464. let match = rx.exec(source);
  1465. while (match) {
  1466. const roots = match[1]
  1467. ? match[1].split(',').map(shortPath => this.convertRootPath(shortPath))
  1468. : [];
  1469. const editors = match[2]
  1470. ? match[2]
  1471. .split(',')
  1472. .map(shortPath => this.convertEditorPath(shortPath))
  1473. : [];
  1474. specs.push({ roots, editors });
  1475. match = rx.exec(source);
  1476. }
  1477. return specs;
  1478. }
  1479. }