api-menu-spec.ts 27 KB

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