api-menu-spec.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939
  1. import * as cp from 'child_process';
  2. import * as path from 'path';
  3. import { expect } from 'chai';
  4. import { BrowserWindow, Menu, MenuItem } from 'electron/main';
  5. import { sortMenuItems } from '../lib/browser/api/menu-utils';
  6. import { emittedOnce } from './events-helpers';
  7. import { ifit, delay } from './spec-helpers';
  8. import { closeWindow } from './window-helpers';
  9. const fixturesPath = path.resolve(__dirname, 'fixtures');
  10. describe('Menu module', function () {
  11. describe('Menu.buildFromTemplate', () => {
  12. it('should be able to attach extra fields', () => {
  13. const menu = Menu.buildFromTemplate([
  14. {
  15. label: 'text',
  16. extra: 'field'
  17. } as MenuItem | Record<string, any>
  18. ]);
  19. expect((menu.items[0] as any).extra).to.equal('field');
  20. });
  21. it('should be able to accept only MenuItems', () => {
  22. const menu = Menu.buildFromTemplate([
  23. new MenuItem({ label: 'one' }),
  24. new MenuItem({ label: 'two' })
  25. ]);
  26. expect(menu.items[0].label).to.equal('one');
  27. expect(menu.items[1].label).to.equal('two');
  28. });
  29. it('should be able to accept only MenuItems in a submenu', () => {
  30. const menu = Menu.buildFromTemplate([
  31. {
  32. label: 'one',
  33. submenu: [
  34. new MenuItem({ label: 'two' }) as any
  35. ]
  36. }
  37. ]);
  38. expect(menu.items[0].label).to.equal('one');
  39. expect(menu.items[0].submenu!.items[0].label).to.equal('two');
  40. });
  41. it('should be able to accept MenuItems and plain objects', () => {
  42. const menu = Menu.buildFromTemplate([
  43. new MenuItem({ label: 'one' }),
  44. { label: 'two' }
  45. ]);
  46. expect(menu.items[0].label).to.equal('one');
  47. expect(menu.items[1].label).to.equal('two');
  48. });
  49. it('does not modify the specified template', () => {
  50. const template = [{ label: 'text', submenu: [{ label: 'sub' }] }];
  51. const templateCopy = JSON.parse(JSON.stringify(template));
  52. Menu.buildFromTemplate(template);
  53. expect(template).to.deep.equal(templateCopy);
  54. });
  55. it('does not throw exceptions for undefined/null values', () => {
  56. expect(() => {
  57. Menu.buildFromTemplate([
  58. {
  59. label: 'text',
  60. accelerator: undefined
  61. },
  62. {
  63. label: 'text again',
  64. accelerator: null as any
  65. }
  66. ]);
  67. }).to.not.throw();
  68. });
  69. it('does throw exceptions for empty objects and null values', () => {
  70. expect(() => {
  71. Menu.buildFromTemplate([{}, null as any]);
  72. }).to.throw(/Invalid template for MenuItem: must have at least one of label, role or type/);
  73. });
  74. it('does throw exception for object without role, label, or type attribute', () => {
  75. expect(() => {
  76. Menu.buildFromTemplate([{ visible: true }]);
  77. }).to.throw(/Invalid template for MenuItem: must have at least one of label, role or type/);
  78. });
  79. it('does throw exception for undefined', () => {
  80. expect(() => {
  81. Menu.buildFromTemplate([undefined as any]);
  82. }).to.throw(/Invalid template for MenuItem: must have at least one of label, role or type/);
  83. });
  84. it('throws when an non-array is passed as a template', () => {
  85. expect(() => {
  86. Menu.buildFromTemplate('hello' as any);
  87. }).to.throw(/Invalid template for Menu: Menu template must be an array/);
  88. });
  89. describe('Menu sorting and building', () => {
  90. describe('sorts groups', () => {
  91. it('does a simple sort', () => {
  92. const items: Electron.MenuItemConstructorOptions[] = [
  93. {
  94. label: 'two',
  95. id: '2',
  96. afterGroupContaining: ['1']
  97. },
  98. { type: 'separator' },
  99. {
  100. id: '1',
  101. label: 'one'
  102. }
  103. ];
  104. const expected = [
  105. {
  106. id: '1',
  107. label: 'one'
  108. },
  109. { type: 'separator' },
  110. {
  111. id: '2',
  112. label: 'two',
  113. afterGroupContaining: ['1']
  114. }
  115. ];
  116. expect(sortMenuItems(items)).to.deep.equal(expected);
  117. });
  118. it('does a simple sort with MenuItems', () => {
  119. const firstItem = new MenuItem({ id: '1', label: 'one' });
  120. const secondItem = new MenuItem({
  121. label: 'two',
  122. id: '2',
  123. afterGroupContaining: ['1']
  124. });
  125. const sep = new MenuItem({ type: 'separator' });
  126. const items = [secondItem, sep, firstItem];
  127. const expected = [firstItem, sep, secondItem];
  128. expect(sortMenuItems(items)).to.deep.equal(expected);
  129. });
  130. it('resolves cycles by ignoring things that conflict', () => {
  131. const items: Electron.MenuItemConstructorOptions[] = [
  132. {
  133. id: '2',
  134. label: 'two',
  135. afterGroupContaining: ['1']
  136. },
  137. { type: 'separator' },
  138. {
  139. id: '1',
  140. label: 'one',
  141. afterGroupContaining: ['2']
  142. }
  143. ];
  144. const expected = [
  145. {
  146. id: '1',
  147. label: 'one',
  148. afterGroupContaining: ['2']
  149. },
  150. { type: 'separator' },
  151. {
  152. id: '2',
  153. label: 'two',
  154. afterGroupContaining: ['1']
  155. }
  156. ];
  157. expect(sortMenuItems(items)).to.deep.equal(expected);
  158. });
  159. it('ignores references to commands that do not exist', () => {
  160. const items: Electron.MenuItemConstructorOptions[] = [
  161. {
  162. id: '1',
  163. label: 'one'
  164. },
  165. { type: 'separator' },
  166. {
  167. id: '2',
  168. label: 'two',
  169. afterGroupContaining: ['does-not-exist']
  170. }
  171. ];
  172. const expected = [
  173. {
  174. id: '1',
  175. label: 'one'
  176. },
  177. { type: 'separator' },
  178. {
  179. id: '2',
  180. label: 'two',
  181. afterGroupContaining: ['does-not-exist']
  182. }
  183. ];
  184. expect(sortMenuItems(items)).to.deep.equal(expected);
  185. });
  186. it('only respects the first matching [before|after]GroupContaining rule in a given group', () => {
  187. const items: Electron.MenuItemConstructorOptions[] = [
  188. {
  189. id: '1',
  190. label: 'one'
  191. },
  192. { type: 'separator' },
  193. {
  194. id: '3',
  195. label: 'three',
  196. beforeGroupContaining: ['1']
  197. },
  198. {
  199. id: '4',
  200. label: 'four',
  201. afterGroupContaining: ['2']
  202. },
  203. { type: 'separator' },
  204. {
  205. id: '2',
  206. label: 'two'
  207. }
  208. ];
  209. const expected = [
  210. {
  211. id: '3',
  212. label: 'three',
  213. beforeGroupContaining: ['1']
  214. },
  215. {
  216. id: '4',
  217. label: 'four',
  218. afterGroupContaining: ['2']
  219. },
  220. { type: 'separator' },
  221. {
  222. id: '1',
  223. label: 'one'
  224. },
  225. { type: 'separator' },
  226. {
  227. id: '2',
  228. label: 'two'
  229. }
  230. ];
  231. expect(sortMenuItems(items)).to.deep.equal(expected);
  232. });
  233. });
  234. describe('moves an item to a different group by merging groups', () => {
  235. it('can move a group of one item', () => {
  236. const items: Electron.MenuItemConstructorOptions[] = [
  237. {
  238. id: '1',
  239. label: 'one'
  240. },
  241. { type: 'separator' },
  242. {
  243. id: '2',
  244. label: 'two'
  245. },
  246. { type: 'separator' },
  247. {
  248. id: '3',
  249. label: 'three',
  250. after: ['1']
  251. },
  252. { type: 'separator' }
  253. ];
  254. const expected = [
  255. {
  256. id: '1',
  257. label: 'one'
  258. },
  259. {
  260. id: '3',
  261. label: 'three',
  262. after: ['1']
  263. },
  264. { type: 'separator' },
  265. {
  266. id: '2',
  267. label: 'two'
  268. }
  269. ];
  270. expect(sortMenuItems(items)).to.deep.equal(expected);
  271. });
  272. it("moves all items in the moving item's group", () => {
  273. const items: Electron.MenuItemConstructorOptions[] = [
  274. {
  275. id: '1',
  276. label: 'one'
  277. },
  278. { type: 'separator' },
  279. {
  280. id: '2',
  281. label: 'two'
  282. },
  283. { type: 'separator' },
  284. {
  285. id: '3',
  286. label: 'three',
  287. after: ['1']
  288. },
  289. {
  290. id: '4',
  291. label: 'four'
  292. },
  293. { type: 'separator' }
  294. ];
  295. const expected = [
  296. {
  297. id: '1',
  298. label: 'one'
  299. },
  300. {
  301. id: '3',
  302. label: 'three',
  303. after: ['1']
  304. },
  305. {
  306. id: '4',
  307. label: 'four'
  308. },
  309. { type: 'separator' },
  310. {
  311. id: '2',
  312. label: 'two'
  313. }
  314. ];
  315. expect(sortMenuItems(items)).to.deep.equal(expected);
  316. });
  317. it("ignores positions relative to commands that don't exist", () => {
  318. const items: Electron.MenuItemConstructorOptions[] = [
  319. {
  320. id: '1',
  321. label: 'one'
  322. },
  323. { type: 'separator' },
  324. {
  325. id: '2',
  326. label: 'two'
  327. },
  328. { type: 'separator' },
  329. {
  330. id: '3',
  331. label: 'three',
  332. after: ['does-not-exist']
  333. },
  334. {
  335. id: '4',
  336. label: 'four',
  337. after: ['1']
  338. },
  339. { type: 'separator' }
  340. ];
  341. const expected = [
  342. {
  343. id: '1',
  344. label: 'one'
  345. },
  346. {
  347. id: '3',
  348. label: 'three',
  349. after: ['does-not-exist']
  350. },
  351. {
  352. id: '4',
  353. label: 'four',
  354. after: ['1']
  355. },
  356. { type: 'separator' },
  357. {
  358. id: '2',
  359. label: 'two'
  360. }
  361. ];
  362. expect(sortMenuItems(items)).to.deep.equal(expected);
  363. });
  364. it('can handle recursive group merging', () => {
  365. const items = [
  366. {
  367. id: '1',
  368. label: 'one',
  369. after: ['3']
  370. },
  371. {
  372. id: '2',
  373. label: 'two',
  374. before: ['1']
  375. },
  376. {
  377. id: '3',
  378. label: 'three'
  379. }
  380. ];
  381. const expected = [
  382. {
  383. id: '3',
  384. label: 'three'
  385. },
  386. {
  387. id: '2',
  388. label: 'two',
  389. before: ['1']
  390. },
  391. {
  392. id: '1',
  393. label: 'one',
  394. after: ['3']
  395. }
  396. ];
  397. expect(sortMenuItems(items)).to.deep.equal(expected);
  398. });
  399. it('can merge multiple groups when given a list of before/after commands', () => {
  400. const items: Electron.MenuItemConstructorOptions[] = [
  401. {
  402. id: '1',
  403. label: 'one'
  404. },
  405. { type: 'separator' },
  406. {
  407. id: '2',
  408. label: 'two'
  409. },
  410. { type: 'separator' },
  411. {
  412. id: '3',
  413. label: 'three',
  414. after: ['1', '2']
  415. }
  416. ];
  417. const expected = [
  418. {
  419. id: '2',
  420. label: 'two'
  421. },
  422. {
  423. id: '1',
  424. label: 'one'
  425. },
  426. {
  427. id: '3',
  428. label: 'three',
  429. after: ['1', '2']
  430. }
  431. ];
  432. expect(sortMenuItems(items)).to.deep.equal(expected);
  433. });
  434. it('can merge multiple groups based on both before/after commands', () => {
  435. const items: Electron.MenuItemConstructorOptions[] = [
  436. {
  437. id: '1',
  438. label: 'one'
  439. },
  440. { type: 'separator' },
  441. {
  442. id: '2',
  443. label: 'two'
  444. },
  445. { type: 'separator' },
  446. {
  447. id: '3',
  448. label: 'three',
  449. after: ['1'],
  450. before: ['2']
  451. }
  452. ];
  453. const expected = [
  454. {
  455. id: '1',
  456. label: 'one'
  457. },
  458. {
  459. id: '3',
  460. label: 'three',
  461. after: ['1'],
  462. before: ['2']
  463. },
  464. {
  465. id: '2',
  466. label: 'two'
  467. }
  468. ];
  469. expect(sortMenuItems(items)).to.deep.equal(expected);
  470. });
  471. });
  472. it('should position before existing item', () => {
  473. const menu = Menu.buildFromTemplate([
  474. {
  475. id: '2',
  476. label: 'two'
  477. }, {
  478. id: '3',
  479. label: 'three'
  480. }, {
  481. id: '1',
  482. label: 'one',
  483. before: ['2']
  484. }
  485. ]);
  486. expect(menu.items[0].label).to.equal('one');
  487. expect(menu.items[1].label).to.equal('two');
  488. expect(menu.items[2].label).to.equal('three');
  489. });
  490. it('should position after existing item', () => {
  491. const menu = Menu.buildFromTemplate([
  492. {
  493. id: '2',
  494. label: 'two',
  495. after: ['1']
  496. },
  497. {
  498. id: '1',
  499. label: 'one'
  500. }, {
  501. id: '3',
  502. label: 'three'
  503. }
  504. ]);
  505. expect(menu.items[0].label).to.equal('one');
  506. expect(menu.items[1].label).to.equal('two');
  507. expect(menu.items[2].label).to.equal('three');
  508. });
  509. it('should filter excess menu separators', () => {
  510. const menuOne = Menu.buildFromTemplate([
  511. {
  512. type: 'separator'
  513. }, {
  514. label: 'a'
  515. }, {
  516. label: 'b'
  517. }, {
  518. label: 'c'
  519. }, {
  520. type: 'separator'
  521. }
  522. ]);
  523. expect(menuOne.items).to.have.length(3);
  524. expect(menuOne.items[0].label).to.equal('a');
  525. expect(menuOne.items[1].label).to.equal('b');
  526. expect(menuOne.items[2].label).to.equal('c');
  527. const menuTwo = Menu.buildFromTemplate([
  528. {
  529. type: 'separator'
  530. }, {
  531. type: 'separator'
  532. }, {
  533. label: 'a'
  534. }, {
  535. label: 'b'
  536. }, {
  537. label: 'c'
  538. }, {
  539. type: 'separator'
  540. }, {
  541. type: 'separator'
  542. }
  543. ]);
  544. expect(menuTwo.items).to.have.length(3);
  545. expect(menuTwo.items[0].label).to.equal('a');
  546. expect(menuTwo.items[1].label).to.equal('b');
  547. expect(menuTwo.items[2].label).to.equal('c');
  548. });
  549. it('should only filter excess menu separators AFTER the re-ordering for before/after is done', () => {
  550. const menuOne = Menu.buildFromTemplate([
  551. {
  552. type: 'separator'
  553. },
  554. {
  555. type: 'normal',
  556. label: 'Foo',
  557. id: 'foo'
  558. },
  559. {
  560. type: 'normal',
  561. label: 'Bar',
  562. id: 'bar'
  563. },
  564. {
  565. type: 'separator',
  566. before: ['bar']
  567. }]);
  568. expect(menuOne.items).to.have.length(3);
  569. expect(menuOne.items[0].label).to.equal('Foo');
  570. expect(menuOne.items[1].type).to.equal('separator');
  571. expect(menuOne.items[2].label).to.equal('Bar');
  572. });
  573. it('should continue inserting items at next index when no specifier is present', () => {
  574. const menu = Menu.buildFromTemplate([
  575. {
  576. id: '2',
  577. label: 'two'
  578. }, {
  579. id: '3',
  580. label: 'three'
  581. }, {
  582. id: '4',
  583. label: 'four'
  584. }, {
  585. id: '5',
  586. label: 'five'
  587. }, {
  588. id: '1',
  589. label: 'one',
  590. before: ['2']
  591. }
  592. ]);
  593. expect(menu.items[0].label).to.equal('one');
  594. expect(menu.items[1].label).to.equal('two');
  595. expect(menu.items[2].label).to.equal('three');
  596. expect(menu.items[3].label).to.equal('four');
  597. expect(menu.items[4].label).to.equal('five');
  598. });
  599. it('should continue inserting MenuItems at next index when no specifier is present', () => {
  600. const menu = Menu.buildFromTemplate([
  601. new MenuItem({
  602. id: '2',
  603. label: 'two'
  604. }), new MenuItem({
  605. id: '3',
  606. label: 'three'
  607. }), new MenuItem({
  608. id: '4',
  609. label: 'four'
  610. }), new MenuItem({
  611. id: '5',
  612. label: 'five'
  613. }), new MenuItem({
  614. id: '1',
  615. label: 'one',
  616. before: ['2']
  617. })
  618. ]);
  619. expect(menu.items[0].label).to.equal('one');
  620. expect(menu.items[1].label).to.equal('two');
  621. expect(menu.items[2].label).to.equal('three');
  622. expect(menu.items[3].label).to.equal('four');
  623. expect(menu.items[4].label).to.equal('five');
  624. });
  625. });
  626. });
  627. describe('Menu.getMenuItemById', () => {
  628. it('should return the item with the given id', () => {
  629. const menu = Menu.buildFromTemplate([
  630. {
  631. label: 'View',
  632. submenu: [
  633. {
  634. label: 'Enter Fullscreen',
  635. accelerator: 'ControlCommandF',
  636. id: 'fullScreen'
  637. }
  638. ]
  639. }
  640. ]);
  641. const fsc = menu.getMenuItemById('fullScreen');
  642. expect(menu.items[0].submenu!.items[0]).to.equal(fsc);
  643. });
  644. it('should return the separator with the given id', () => {
  645. const menu = Menu.buildFromTemplate([
  646. {
  647. label: 'Item 1',
  648. id: 'item_1'
  649. },
  650. {
  651. id: 'separator',
  652. type: 'separator'
  653. },
  654. {
  655. label: 'Item 2',
  656. id: 'item_2'
  657. }
  658. ]);
  659. const separator = menu.getMenuItemById('separator');
  660. expect(separator).to.be.an('object');
  661. expect(separator).to.equal(menu.items[1]);
  662. });
  663. });
  664. describe('Menu.insert', () => {
  665. it('should throw when attempting to insert at out-of-range indices', () => {
  666. const menu = Menu.buildFromTemplate([
  667. { label: '1' },
  668. { label: '2' },
  669. { label: '3' }
  670. ]);
  671. const item = new MenuItem({ label: 'badInsert' });
  672. expect(() => {
  673. menu.insert(9999, item);
  674. }).to.throw(/Position 9999 cannot be greater than the total MenuItem count/);
  675. expect(() => {
  676. menu.insert(-9999, item);
  677. }).to.throw(/Position -9999 cannot be less than 0/);
  678. });
  679. it('should store item in @items by its index', () => {
  680. const menu = Menu.buildFromTemplate([
  681. { label: '1' },
  682. { label: '2' },
  683. { label: '3' }
  684. ]);
  685. const item = new MenuItem({ label: 'inserted' });
  686. menu.insert(1, item);
  687. expect(menu.items[0].label).to.equal('1');
  688. expect(menu.items[1].label).to.equal('inserted');
  689. expect(menu.items[2].label).to.equal('2');
  690. expect(menu.items[3].label).to.equal('3');
  691. });
  692. });
  693. describe('Menu.append', () => {
  694. it('should add the item to the end of the menu', () => {
  695. const menu = Menu.buildFromTemplate([
  696. { label: '1' },
  697. { label: '2' },
  698. { label: '3' }
  699. ]);
  700. const item = new MenuItem({ label: 'inserted' });
  701. menu.append(item);
  702. expect(menu.items[0].label).to.equal('1');
  703. expect(menu.items[1].label).to.equal('2');
  704. expect(menu.items[2].label).to.equal('3');
  705. expect(menu.items[3].label).to.equal('inserted');
  706. });
  707. });
  708. describe('Menu.popup', () => {
  709. let w: BrowserWindow;
  710. let menu: Menu;
  711. beforeEach(() => {
  712. w = new BrowserWindow({ show: false, width: 200, height: 200 });
  713. menu = Menu.buildFromTemplate([
  714. { label: '1' },
  715. { label: '2' },
  716. { label: '3' }
  717. ]);
  718. });
  719. afterEach(async () => {
  720. menu.closePopup();
  721. menu.closePopup(w);
  722. await closeWindow(w);
  723. w = null as unknown as BrowserWindow;
  724. });
  725. it('throws an error if options is not an object', () => {
  726. expect(() => {
  727. menu.popup('this is a string, not an object' as any);
  728. }).to.throw(/Options must be an object/);
  729. });
  730. it('allows for options to be optional', () => {
  731. expect(() => {
  732. menu.popup({});
  733. }).to.not.throw();
  734. });
  735. it('should emit menu-will-show event', (done) => {
  736. menu.on('menu-will-show', () => { done(); });
  737. menu.popup({ window: w });
  738. });
  739. it('should emit menu-will-close event', (done) => {
  740. menu.on('menu-will-close', () => { done(); });
  741. menu.popup({ window: w });
  742. // https://github.com/electron/electron/issues/19411
  743. setTimeout(() => {
  744. menu.closePopup();
  745. });
  746. });
  747. it('returns immediately', () => {
  748. const input = { window: w, x: 100, y: 101 };
  749. const output = menu.popup(input) as unknown as {x: number, y: number, browserWindow: BrowserWindow};
  750. expect(output.x).to.equal(input.x);
  751. expect(output.y).to.equal(input.y);
  752. expect(output.browserWindow).to.equal(input.window);
  753. });
  754. it('works without a given BrowserWindow and options', () => {
  755. const { browserWindow, x, y } = menu.popup({ x: 100, y: 101 }) as unknown as {x: number, y: number, browserWindow: BrowserWindow};
  756. expect(browserWindow.constructor.name).to.equal('BrowserWindow');
  757. expect(x).to.equal(100);
  758. expect(y).to.equal(101);
  759. });
  760. it('works with a given BrowserWindow, options and callback', (done) => {
  761. const { x, y } = menu.popup({
  762. window: w,
  763. x: 100,
  764. y: 101,
  765. callback: () => done()
  766. }) as unknown as {x: number, y: number};
  767. expect(x).to.equal(100);
  768. expect(y).to.equal(101);
  769. // https://github.com/electron/electron/issues/19411
  770. setTimeout(() => {
  771. menu.closePopup();
  772. });
  773. });
  774. it('works with a given BrowserWindow, no options, and a callback', (done) => {
  775. menu.popup({ window: w, callback: () => done() });
  776. // https://github.com/electron/electron/issues/19411
  777. setTimeout(() => {
  778. menu.closePopup();
  779. });
  780. });
  781. it('prevents menu from getting garbage-collected when popuping', async () => {
  782. const menu = Menu.buildFromTemplate([{ role: 'paste' }]);
  783. menu.popup({ window: w });
  784. // Keep a weak reference to the menu.
  785. // eslint-disable-next-line no-undef
  786. const wr = new (globalThis as any).WeakRef(menu);
  787. await delay();
  788. // Do garbage collection, since |menu| is not referenced in this closure
  789. // it would be gone after next call.
  790. const v8Util = process._linkedBinding('electron_common_v8_util');
  791. v8Util.requestGarbageCollectionForTesting();
  792. await delay();
  793. // Try to receive menu from weak reference.
  794. if (wr.deref()) {
  795. wr.deref().closePopup();
  796. } else {
  797. throw new Error('Menu is garbage-collected while popuping');
  798. }
  799. });
  800. });
  801. describe('Menu.setApplicationMenu', () => {
  802. it('sets a menu', () => {
  803. const menu = Menu.buildFromTemplate([
  804. { label: '1' },
  805. { label: '2' }
  806. ]);
  807. Menu.setApplicationMenu(menu);
  808. expect(Menu.getApplicationMenu()).to.not.be.null('application menu');
  809. });
  810. // TODO(nornagon): this causes the focus handling tests to fail
  811. it.skip('unsets a menu with null', () => {
  812. Menu.setApplicationMenu(null);
  813. expect(Menu.getApplicationMenu()).to.be.null('application menu');
  814. });
  815. ifit(process.platform !== 'darwin')('does not override menu visibility on startup', async () => {
  816. const appPath = path.join(fixturesPath, 'api', 'test-menu-visibility');
  817. const appProcess = cp.spawn(process.execPath, [appPath]);
  818. let output = '';
  819. await new Promise((resolve) => {
  820. appProcess.stdout.on('data', data => {
  821. output += data;
  822. if (data.indexOf('Window has') > -1) {
  823. resolve();
  824. }
  825. });
  826. });
  827. expect(output).to.include('Window has no menu');
  828. });
  829. ifit(process.platform !== 'darwin')('does not override null menu on startup', async () => {
  830. const appPath = path.join(fixturesPath, 'api', 'test-menu-null');
  831. const appProcess = cp.spawn(process.execPath, [appPath]);
  832. let output = '';
  833. appProcess.stdout.on('data', data => { output += data; });
  834. appProcess.stderr.on('data', data => { output += data; });
  835. const [code] = await emittedOnce(appProcess, 'exit');
  836. if (!output.includes('Window has no menu')) {
  837. console.log(code, output);
  838. }
  839. expect(output).to.include('Window has no menu');
  840. });
  841. });
  842. });