pane-spec.js 59 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731
  1. const { extend } = require('underscore-plus');
  2. const { Emitter } = require('event-kit');
  3. const Grim = require('grim');
  4. const Pane = require('../src/pane');
  5. const PaneContainer = require('../src/pane-container');
  6. const { conditionPromise, timeoutPromise } = require('./async-spec-helpers');
  7. describe('Pane', () => {
  8. let confirm, showSaveDialog, deserializerDisposable;
  9. class Item {
  10. static deserialize({ name, uri }) {
  11. return new Item(name, uri);
  12. }
  13. constructor(name, uri) {
  14. this.name = name;
  15. this.uri = uri;
  16. this.emitter = new Emitter();
  17. this.destroyed = false;
  18. }
  19. getURI() {
  20. return this.uri;
  21. }
  22. getPath() {
  23. return this.path;
  24. }
  25. isEqual(other) {
  26. return this.name === (other && other.name);
  27. }
  28. isPermanentDockItem() {
  29. return false;
  30. }
  31. isDestroyed() {
  32. return this.destroyed;
  33. }
  34. serialize() {
  35. return { deserializer: 'Item', name: this.name, uri: this.uri };
  36. }
  37. copy() {
  38. return new Item(this.name, this.uri);
  39. }
  40. destroy() {
  41. this.destroyed = true;
  42. return this.emitter.emit('did-destroy');
  43. }
  44. onDidDestroy(fn) {
  45. return this.emitter.on('did-destroy', fn);
  46. }
  47. onDidTerminatePendingState(callback) {
  48. return this.emitter.on('terminate-pending-state', callback);
  49. }
  50. terminatePendingState() {
  51. return this.emitter.emit('terminate-pending-state');
  52. }
  53. }
  54. beforeEach(() => {
  55. confirm = spyOn(atom.applicationDelegate, 'confirm');
  56. showSaveDialog = spyOn(atom.applicationDelegate, 'showSaveDialog');
  57. deserializerDisposable = atom.deserializers.add(Item);
  58. });
  59. afterEach(() => {
  60. deserializerDisposable.dispose();
  61. });
  62. function paneParams(params) {
  63. return extend(
  64. {
  65. applicationDelegate: atom.applicationDelegate,
  66. config: atom.config,
  67. deserializerManager: atom.deserializers,
  68. notificationManager: atom.notifications
  69. },
  70. params
  71. );
  72. }
  73. describe('construction', () => {
  74. it('sets the active item to the first item', () => {
  75. const pane = new Pane(
  76. paneParams({ items: [new Item('A'), new Item('B')] })
  77. );
  78. expect(pane.getActiveItem()).toBe(pane.itemAtIndex(0));
  79. });
  80. it('compacts the items array', () => {
  81. const pane = new Pane(
  82. paneParams({ items: [undefined, new Item('A'), null, new Item('B')] })
  83. );
  84. expect(pane.getItems().length).toBe(2);
  85. expect(pane.getActiveItem()).toBe(pane.itemAtIndex(0));
  86. });
  87. });
  88. describe('::activate()', () => {
  89. let container, pane1, pane2;
  90. beforeEach(() => {
  91. container = new PaneContainer({
  92. location: 'center',
  93. config: atom.config,
  94. applicationDelegate: atom.applicationDelegate
  95. });
  96. container.getActivePane().splitRight();
  97. [pane1, pane2] = container.getPanes();
  98. });
  99. it('changes the active pane on the container', () => {
  100. expect(container.getActivePane()).toBe(pane2);
  101. pane1.activate();
  102. expect(container.getActivePane()).toBe(pane1);
  103. pane2.activate();
  104. expect(container.getActivePane()).toBe(pane2);
  105. });
  106. it('invokes ::onDidChangeActivePane observers on the container', () => {
  107. const observed = [];
  108. container.onDidChangeActivePane(activePane => observed.push(activePane));
  109. pane1.activate();
  110. pane1.activate();
  111. pane2.activate();
  112. pane1.activate();
  113. expect(observed).toEqual([pane1, pane2, pane1]);
  114. });
  115. it('invokes ::onDidChangeActive observers on the relevant panes', () => {
  116. const observed = [];
  117. pane1.onDidChangeActive(active => observed.push(active));
  118. pane1.activate();
  119. pane2.activate();
  120. expect(observed).toEqual([true, false]);
  121. });
  122. it('invokes ::onDidActivate() observers', () => {
  123. let eventCount = 0;
  124. pane1.onDidActivate(() => eventCount++);
  125. pane1.activate();
  126. pane1.activate();
  127. pane2.activate();
  128. expect(eventCount).toBe(2);
  129. });
  130. });
  131. describe('::addItem(item, index)', () => {
  132. it('adds the item at the given index', () => {
  133. const pane = new Pane(
  134. paneParams({ items: [new Item('A'), new Item('B')] })
  135. );
  136. const [item1, item2] = pane.getItems();
  137. const item3 = new Item('C');
  138. pane.addItem(item3, { index: 1 });
  139. expect(pane.getItems()).toEqual([item1, item3, item2]);
  140. });
  141. it('adds the item after the active item if no index is provided', () => {
  142. const pane = new Pane(
  143. paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] })
  144. );
  145. const [item1, item2, item3] = pane.getItems();
  146. pane.activateItem(item2);
  147. const item4 = new Item('D');
  148. pane.addItem(item4);
  149. expect(pane.getItems()).toEqual([item1, item2, item4, item3]);
  150. });
  151. it('sets the active item after adding the first item', () => {
  152. const pane = new Pane(paneParams());
  153. const item = new Item('A');
  154. pane.addItem(item);
  155. expect(pane.getActiveItem()).toBe(item);
  156. });
  157. it('invokes ::onDidAddItem() observers', () => {
  158. const pane = new Pane(
  159. paneParams({ items: [new Item('A'), new Item('B')] })
  160. );
  161. const events = [];
  162. pane.onDidAddItem(event => events.push(event));
  163. const item = new Item('C');
  164. pane.addItem(item, { index: 1 });
  165. expect(events).toEqual([{ item, index: 1, moved: false }]);
  166. });
  167. it('throws an exception if the item is already present on a pane', () => {
  168. const item = new Item('A');
  169. const container = new PaneContainer({
  170. config: atom.config,
  171. applicationDelegate: atom.applicationDelegate
  172. });
  173. const pane1 = container.getActivePane();
  174. pane1.addItem(item);
  175. const pane2 = pane1.splitRight();
  176. expect(() => pane2.addItem(item)).toThrow();
  177. });
  178. it("throws an exception if the item isn't an object", () => {
  179. const pane = new Pane(paneParams({ items: [] }));
  180. expect(() => pane.addItem(null)).toThrow();
  181. expect(() => pane.addItem('foo')).toThrow();
  182. expect(() => pane.addItem(1)).toThrow();
  183. });
  184. it('destroys any existing pending item', () => {
  185. const pane = new Pane(paneParams({ items: [] }));
  186. const itemA = new Item('A');
  187. const itemB = new Item('B');
  188. const itemC = new Item('C');
  189. pane.addItem(itemA, { pending: false });
  190. pane.addItem(itemB, { pending: true });
  191. pane.addItem(itemC, { pending: false });
  192. expect(itemB.isDestroyed()).toBe(true);
  193. });
  194. it('adds the new item before destroying any existing pending item', () => {
  195. const eventOrder = [];
  196. const pane = new Pane(paneParams({ items: [] }));
  197. const itemA = new Item('A');
  198. const itemB = new Item('B');
  199. pane.addItem(itemA, { pending: true });
  200. pane.onDidAddItem(function({ item }) {
  201. if (item === itemB) eventOrder.push('add');
  202. });
  203. pane.onDidRemoveItem(function({ item }) {
  204. if (item === itemA) eventOrder.push('remove');
  205. });
  206. pane.addItem(itemB);
  207. waitsFor(() => eventOrder.length === 2);
  208. runs(() => expect(eventOrder).toEqual(['add', 'remove']));
  209. });
  210. it('subscribes to be notified when item terminates its pending state', () => {
  211. const fakeDisposable = { dispose: () => {} };
  212. const spy = jasmine
  213. .createSpy('onDidTerminatePendingState')
  214. .andReturn(fakeDisposable);
  215. const pane = new Pane(paneParams({ items: [] }));
  216. const item = {
  217. getTitle: () => '',
  218. onDidTerminatePendingState: spy
  219. };
  220. pane.addItem(item);
  221. expect(spy).toHaveBeenCalled();
  222. });
  223. it('subscribes to be notified when item is destroyed', () => {
  224. const fakeDisposable = { dispose: () => {} };
  225. const spy = jasmine.createSpy('onDidDestroy').andReturn(fakeDisposable);
  226. const pane = new Pane(paneParams({ items: [] }));
  227. const item = {
  228. getTitle: () => '',
  229. onDidDestroy: spy
  230. };
  231. pane.addItem(item);
  232. expect(spy).toHaveBeenCalled();
  233. });
  234. describe('when using the old API of ::addItem(item, index)', () => {
  235. beforeEach(() => spyOn(Grim, 'deprecate'));
  236. it('supports the older public API', () => {
  237. const pane = new Pane(paneParams({ items: [] }));
  238. const itemA = new Item('A');
  239. const itemB = new Item('B');
  240. const itemC = new Item('C');
  241. pane.addItem(itemA, 0);
  242. pane.addItem(itemB, 0);
  243. pane.addItem(itemC, 0);
  244. expect(pane.getItems()).toEqual([itemC, itemB, itemA]);
  245. });
  246. it('shows a deprecation warning', () => {
  247. const pane = new Pane(paneParams({ items: [] }));
  248. pane.addItem(new Item(), 2);
  249. expect(Grim.deprecate).toHaveBeenCalledWith(
  250. 'Pane::addItem(item, 2) is deprecated in favor of Pane::addItem(item, {index: 2})'
  251. );
  252. });
  253. });
  254. });
  255. describe('::activateItem(item)', () => {
  256. let pane = null;
  257. beforeEach(() => {
  258. pane = new Pane(paneParams({ items: [new Item('A'), new Item('B')] }));
  259. });
  260. it('changes the active item to the current item', () => {
  261. expect(pane.getActiveItem()).toBe(pane.itemAtIndex(0));
  262. pane.activateItem(pane.itemAtIndex(1));
  263. expect(pane.getActiveItem()).toBe(pane.itemAtIndex(1));
  264. });
  265. it("adds the given item if it isn't present in ::items", () => {
  266. const item = new Item('C');
  267. pane.activateItem(item);
  268. expect(pane.getItems().includes(item)).toBe(true);
  269. expect(pane.getActiveItem()).toBe(item);
  270. });
  271. it('invokes ::onDidChangeActiveItem() observers', () => {
  272. const observed = [];
  273. pane.onDidChangeActiveItem(item => observed.push(item));
  274. pane.activateItem(pane.itemAtIndex(1));
  275. expect(observed).toEqual([pane.itemAtIndex(1)]);
  276. });
  277. describe('when the item being activated is pending', () => {
  278. let itemC = null;
  279. let itemD = null;
  280. beforeEach(() => {
  281. itemC = new Item('C');
  282. itemD = new Item('D');
  283. });
  284. it('replaces the active item if it is pending', () => {
  285. pane.activateItem(itemC, { pending: true });
  286. expect(pane.getItems().map(item => item.name)).toEqual(['A', 'C', 'B']);
  287. pane.activateItem(itemD, { pending: true });
  288. expect(pane.getItems().map(item => item.name)).toEqual(['A', 'D', 'B']);
  289. });
  290. it('adds the item after the active item if it is not pending', () => {
  291. pane.activateItem(itemC, { pending: true });
  292. pane.activateItemAtIndex(2);
  293. pane.activateItem(itemD, { pending: true });
  294. expect(pane.getItems().map(item => item.name)).toEqual(['A', 'B', 'D']);
  295. });
  296. });
  297. });
  298. describe('::setPendingItem', () => {
  299. let pane = null;
  300. beforeEach(() => {
  301. pane = atom.workspace.getActivePane();
  302. });
  303. it('changes the pending item', () => {
  304. expect(pane.getPendingItem()).toBeNull();
  305. pane.setPendingItem('fake item');
  306. expect(pane.getPendingItem()).toEqual('fake item');
  307. });
  308. });
  309. describe('::onItemDidTerminatePendingState callback', () => {
  310. let pane = null;
  311. let callbackCalled = false;
  312. beforeEach(() => {
  313. pane = atom.workspace.getActivePane();
  314. callbackCalled = false;
  315. });
  316. it('is called when the pending item changes', () => {
  317. pane.setPendingItem('fake item one');
  318. pane.onItemDidTerminatePendingState(function(item) {
  319. callbackCalled = true;
  320. expect(item).toEqual('fake item one');
  321. });
  322. pane.setPendingItem('fake item two');
  323. expect(callbackCalled).toBeTruthy();
  324. });
  325. it('has access to the new pending item via ::getPendingItem', () => {
  326. pane.setPendingItem('fake item one');
  327. pane.onItemDidTerminatePendingState(function(item) {
  328. callbackCalled = true;
  329. expect(pane.getPendingItem()).toEqual('fake item two');
  330. });
  331. pane.setPendingItem('fake item two');
  332. expect(callbackCalled).toBeTruthy();
  333. });
  334. it("isn't called when a pending item is replaced with a new one", async () => {
  335. pane = null;
  336. const pendingSpy = jasmine.createSpy('onItemDidTerminatePendingState');
  337. const destroySpy = jasmine.createSpy('onWillDestroyItem');
  338. await atom.workspace.open('sample.txt', { pending: true });
  339. pane = atom.workspace.getActivePane();
  340. pane.onItemDidTerminatePendingState(pendingSpy);
  341. pane.onWillDestroyItem(destroySpy);
  342. await atom.workspace.open('sample.js', { pending: true });
  343. expect(destroySpy).toHaveBeenCalled();
  344. expect(pendingSpy).not.toHaveBeenCalled();
  345. });
  346. });
  347. describe('::activateNextRecentlyUsedItem() and ::activatePreviousRecentlyUsedItem()', () => {
  348. it('sets the active item to the next/previous item in the itemStack, looping around at either end', () => {
  349. const pane = new Pane(
  350. paneParams({
  351. items: [
  352. new Item('A'),
  353. new Item('B'),
  354. new Item('C'),
  355. new Item('D'),
  356. new Item('E')
  357. ]
  358. })
  359. );
  360. const [item1, item2, item3, item4, item5] = pane.getItems();
  361. pane.itemStack = [item3, item1, item2, item5, item4];
  362. pane.activateItem(item4);
  363. expect(pane.getActiveItem()).toBe(item4);
  364. pane.activateNextRecentlyUsedItem();
  365. expect(pane.getActiveItem()).toBe(item5);
  366. pane.activateNextRecentlyUsedItem();
  367. expect(pane.getActiveItem()).toBe(item2);
  368. pane.activatePreviousRecentlyUsedItem();
  369. expect(pane.getActiveItem()).toBe(item5);
  370. pane.activatePreviousRecentlyUsedItem();
  371. expect(pane.getActiveItem()).toBe(item4);
  372. pane.activatePreviousRecentlyUsedItem();
  373. expect(pane.getActiveItem()).toBe(item3);
  374. pane.activatePreviousRecentlyUsedItem();
  375. expect(pane.getActiveItem()).toBe(item1);
  376. pane.activateNextRecentlyUsedItem();
  377. expect(pane.getActiveItem()).toBe(item3);
  378. pane.activateNextRecentlyUsedItem();
  379. expect(pane.getActiveItem()).toBe(item4);
  380. pane.activateNextRecentlyUsedItem();
  381. pane.moveActiveItemToTopOfStack();
  382. expect(pane.getActiveItem()).toBe(item5);
  383. expect(pane.itemStack[4]).toBe(item5);
  384. });
  385. });
  386. describe('::activateNextItem() and ::activatePreviousItem()', () => {
  387. it('sets the active item to the next/previous item, looping around at either end', () => {
  388. const pane = new Pane(
  389. paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] })
  390. );
  391. const [item1, item2, item3] = pane.getItems();
  392. expect(pane.getActiveItem()).toBe(item1);
  393. pane.activatePreviousItem();
  394. expect(pane.getActiveItem()).toBe(item3);
  395. pane.activatePreviousItem();
  396. expect(pane.getActiveItem()).toBe(item2);
  397. pane.activateNextItem();
  398. expect(pane.getActiveItem()).toBe(item3);
  399. pane.activateNextItem();
  400. expect(pane.getActiveItem()).toBe(item1);
  401. });
  402. });
  403. describe('::activateLastItem()', () => {
  404. it('sets the active item to the last item', () => {
  405. const pane = new Pane(
  406. paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] })
  407. );
  408. const [item1, , item3] = pane.getItems();
  409. expect(pane.getActiveItem()).toBe(item1);
  410. pane.activateLastItem();
  411. expect(pane.getActiveItem()).toBe(item3);
  412. });
  413. });
  414. describe('::moveItemRight() and ::moveItemLeft()', () => {
  415. it('moves the active item to the right and left, without looping around at either end', () => {
  416. const pane = new Pane(
  417. paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] })
  418. );
  419. const [item1, item2, item3] = pane.getItems();
  420. pane.activateItemAtIndex(0);
  421. expect(pane.getActiveItem()).toBe(item1);
  422. pane.moveItemLeft();
  423. expect(pane.getItems()).toEqual([item1, item2, item3]);
  424. pane.moveItemRight();
  425. expect(pane.getItems()).toEqual([item2, item1, item3]);
  426. pane.moveItemLeft();
  427. expect(pane.getItems()).toEqual([item1, item2, item3]);
  428. pane.activateItemAtIndex(2);
  429. expect(pane.getActiveItem()).toBe(item3);
  430. pane.moveItemRight();
  431. expect(pane.getItems()).toEqual([item1, item2, item3]);
  432. });
  433. });
  434. describe('::activateItemAtIndex(index)', () => {
  435. it('activates the item at the given index', () => {
  436. const pane = new Pane(
  437. paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] })
  438. );
  439. const [item1, item2, item3] = pane.getItems();
  440. pane.activateItemAtIndex(2);
  441. expect(pane.getActiveItem()).toBe(item3);
  442. pane.activateItemAtIndex(1);
  443. expect(pane.getActiveItem()).toBe(item2);
  444. pane.activateItemAtIndex(0);
  445. expect(pane.getActiveItem()).toBe(item1);
  446. // Doesn't fail with out-of-bounds indices
  447. pane.activateItemAtIndex(100);
  448. expect(pane.getActiveItem()).toBe(item1);
  449. pane.activateItemAtIndex(-1);
  450. expect(pane.getActiveItem()).toBe(item1);
  451. });
  452. });
  453. describe('::destroyItem(item)', () => {
  454. let pane, item1, item2, item3;
  455. beforeEach(() => {
  456. pane = new Pane(
  457. paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] })
  458. );
  459. [item1, item2, item3] = pane.getItems();
  460. });
  461. it('removes the item from the items list and destroys it', () => {
  462. expect(pane.getActiveItem()).toBe(item1);
  463. pane.destroyItem(item2);
  464. expect(pane.getItems().includes(item2)).toBe(false);
  465. expect(item2.isDestroyed()).toBe(true);
  466. expect(pane.getActiveItem()).toBe(item1);
  467. pane.destroyItem(item1);
  468. expect(pane.getItems().includes(item1)).toBe(false);
  469. expect(item1.isDestroyed()).toBe(true);
  470. });
  471. it('removes the item from the itemStack', () => {
  472. pane.itemStack = [item2, item3, item1];
  473. pane.activateItem(item1);
  474. expect(pane.getActiveItem()).toBe(item1);
  475. pane.destroyItem(item3);
  476. expect(pane.itemStack).toEqual([item2, item1]);
  477. expect(pane.getActiveItem()).toBe(item1);
  478. pane.destroyItem(item1);
  479. expect(pane.itemStack).toEqual([item2]);
  480. expect(pane.getActiveItem()).toBe(item2);
  481. pane.destroyItem(item2);
  482. expect(pane.itemStack).toEqual([]);
  483. expect(pane.getActiveItem()).toBeUndefined();
  484. });
  485. it('does nothing if prevented', () => {
  486. const container = new PaneContainer({
  487. config: atom.config,
  488. deserializerManager: atom.deserializers,
  489. applicationDelegate: atom.applicationDelegate
  490. });
  491. pane.setContainer(container);
  492. container.onWillDestroyPaneItem(e => e.prevent());
  493. pane.itemStack = [item2, item3, item1];
  494. pane.activateItem(item1);
  495. expect(pane.getActiveItem()).toBe(item1);
  496. pane.destroyItem(item3);
  497. expect(pane.itemStack).toEqual([item2, item3, item1]);
  498. expect(pane.getActiveItem()).toBe(item1);
  499. pane.destroyItem(item1);
  500. expect(pane.itemStack).toEqual([item2, item3, item1]);
  501. expect(pane.getActiveItem()).toBe(item1);
  502. pane.destroyItem(item2);
  503. expect(pane.itemStack).toEqual([item2, item3, item1]);
  504. expect(pane.getActiveItem()).toBe(item1);
  505. });
  506. it('invokes ::onWillDestroyItem() and PaneContainer::onWillDestroyPaneItem observers before destroying the item', async () => {
  507. jasmine.useRealClock();
  508. pane.container = new PaneContainer({ config: atom.config, confirm });
  509. const events = [];
  510. pane.onWillDestroyItem(async event => {
  511. expect(item2.isDestroyed()).toBe(false);
  512. await timeoutPromise(50);
  513. expect(item2.isDestroyed()).toBe(false);
  514. events.push(['will-destroy-item', event]);
  515. });
  516. pane.container.onWillDestroyPaneItem(async event => {
  517. expect(item2.isDestroyed()).toBe(false);
  518. await timeoutPromise(50);
  519. expect(item2.isDestroyed()).toBe(false);
  520. events.push(['will-destroy-pane-item', event]);
  521. });
  522. await pane.destroyItem(item2);
  523. expect(item2.isDestroyed()).toBe(true);
  524. expect(events[0][0]).toEqual('will-destroy-item');
  525. expect(events[0][1].item).toEqual(item2);
  526. expect(events[0][1].index).toEqual(1);
  527. expect(events[1][0]).toEqual('will-destroy-pane-item');
  528. expect(events[1][1].item).toEqual(item2);
  529. expect(events[1][1].index).toEqual(1);
  530. expect(typeof events[1][1].prevent).toEqual('function');
  531. expect(events[1][1].pane).toEqual(pane);
  532. });
  533. it('invokes ::onWillRemoveItem() observers', () => {
  534. const events = [];
  535. pane.onWillRemoveItem(event => events.push(event));
  536. pane.destroyItem(item2);
  537. expect(events).toEqual([
  538. { item: item2, index: 1, moved: false, destroyed: true }
  539. ]);
  540. });
  541. it('invokes ::onDidRemoveItem() observers', () => {
  542. const events = [];
  543. pane.onDidRemoveItem(event => events.push(event));
  544. pane.destroyItem(item2);
  545. expect(events).toEqual([
  546. { item: item2, index: 1, moved: false, destroyed: true }
  547. ]);
  548. });
  549. describe('when the destroyed item is the active item and is the first item', () => {
  550. it('activates the next item', () => {
  551. expect(pane.getActiveItem()).toBe(item1);
  552. pane.destroyItem(item1);
  553. expect(pane.getActiveItem()).toBe(item2);
  554. });
  555. });
  556. describe('when the destroyed item is the active item and is not the first item', () => {
  557. beforeEach(() => pane.activateItem(item2));
  558. it('activates the previous item', () => {
  559. expect(pane.getActiveItem()).toBe(item2);
  560. pane.destroyItem(item2);
  561. expect(pane.getActiveItem()).toBe(item1);
  562. });
  563. });
  564. describe('if the item is modified', () => {
  565. let itemURI = null;
  566. beforeEach(() => {
  567. item1.shouldPromptToSave = () => true;
  568. item1.save = jasmine.createSpy('save');
  569. item1.saveAs = jasmine.createSpy('saveAs');
  570. item1.getURI = () => itemURI;
  571. });
  572. describe('if the [Save] option is selected', () => {
  573. describe('when the item has a uri', () => {
  574. it('saves the item before destroying it', async () => {
  575. itemURI = 'test';
  576. confirm.andCallFake((options, callback) => callback(0));
  577. const success = await pane.destroyItem(item1);
  578. expect(item1.save).toHaveBeenCalled();
  579. expect(pane.getItems().includes(item1)).toBe(false);
  580. expect(item1.isDestroyed()).toBe(true);
  581. expect(success).toBe(true);
  582. });
  583. });
  584. describe('when the item has no uri', () => {
  585. it('presents a save-as dialog, then saves the item with the given uri before removing and destroying it', async () => {
  586. jasmine.useRealClock();
  587. itemURI = null;
  588. showSaveDialog.andCallFake((options, callback) =>
  589. callback('/selected/path')
  590. );
  591. confirm.andCallFake((options, callback) => callback(0));
  592. const success = await pane.destroyItem(item1);
  593. expect(showSaveDialog.mostRecentCall.args[0]).toEqual({});
  594. await conditionPromise(() => item1.saveAs.callCount === 1);
  595. expect(item1.saveAs).toHaveBeenCalledWith('/selected/path');
  596. expect(pane.getItems().includes(item1)).toBe(false);
  597. expect(item1.isDestroyed()).toBe(true);
  598. expect(success).toBe(true);
  599. });
  600. });
  601. });
  602. describe("if the [Don't Save] option is selected", () => {
  603. it('removes and destroys the item without saving it', async () => {
  604. confirm.andCallFake((options, callback) => callback(2));
  605. const success = await pane.destroyItem(item1);
  606. expect(item1.save).not.toHaveBeenCalled();
  607. expect(pane.getItems().includes(item1)).toBe(false);
  608. expect(item1.isDestroyed()).toBe(true);
  609. expect(success).toBe(true);
  610. });
  611. });
  612. describe('if the [Cancel] option is selected', () => {
  613. it('does not save, remove, or destroy the item', async () => {
  614. confirm.andCallFake((options, callback) => callback(1));
  615. const success = await pane.destroyItem(item1);
  616. expect(item1.save).not.toHaveBeenCalled();
  617. expect(pane.getItems().includes(item1)).toBe(true);
  618. expect(item1.isDestroyed()).toBe(false);
  619. expect(success).toBe(false);
  620. });
  621. });
  622. describe('when force=true', () => {
  623. it('destroys the item immediately', async () => {
  624. const success = await pane.destroyItem(item1, true);
  625. expect(item1.save).not.toHaveBeenCalled();
  626. expect(pane.getItems().includes(item1)).toBe(false);
  627. expect(item1.isDestroyed()).toBe(true);
  628. expect(success).toBe(true);
  629. });
  630. });
  631. });
  632. describe('when the last item is destroyed', () => {
  633. describe("when the 'core.destroyEmptyPanes' config option is false (the default)", () => {
  634. it('does not destroy the pane, but leaves it in place with empty items', () => {
  635. expect(atom.config.get('core.destroyEmptyPanes')).toBe(false);
  636. for (let item of pane.getItems()) {
  637. pane.destroyItem(item);
  638. }
  639. expect(pane.isDestroyed()).toBe(false);
  640. expect(pane.getActiveItem()).toBeUndefined();
  641. expect(() => pane.saveActiveItem()).not.toThrow();
  642. expect(() => pane.saveActiveItemAs()).not.toThrow();
  643. });
  644. });
  645. describe("when the 'core.destroyEmptyPanes' config option is true", () => {
  646. it('destroys the pane', () => {
  647. atom.config.set('core.destroyEmptyPanes', true);
  648. for (let item of pane.getItems()) {
  649. pane.destroyItem(item);
  650. }
  651. expect(pane.isDestroyed()).toBe(true);
  652. });
  653. });
  654. });
  655. describe('when passed a permanent dock item', () => {
  656. it("doesn't destroy the item", async () => {
  657. spyOn(item1, 'isPermanentDockItem').andReturn(true);
  658. const success = await pane.destroyItem(item1);
  659. expect(pane.getItems().includes(item1)).toBe(true);
  660. expect(item1.isDestroyed()).toBe(false);
  661. expect(success).toBe(false);
  662. });
  663. it('destroy the item if force=true', async () => {
  664. spyOn(item1, 'isPermanentDockItem').andReturn(true);
  665. const success = await pane.destroyItem(item1, true);
  666. expect(pane.getItems().includes(item1)).toBe(false);
  667. expect(item1.isDestroyed()).toBe(true);
  668. expect(success).toBe(true);
  669. });
  670. });
  671. });
  672. describe('::destroyActiveItem()', () => {
  673. it('destroys the active item', () => {
  674. const pane = new Pane(
  675. paneParams({ items: [new Item('A'), new Item('B')] })
  676. );
  677. const activeItem = pane.getActiveItem();
  678. pane.destroyActiveItem();
  679. expect(activeItem.isDestroyed()).toBe(true);
  680. expect(pane.getItems().includes(activeItem)).toBe(false);
  681. });
  682. it('does not throw an exception if there are no more items', () => {
  683. const pane = new Pane(paneParams());
  684. pane.destroyActiveItem();
  685. });
  686. });
  687. describe('::destroyItems()', () => {
  688. it('destroys all items', async () => {
  689. const pane = new Pane(
  690. paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] })
  691. );
  692. const [item1, item2, item3] = pane.getItems();
  693. await pane.destroyItems();
  694. expect(item1.isDestroyed()).toBe(true);
  695. expect(item2.isDestroyed()).toBe(true);
  696. expect(item3.isDestroyed()).toBe(true);
  697. expect(pane.getItems()).toEqual([]);
  698. });
  699. });
  700. describe('::observeItems()', () => {
  701. it('invokes the observer with all current and future items', () => {
  702. const pane = new Pane(paneParams({ items: [new Item(), new Item()] }));
  703. const [item1, item2] = pane.getItems();
  704. const observed = [];
  705. pane.observeItems(item => observed.push(item));
  706. const item3 = new Item();
  707. pane.addItem(item3);
  708. expect(observed).toEqual([item1, item2, item3]);
  709. });
  710. });
  711. describe('when an item emits a destroyed event', () => {
  712. it('removes it from the list of items', () => {
  713. const pane = new Pane(
  714. paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] })
  715. );
  716. const [item1, , item3] = pane.getItems();
  717. pane.itemAtIndex(1).destroy();
  718. expect(pane.getItems()).toEqual([item1, item3]);
  719. });
  720. });
  721. describe('::destroyInactiveItems()', () => {
  722. it('destroys all items but the active item', () => {
  723. const pane = new Pane(
  724. paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] })
  725. );
  726. const [, item2] = pane.getItems();
  727. pane.activateItem(item2);
  728. pane.destroyInactiveItems();
  729. expect(pane.getItems()).toEqual([item2]);
  730. });
  731. });
  732. describe('::saveActiveItem()', () => {
  733. let pane;
  734. beforeEach(() => {
  735. pane = new Pane(paneParams({ items: [new Item('A')] }));
  736. showSaveDialog.andCallFake((options, callback) =>
  737. callback('/selected/path')
  738. );
  739. });
  740. describe('when the active item has a uri', () => {
  741. beforeEach(() => {
  742. pane.getActiveItem().uri = 'test';
  743. });
  744. describe('when the active item has a save method', () => {
  745. it('saves the current item', () => {
  746. pane.getActiveItem().save = jasmine.createSpy('save');
  747. pane.saveActiveItem();
  748. expect(pane.getActiveItem().save).toHaveBeenCalled();
  749. });
  750. });
  751. describe('when the current item has no save method', () => {
  752. it('does nothing', () => {
  753. expect(pane.getActiveItem().save).toBeUndefined();
  754. pane.saveActiveItem();
  755. });
  756. });
  757. });
  758. describe('when the current item has no uri', () => {
  759. describe('when the current item has a saveAs method', () => {
  760. it('opens a save dialog and saves the current item as the selected path', async () => {
  761. pane.getActiveItem().saveAs = jasmine.createSpy('saveAs');
  762. await pane.saveActiveItem();
  763. expect(showSaveDialog.mostRecentCall.args[0]).toEqual({});
  764. expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith(
  765. '/selected/path'
  766. );
  767. });
  768. });
  769. describe('when the current item has no saveAs method', () => {
  770. it('does nothing', async () => {
  771. expect(pane.getActiveItem().saveAs).toBeUndefined();
  772. await pane.saveActiveItem();
  773. expect(showSaveDialog).not.toHaveBeenCalled();
  774. });
  775. });
  776. it('does nothing if the user cancels choosing a path', async () => {
  777. pane.getActiveItem().saveAs = jasmine.createSpy('saveAs');
  778. showSaveDialog.andCallFake((options, callback) => callback(undefined));
  779. await pane.saveActiveItem();
  780. expect(pane.getActiveItem().saveAs).not.toHaveBeenCalled();
  781. });
  782. });
  783. describe("when the item's saveAs rejects with a well-known IO error", () => {
  784. it('creates a notification', () => {
  785. pane.getActiveItem().saveAs = () => {
  786. const error = new Error("EACCES, permission denied '/foo'");
  787. error.path = '/foo';
  788. error.code = 'EACCES';
  789. return Promise.reject(error);
  790. };
  791. waitsFor(done => {
  792. const subscription = atom.notifications.onDidAddNotification(function(
  793. notification
  794. ) {
  795. expect(notification.getType()).toBe('warning');
  796. expect(notification.getMessage()).toContain('Permission denied');
  797. expect(notification.getMessage()).toContain('/foo');
  798. subscription.dispose();
  799. done();
  800. });
  801. pane.saveActiveItem();
  802. });
  803. });
  804. });
  805. describe("when the item's saveAs throws a well-known IO error", () => {
  806. it('creates a notification', () => {
  807. pane.getActiveItem().saveAs = () => {
  808. const error = new Error("EACCES, permission denied '/foo'");
  809. error.path = '/foo';
  810. error.code = 'EACCES';
  811. throw error;
  812. };
  813. waitsFor(done => {
  814. const subscription = atom.notifications.onDidAddNotification(function(
  815. notification
  816. ) {
  817. expect(notification.getType()).toBe('warning');
  818. expect(notification.getMessage()).toContain('Permission denied');
  819. expect(notification.getMessage()).toContain('/foo');
  820. subscription.dispose();
  821. done();
  822. });
  823. pane.saveActiveItem();
  824. });
  825. });
  826. });
  827. });
  828. describe('::saveActiveItemAs()', () => {
  829. let pane = null;
  830. beforeEach(() => {
  831. pane = new Pane(paneParams({ items: [new Item('A')] }));
  832. showSaveDialog.andCallFake((options, callback) =>
  833. callback('/selected/path')
  834. );
  835. });
  836. describe('when the current item has a saveAs method', () => {
  837. it('opens the save dialog and calls saveAs on the item with the selected path', async () => {
  838. jasmine.useRealClock();
  839. pane.getActiveItem().path = __filename;
  840. pane.getActiveItem().saveAs = jasmine.createSpy('saveAs');
  841. pane.saveActiveItemAs();
  842. expect(showSaveDialog.mostRecentCall.args[0]).toEqual({
  843. defaultPath: __filename
  844. });
  845. await conditionPromise(
  846. () => pane.getActiveItem().saveAs.callCount === 1
  847. );
  848. expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith(
  849. '/selected/path'
  850. );
  851. });
  852. });
  853. describe('when the current item does not have a saveAs method', () => {
  854. it('does nothing', () => {
  855. expect(pane.getActiveItem().saveAs).toBeUndefined();
  856. pane.saveActiveItemAs();
  857. expect(showSaveDialog).not.toHaveBeenCalled();
  858. });
  859. });
  860. describe("when the item's saveAs method throws a well-known IO error", () => {
  861. it('creates a notification', () => {
  862. pane.getActiveItem().saveAs = () => {
  863. const error = new Error("EACCES, permission denied '/foo'");
  864. error.path = '/foo';
  865. error.code = 'EACCES';
  866. return Promise.reject(error);
  867. };
  868. waitsFor(done => {
  869. const subscription = atom.notifications.onDidAddNotification(function(
  870. notification
  871. ) {
  872. expect(notification.getType()).toBe('warning');
  873. expect(notification.getMessage()).toContain('Permission denied');
  874. expect(notification.getMessage()).toContain('/foo');
  875. subscription.dispose();
  876. done();
  877. });
  878. pane.saveActiveItemAs();
  879. });
  880. });
  881. });
  882. });
  883. describe('::itemForURI(uri)', () => {
  884. it('returns the item for which a call to .getURI() returns the given uri', () => {
  885. const pane = new Pane(
  886. paneParams({
  887. items: [new Item('A'), new Item('B'), new Item('C'), new Item('D')]
  888. })
  889. );
  890. const [item1, item2] = pane.getItems();
  891. item1.uri = 'a';
  892. item2.uri = 'b';
  893. expect(pane.itemForURI('a')).toBe(item1);
  894. expect(pane.itemForURI('b')).toBe(item2);
  895. expect(pane.itemForURI('bogus')).toBeUndefined();
  896. });
  897. });
  898. describe('::moveItem(item, index)', () => {
  899. let pane, item1, item2, item3, item4;
  900. beforeEach(() => {
  901. pane = new Pane(
  902. paneParams({
  903. items: [new Item('A'), new Item('B'), new Item('C'), new Item('D')]
  904. })
  905. );
  906. [item1, item2, item3, item4] = pane.getItems();
  907. });
  908. it('moves the item to the given index and invokes ::onDidMoveItem observers', () => {
  909. pane.moveItem(item1, 2);
  910. expect(pane.getItems()).toEqual([item2, item3, item1, item4]);
  911. pane.moveItem(item2, 3);
  912. expect(pane.getItems()).toEqual([item3, item1, item4, item2]);
  913. pane.moveItem(item2, 1);
  914. expect(pane.getItems()).toEqual([item3, item2, item1, item4]);
  915. });
  916. it('invokes ::onDidMoveItem() observers', () => {
  917. const events = [];
  918. pane.onDidMoveItem(event => events.push(event));
  919. pane.moveItem(item1, 2);
  920. pane.moveItem(item2, 3);
  921. expect(events).toEqual([
  922. { item: item1, oldIndex: 0, newIndex: 2 },
  923. { item: item2, oldIndex: 0, newIndex: 3 }
  924. ]);
  925. });
  926. });
  927. describe('::moveItemToPane(item, pane, index)', () => {
  928. let container, pane1, pane2;
  929. let item1, item2, item3, item4, item5;
  930. beforeEach(() => {
  931. container = new PaneContainer({ config: atom.config, confirm });
  932. pane1 = container.getActivePane();
  933. pane1.addItems([new Item('A'), new Item('B'), new Item('C')]);
  934. pane2 = pane1.splitRight({ items: [new Item('D'), new Item('E')] });
  935. [item1, item2, item3] = pane1.getItems();
  936. [item4, item5] = pane2.getItems();
  937. });
  938. it('moves the item to the given pane at the given index', () => {
  939. pane1.moveItemToPane(item2, pane2, 1);
  940. expect(pane1.getItems()).toEqual([item1, item3]);
  941. expect(pane2.getItems()).toEqual([item4, item2, item5]);
  942. });
  943. it('invokes ::onWillRemoveItem() observers', () => {
  944. const events = [];
  945. pane1.onWillRemoveItem(event => events.push(event));
  946. pane1.moveItemToPane(item2, pane2, 1);
  947. expect(events).toEqual([
  948. { item: item2, index: 1, moved: true, destroyed: false }
  949. ]);
  950. });
  951. it('invokes ::onDidRemoveItem() observers', () => {
  952. const events = [];
  953. pane1.onDidRemoveItem(event => events.push(event));
  954. pane1.moveItemToPane(item2, pane2, 1);
  955. expect(events).toEqual([
  956. { item: item2, index: 1, moved: true, destroyed: false }
  957. ]);
  958. });
  959. it('does not invoke ::onDidAddPaneItem observers on the container', () => {
  960. const addedItems = [];
  961. container.onDidAddPaneItem(item => addedItems.push(item));
  962. pane1.moveItemToPane(item2, pane2, 1);
  963. expect(addedItems).toEqual([]);
  964. });
  965. describe('when the moved item the last item in the source pane', () => {
  966. beforeEach(() => item5.destroy());
  967. describe("when the 'core.destroyEmptyPanes' config option is false (the default)", () => {
  968. it('does not destroy the pane or the item', () => {
  969. pane2.moveItemToPane(item4, pane1, 0);
  970. expect(pane2.isDestroyed()).toBe(false);
  971. expect(item4.isDestroyed()).toBe(false);
  972. });
  973. });
  974. describe("when the 'core.destroyEmptyPanes' config option is true", () => {
  975. it('destroys the pane, but not the item', () => {
  976. atom.config.set('core.destroyEmptyPanes', true);
  977. pane2.moveItemToPane(item4, pane1, 0);
  978. expect(pane2.isDestroyed()).toBe(true);
  979. expect(item4.isDestroyed()).toBe(false);
  980. });
  981. });
  982. });
  983. describe('when the item being moved is pending', () => {
  984. it('is made permanent in the new pane', () => {
  985. const item6 = new Item('F');
  986. pane1.addItem(item6, { pending: true });
  987. expect(pane1.getPendingItem()).toEqual(item6);
  988. pane1.moveItemToPane(item6, pane2, 0);
  989. expect(pane2.getPendingItem()).not.toEqual(item6);
  990. });
  991. });
  992. describe('when the target pane has a pending item', () => {
  993. it('does not destroy the pending item', () => {
  994. const item6 = new Item('F');
  995. pane1.addItem(item6, { pending: true });
  996. expect(pane1.getPendingItem()).toEqual(item6);
  997. pane2.moveItemToPane(item5, pane1, 0);
  998. expect(pane1.getPendingItem()).toEqual(item6);
  999. });
  1000. });
  1001. });
  1002. describe('split methods', () => {
  1003. let pane1, item1, container;
  1004. beforeEach(() => {
  1005. container = new PaneContainer({
  1006. config: atom.config,
  1007. confirm,
  1008. deserializerManager: atom.deserializers
  1009. });
  1010. pane1 = container.getActivePane();
  1011. item1 = new Item('A');
  1012. pane1.addItem(item1);
  1013. });
  1014. describe('::splitLeft(params)', () => {
  1015. describe('when the parent is the container root', () => {
  1016. it('replaces itself with a row and inserts a new pane to the left of itself', () => {
  1017. const pane2 = pane1.splitLeft({ items: [new Item('B')] });
  1018. const pane3 = pane1.splitLeft({ items: [new Item('C')] });
  1019. expect(container.root.orientation).toBe('horizontal');
  1020. expect(container.root.children).toEqual([pane2, pane3, pane1]);
  1021. });
  1022. });
  1023. describe('when `moveActiveItem: true` is passed in the params', () => {
  1024. it('moves the active item', () => {
  1025. const pane2 = pane1.splitLeft({ moveActiveItem: true });
  1026. expect(pane2.getActiveItem()).toBe(item1);
  1027. });
  1028. });
  1029. describe('when `copyActiveItem: true` is passed in the params', () => {
  1030. it('duplicates the active item', () => {
  1031. const pane2 = pane1.splitLeft({ copyActiveItem: true });
  1032. expect(pane2.getActiveItem()).toEqual(pane1.getActiveItem());
  1033. });
  1034. it("does nothing if the active item doesn't implement .copy()", () => {
  1035. item1.copy = null;
  1036. const pane2 = pane1.splitLeft({ copyActiveItem: true });
  1037. expect(pane2.getActiveItem()).toBeUndefined();
  1038. });
  1039. });
  1040. describe('when the parent is a column', () => {
  1041. it('replaces itself with a row and inserts a new pane to the left of itself', () => {
  1042. pane1.splitDown();
  1043. const pane2 = pane1.splitLeft({ items: [new Item('B')] });
  1044. const pane3 = pane1.splitLeft({ items: [new Item('C')] });
  1045. const row = container.root.children[0];
  1046. expect(row.orientation).toBe('horizontal');
  1047. expect(row.children).toEqual([pane2, pane3, pane1]);
  1048. });
  1049. });
  1050. });
  1051. describe('::splitRight(params)', () => {
  1052. describe('when the parent is the container root', () => {
  1053. it('replaces itself with a row and inserts a new pane to the right of itself', () => {
  1054. const pane2 = pane1.splitRight({ items: [new Item('B')] });
  1055. const pane3 = pane1.splitRight({ items: [new Item('C')] });
  1056. expect(container.root.orientation).toBe('horizontal');
  1057. expect(container.root.children).toEqual([pane1, pane3, pane2]);
  1058. });
  1059. });
  1060. describe('when `moveActiveItem: true` is passed in the params', () => {
  1061. it('moves the active item', () => {
  1062. const pane2 = pane1.splitRight({ moveActiveItem: true });
  1063. expect(pane2.getActiveItem()).toBe(item1);
  1064. });
  1065. });
  1066. describe('when `copyActiveItem: true` is passed in the params', () => {
  1067. it('duplicates the active item', () => {
  1068. const pane2 = pane1.splitRight({ copyActiveItem: true });
  1069. expect(pane2.getActiveItem()).toEqual(pane1.getActiveItem());
  1070. });
  1071. });
  1072. describe('when the parent is a column', () => {
  1073. it('replaces itself with a row and inserts a new pane to the right of itself', () => {
  1074. pane1.splitDown();
  1075. const pane2 = pane1.splitRight({ items: [new Item('B')] });
  1076. const pane3 = pane1.splitRight({ items: [new Item('C')] });
  1077. const row = container.root.children[0];
  1078. expect(row.orientation).toBe('horizontal');
  1079. expect(row.children).toEqual([pane1, pane3, pane2]);
  1080. });
  1081. });
  1082. });
  1083. describe('::splitUp(params)', () => {
  1084. describe('when the parent is the container root', () => {
  1085. it('replaces itself with a column and inserts a new pane above itself', () => {
  1086. const pane2 = pane1.splitUp({ items: [new Item('B')] });
  1087. const pane3 = pane1.splitUp({ items: [new Item('C')] });
  1088. expect(container.root.orientation).toBe('vertical');
  1089. expect(container.root.children).toEqual([pane2, pane3, pane1]);
  1090. });
  1091. });
  1092. describe('when `moveActiveItem: true` is passed in the params', () => {
  1093. it('moves the active item', () => {
  1094. const pane2 = pane1.splitUp({ moveActiveItem: true });
  1095. expect(pane2.getActiveItem()).toBe(item1);
  1096. });
  1097. });
  1098. describe('when `copyActiveItem: true` is passed in the params', () => {
  1099. it('duplicates the active item', () => {
  1100. const pane2 = pane1.splitUp({ copyActiveItem: true });
  1101. expect(pane2.getActiveItem()).toEqual(pane1.getActiveItem());
  1102. });
  1103. });
  1104. describe('when the parent is a row', () => {
  1105. it('replaces itself with a column and inserts a new pane above itself', () => {
  1106. pane1.splitRight();
  1107. const pane2 = pane1.splitUp({ items: [new Item('B')] });
  1108. const pane3 = pane1.splitUp({ items: [new Item('C')] });
  1109. const column = container.root.children[0];
  1110. expect(column.orientation).toBe('vertical');
  1111. expect(column.children).toEqual([pane2, pane3, pane1]);
  1112. });
  1113. });
  1114. });
  1115. describe('::splitDown(params)', () => {
  1116. describe('when the parent is the container root', () => {
  1117. it('replaces itself with a column and inserts a new pane below itself', () => {
  1118. const pane2 = pane1.splitDown({ items: [new Item('B')] });
  1119. const pane3 = pane1.splitDown({ items: [new Item('C')] });
  1120. expect(container.root.orientation).toBe('vertical');
  1121. expect(container.root.children).toEqual([pane1, pane3, pane2]);
  1122. });
  1123. });
  1124. describe('when `moveActiveItem: true` is passed in the params', () => {
  1125. it('moves the active item', () => {
  1126. const pane2 = pane1.splitDown({ moveActiveItem: true });
  1127. expect(pane2.getActiveItem()).toBe(item1);
  1128. });
  1129. });
  1130. describe('when `copyActiveItem: true` is passed in the params', () => {
  1131. it('duplicates the active item', () => {
  1132. const pane2 = pane1.splitDown({ copyActiveItem: true });
  1133. expect(pane2.getActiveItem()).toEqual(pane1.getActiveItem());
  1134. });
  1135. });
  1136. describe('when the parent is a row', () => {
  1137. it('replaces itself with a column and inserts a new pane below itself', () => {
  1138. pane1.splitRight();
  1139. const pane2 = pane1.splitDown({ items: [new Item('B')] });
  1140. const pane3 = pane1.splitDown({ items: [new Item('C')] });
  1141. const column = container.root.children[0];
  1142. expect(column.orientation).toBe('vertical');
  1143. expect(column.children).toEqual([pane1, pane3, pane2]);
  1144. });
  1145. });
  1146. });
  1147. describe('when the pane is empty', () => {
  1148. describe('when `moveActiveItem: true` is passed in the params', () => {
  1149. it('gracefully ignores the moveActiveItem parameter', () => {
  1150. pane1.destroyItem(item1);
  1151. expect(pane1.getActiveItem()).toBe(undefined);
  1152. const pane2 = pane1.split('horizontal', 'before', {
  1153. moveActiveItem: true
  1154. });
  1155. expect(container.root.children).toEqual([pane2, pane1]);
  1156. expect(pane2.getActiveItem()).toBe(undefined);
  1157. });
  1158. });
  1159. describe('when `copyActiveItem: true` is passed in the params', () => {
  1160. it('gracefully ignores the copyActiveItem parameter', () => {
  1161. pane1.destroyItem(item1);
  1162. expect(pane1.getActiveItem()).toBe(undefined);
  1163. const pane2 = pane1.split('horizontal', 'before', {
  1164. copyActiveItem: true
  1165. });
  1166. expect(container.root.children).toEqual([pane2, pane1]);
  1167. expect(pane2.getActiveItem()).toBe(undefined);
  1168. });
  1169. });
  1170. });
  1171. it('activates the new pane', () => {
  1172. expect(pane1.isActive()).toBe(true);
  1173. const pane2 = pane1.splitRight();
  1174. expect(pane1.isActive()).toBe(false);
  1175. expect(pane2.isActive()).toBe(true);
  1176. });
  1177. });
  1178. describe('::close()', () => {
  1179. it('prompts to save unsaved items before destroying the pane', async () => {
  1180. const pane = new Pane(
  1181. paneParams({ items: [new Item('A'), new Item('B')] })
  1182. );
  1183. const [item1] = pane.getItems();
  1184. item1.shouldPromptToSave = () => true;
  1185. item1.getURI = () => '/test/path';
  1186. item1.save = jasmine.createSpy('save');
  1187. confirm.andCallFake((options, callback) => callback(0));
  1188. await pane.close();
  1189. expect(confirm).toHaveBeenCalled();
  1190. expect(item1.save).toHaveBeenCalled();
  1191. expect(pane.isDestroyed()).toBe(true);
  1192. });
  1193. it('does not destroy the pane if the user clicks cancel', async () => {
  1194. const pane = new Pane(
  1195. paneParams({ items: [new Item('A'), new Item('B')] })
  1196. );
  1197. const [item1] = pane.getItems();
  1198. item1.shouldPromptToSave = () => true;
  1199. item1.getURI = () => '/test/path';
  1200. item1.save = jasmine.createSpy('save');
  1201. confirm.andCallFake((options, callback) => callback(1));
  1202. await pane.close();
  1203. expect(confirm).toHaveBeenCalled();
  1204. expect(item1.save).not.toHaveBeenCalled();
  1205. expect(pane.isDestroyed()).toBe(false);
  1206. });
  1207. it('does not destroy the pane if the user starts to save but then does not choose a path', async () => {
  1208. const pane = new Pane(
  1209. paneParams({ items: [new Item('A'), new Item('B')] })
  1210. );
  1211. const [item1] = pane.getItems();
  1212. item1.shouldPromptToSave = () => true;
  1213. item1.saveAs = jasmine.createSpy('saveAs');
  1214. confirm.andCallFake((options, callback) => callback(0));
  1215. showSaveDialog.andCallFake((options, callback) => callback(undefined));
  1216. await pane.close();
  1217. expect(atom.applicationDelegate.confirm).toHaveBeenCalled();
  1218. expect(confirm.callCount).toBe(1);
  1219. expect(item1.saveAs).not.toHaveBeenCalled();
  1220. expect(pane.isDestroyed()).toBe(false);
  1221. });
  1222. describe('when item fails to save', () => {
  1223. let pane, item1;
  1224. beforeEach(() => {
  1225. pane = new Pane({
  1226. items: [new Item('A'), new Item('B')],
  1227. applicationDelegate: atom.applicationDelegate,
  1228. config: atom.config
  1229. });
  1230. [item1] = pane.getItems();
  1231. item1.shouldPromptToSave = () => true;
  1232. item1.getURI = () => '/test/path';
  1233. item1.save = jasmine.createSpy('save').andCallFake(() => {
  1234. const error = new Error("EACCES, permission denied '/test/path'");
  1235. error.path = '/test/path';
  1236. error.code = 'EACCES';
  1237. throw error;
  1238. });
  1239. });
  1240. it('does not destroy the pane if save fails and user clicks cancel', async () => {
  1241. let confirmations = 0;
  1242. confirm.andCallFake((options, callback) => {
  1243. confirmations++;
  1244. if (confirmations === 1) {
  1245. callback(0); // click save
  1246. } else {
  1247. callback(1);
  1248. }
  1249. }); // click cancel
  1250. await pane.close();
  1251. expect(atom.applicationDelegate.confirm).toHaveBeenCalled();
  1252. expect(confirmations).toBe(2);
  1253. expect(item1.save).toHaveBeenCalled();
  1254. expect(pane.isDestroyed()).toBe(false);
  1255. });
  1256. it('does destroy the pane if the user saves the file under a new name', async () => {
  1257. item1.saveAs = jasmine.createSpy('saveAs').andReturn(true);
  1258. let confirmations = 0;
  1259. confirm.andCallFake((options, callback) => {
  1260. confirmations++;
  1261. callback(0);
  1262. }); // save and then save as
  1263. showSaveDialog.andCallFake((options, callback) => callback('new/path'));
  1264. await pane.close();
  1265. expect(atom.applicationDelegate.confirm).toHaveBeenCalled();
  1266. expect(confirmations).toBe(2);
  1267. expect(
  1268. atom.applicationDelegate.showSaveDialog.mostRecentCall.args[0]
  1269. ).toEqual({});
  1270. expect(item1.save).toHaveBeenCalled();
  1271. expect(item1.saveAs).toHaveBeenCalled();
  1272. expect(pane.isDestroyed()).toBe(true);
  1273. });
  1274. it('asks again if the saveAs also fails', async () => {
  1275. item1.saveAs = jasmine.createSpy('saveAs').andCallFake(() => {
  1276. const error = new Error("EACCES, permission denied '/test/path'");
  1277. error.path = '/test/path';
  1278. error.code = 'EACCES';
  1279. throw error;
  1280. });
  1281. let confirmations = 0;
  1282. confirm.andCallFake((options, callback) => {
  1283. confirmations++;
  1284. if (confirmations < 3) {
  1285. callback(0); // save, save as, save as
  1286. } else {
  1287. callback(2); // don't save
  1288. }
  1289. });
  1290. showSaveDialog.andCallFake((options, callback) => callback('new/path'));
  1291. await pane.close();
  1292. expect(atom.applicationDelegate.confirm).toHaveBeenCalled();
  1293. expect(confirmations).toBe(3);
  1294. expect(
  1295. atom.applicationDelegate.showSaveDialog.mostRecentCall.args[0]
  1296. ).toEqual({});
  1297. expect(item1.save).toHaveBeenCalled();
  1298. expect(item1.saveAs).toHaveBeenCalled();
  1299. expect(pane.isDestroyed()).toBe(true);
  1300. });
  1301. });
  1302. });
  1303. describe('::destroy()', () => {
  1304. let container, pane1, pane2;
  1305. beforeEach(() => {
  1306. container = new PaneContainer({ config: atom.config, confirm });
  1307. pane1 = container.root;
  1308. pane1.addItems([new Item('A'), new Item('B')]);
  1309. pane2 = pane1.splitRight();
  1310. });
  1311. it('invokes ::onWillDestroy observers before destroying items', () => {
  1312. let itemsDestroyed = null;
  1313. pane1.onWillDestroy(() => {
  1314. itemsDestroyed = pane1.getItems().map(item => item.isDestroyed());
  1315. });
  1316. pane1.destroy();
  1317. expect(itemsDestroyed).toEqual([false, false]);
  1318. });
  1319. it("destroys the pane's destroyable items", () => {
  1320. const [item1, item2] = pane1.getItems();
  1321. pane1.destroy();
  1322. expect(item1.isDestroyed()).toBe(true);
  1323. expect(item2.isDestroyed()).toBe(true);
  1324. });
  1325. describe('if the pane is active', () => {
  1326. it('makes the next pane active', () => {
  1327. expect(pane2.isActive()).toBe(true);
  1328. pane2.destroy();
  1329. expect(pane1.isActive()).toBe(true);
  1330. });
  1331. });
  1332. describe("if the pane's parent has more than two children", () => {
  1333. it('removes the pane from its parent', () => {
  1334. const pane3 = pane2.splitRight();
  1335. expect(container.root.children).toEqual([pane1, pane2, pane3]);
  1336. pane2.destroy();
  1337. expect(container.root.children).toEqual([pane1, pane3]);
  1338. });
  1339. });
  1340. describe("if the pane's parent has two children", () => {
  1341. it('replaces the parent with its last remaining child', () => {
  1342. const pane3 = pane2.splitDown();
  1343. expect(container.root.children[0]).toBe(pane1);
  1344. expect(container.root.children[1].children).toEqual([pane2, pane3]);
  1345. pane3.destroy();
  1346. expect(container.root.children).toEqual([pane1, pane2]);
  1347. pane2.destroy();
  1348. expect(container.root).toBe(pane1);
  1349. });
  1350. });
  1351. });
  1352. describe('pending state', () => {
  1353. let editor1, pane, eventCount;
  1354. beforeEach(async () => {
  1355. editor1 = await atom.workspace.open('sample.txt', { pending: true });
  1356. pane = atom.workspace.getActivePane();
  1357. eventCount = 0;
  1358. editor1.onDidTerminatePendingState(() => eventCount++);
  1359. });
  1360. it('does not open file in pending state by default', async () => {
  1361. await atom.workspace.open('sample.js');
  1362. expect(pane.getPendingItem()).toBeNull();
  1363. });
  1364. it("opens file in pending state if 'pending' option is true", () => {
  1365. expect(pane.getPendingItem()).toEqual(editor1);
  1366. });
  1367. it('terminates pending state if ::terminatePendingState is invoked', () => {
  1368. editor1.terminatePendingState();
  1369. expect(pane.getPendingItem()).toBeNull();
  1370. expect(eventCount).toBe(1);
  1371. });
  1372. it('terminates pending state when buffer is changed', () => {
  1373. editor1.insertText("I'll be back!");
  1374. advanceClock(editor1.getBuffer().stoppedChangingDelay);
  1375. expect(pane.getPendingItem()).toBeNull();
  1376. expect(eventCount).toBe(1);
  1377. });
  1378. it('only calls terminate handler once when text is modified twice', async () => {
  1379. const originalText = editor1.getText();
  1380. editor1.insertText('Some text');
  1381. advanceClock(editor1.getBuffer().stoppedChangingDelay);
  1382. await editor1.save();
  1383. editor1.insertText('More text');
  1384. advanceClock(editor1.getBuffer().stoppedChangingDelay);
  1385. expect(pane.getPendingItem()).toBeNull();
  1386. expect(eventCount).toBe(1);
  1387. // Reset fixture back to original state
  1388. editor1.setText(originalText);
  1389. await editor1.save();
  1390. });
  1391. it('only calls clearPendingItem if there is a pending item to clear', () => {
  1392. spyOn(pane, 'clearPendingItem').andCallThrough();
  1393. editor1.terminatePendingState();
  1394. editor1.terminatePendingState();
  1395. expect(pane.getPendingItem()).toBeNull();
  1396. expect(pane.clearPendingItem.callCount).toBe(1);
  1397. });
  1398. });
  1399. describe('serialization', () => {
  1400. let pane = null;
  1401. beforeEach(() => {
  1402. pane = new Pane(
  1403. paneParams({
  1404. items: [new Item('A', 'a'), new Item('B', 'b'), new Item('C', 'c')],
  1405. flexScale: 2
  1406. })
  1407. );
  1408. });
  1409. it('can serialize and deserialize the pane and all its items', () => {
  1410. const newPane = Pane.deserialize(pane.serialize(), atom);
  1411. expect(newPane.getItems()).toEqual(pane.getItems());
  1412. });
  1413. it('restores the active item on deserialization', () => {
  1414. pane.activateItemAtIndex(1);
  1415. const newPane = Pane.deserialize(pane.serialize(), atom);
  1416. expect(newPane.getActiveItem()).toEqual(newPane.itemAtIndex(1));
  1417. });
  1418. it("restores the active item when it doesn't implement getURI()", () => {
  1419. pane.items[1].getURI = null;
  1420. pane.activateItemAtIndex(1);
  1421. const newPane = Pane.deserialize(pane.serialize(), atom);
  1422. expect(newPane.getActiveItem()).toEqual(newPane.itemAtIndex(1));
  1423. });
  1424. it("restores the correct item when it doesn't implement getURI() and some items weren't deserialized", () => {
  1425. const unserializable = {};
  1426. pane.addItem(unserializable, { index: 0 });
  1427. pane.items[2].getURI = null;
  1428. pane.activateItemAtIndex(2);
  1429. const newPane = Pane.deserialize(pane.serialize(), atom);
  1430. expect(newPane.getActiveItem()).toEqual(newPane.itemAtIndex(1));
  1431. });
  1432. it('does not include items that cannot be deserialized', () => {
  1433. spyOn(console, 'warn');
  1434. const unserializable = {};
  1435. pane.activateItem(unserializable);
  1436. const newPane = Pane.deserialize(pane.serialize(), atom);
  1437. expect(newPane.getActiveItem()).toEqual(pane.itemAtIndex(0));
  1438. expect(newPane.getItems().length).toBe(pane.getItems().length - 1);
  1439. });
  1440. it("includes the pane's focus state in the serialized state", () => {
  1441. pane.focus();
  1442. const newPane = Pane.deserialize(pane.serialize(), atom);
  1443. expect(newPane.focused).toBe(true);
  1444. });
  1445. it('can serialize and deserialize the order of the items in the itemStack', () => {
  1446. const [item1, item2, item3] = pane.getItems();
  1447. pane.itemStack = [item3, item1, item2];
  1448. const newPane = Pane.deserialize(pane.serialize(), atom);
  1449. expect(newPane.itemStack).toEqual(pane.itemStack);
  1450. expect(newPane.itemStack[2]).toEqual(item2);
  1451. });
  1452. it('builds the itemStack if the itemStack is not serialized', () => {
  1453. const newPane = Pane.deserialize(pane.serialize(), atom);
  1454. expect(newPane.getItems()).toEqual(newPane.itemStack);
  1455. });
  1456. it('rebuilds the itemStack if items.length does not match itemStack.length', () => {
  1457. const [, item2, item3] = pane.getItems();
  1458. pane.itemStack = [item2, item3];
  1459. const newPane = Pane.deserialize(pane.serialize(), atom);
  1460. expect(newPane.getItems()).toEqual(newPane.itemStack);
  1461. });
  1462. it('does not serialize the reference to the items in the itemStack for pane items that will not be serialized', () => {
  1463. const [item1, item2, item3] = pane.getItems();
  1464. pane.itemStack = [item2, item1, item3];
  1465. const unserializable = {};
  1466. pane.activateItem(unserializable);
  1467. const newPane = Pane.deserialize(pane.serialize(), atom);
  1468. expect(newPane.itemStack).toEqual([item2, item1, item3]);
  1469. });
  1470. });
  1471. });