api-menu-spec.js 22 KB

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