dialog.spec.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { Dialog, showDialog } from '@jupyterlab/apputils';
  4. import {
  5. acceptDialog,
  6. dismissDialog,
  7. waitForDialog
  8. } from '@jupyterlab/testutils';
  9. import { each } from '@lumino/algorithm';
  10. import { Message } from '@lumino/messaging';
  11. import { Widget } from '@lumino/widgets';
  12. import * as React from 'react';
  13. import { generate, simulate } from 'simulate-event';
  14. class TestDialog extends Dialog<any> {
  15. methods: string[] = [];
  16. events: string[] = [];
  17. handleEvent(event: Event): void {
  18. super.handleEvent(event);
  19. this.events.push(event.type);
  20. }
  21. protected onAfterAttach(msg: Message): void {
  22. super.onAfterAttach(msg);
  23. this.methods.push('onAfterAttach');
  24. }
  25. protected onAfterDetach(msg: Message): void {
  26. super.onAfterDetach(msg);
  27. this.methods.push('onAfterDetach');
  28. }
  29. protected onCloseRequest(msg: Message): void {
  30. super.onCloseRequest(msg);
  31. this.methods.push('onCloseRequest');
  32. }
  33. }
  34. class ValueWidget extends Widget {
  35. getValue(): string {
  36. return 'foo';
  37. }
  38. }
  39. describe('@jupyterlab/apputils', () => {
  40. describe('Dialog', () => {
  41. let dialog: TestDialog;
  42. beforeEach(() => {
  43. dialog = new TestDialog();
  44. });
  45. afterEach(() => {
  46. dialog.dispose();
  47. });
  48. describe('#constructor()', () => {
  49. it('should create a new dialog', () => {
  50. expect(dialog).toBeInstanceOf(Dialog);
  51. });
  52. it('should accept options', () => {
  53. const dialog = new TestDialog({
  54. title: 'foo',
  55. body: 'Hello',
  56. buttons: [Dialog.okButton()],
  57. hasClose: false
  58. });
  59. expect(dialog).toBeInstanceOf(Dialog);
  60. dialog.dispose();
  61. });
  62. });
  63. describe('#launch()', () => {
  64. it.skip('should attach the dialog to the host', async () => {
  65. const host = document.createElement('div');
  66. const dialog = new TestDialog({ host });
  67. document.body.appendChild(host);
  68. void dialog.launch();
  69. await waitForDialog(host);
  70. expect(host.contains(dialog.node)).toBe(true);
  71. dialog.dispose();
  72. document.body.removeChild(host);
  73. });
  74. it('should resolve with `true` when accepted', async () => {
  75. const prompt = dialog.launch();
  76. await waitForDialog();
  77. dialog.resolve();
  78. expect((await prompt).button.accept).toBe(true);
  79. });
  80. it('should resolve with `false` when rejected', async () => {
  81. const prompt = dialog.launch();
  82. await waitForDialog();
  83. dialog.reject();
  84. expect((await prompt).button.accept).toBe(false);
  85. });
  86. it('should resolve with `false` when closed', async () => {
  87. const prompt = dialog.launch();
  88. await waitForDialog();
  89. dialog.close();
  90. expect((await prompt).button.accept).toBe(false);
  91. });
  92. it('should return focus to the original focused element', async () => {
  93. const input = document.createElement('input');
  94. document.body.appendChild(input);
  95. input.focus();
  96. expect(document.activeElement).toBe(input);
  97. const prompt = dialog.launch();
  98. await waitForDialog();
  99. expect(document.activeElement).not.toBe(input);
  100. dialog.resolve();
  101. await prompt;
  102. expect(document.activeElement).toBe(input);
  103. document.body.removeChild(input);
  104. });
  105. });
  106. describe('#resolve()', () => {
  107. it('should resolve with the default item', async () => {
  108. const prompt = dialog.launch();
  109. await waitForDialog();
  110. dialog.resolve();
  111. expect((await prompt).button.accept).toBe(true);
  112. });
  113. it('should resolve with the item at the given index', async () => {
  114. const prompt = dialog.launch();
  115. await waitForDialog();
  116. dialog.resolve(0);
  117. expect((await prompt).button.accept).toBe(false);
  118. });
  119. });
  120. describe('#reject()', () => {
  121. it('should reject with the default reject item', async () => {
  122. const prompt = dialog.launch();
  123. await waitForDialog();
  124. dialog.reject();
  125. const result = await prompt;
  126. expect(result.button.label).toBe('Cancel');
  127. expect(result.button.accept).toBe(false);
  128. });
  129. });
  130. describe('#handleEvent()', () => {
  131. describe('keydown', () => {
  132. it('should reject on escape key', async () => {
  133. const prompt = dialog.launch();
  134. await waitForDialog();
  135. simulate(dialog.node, 'keydown', { keyCode: 27 });
  136. expect((await prompt).button.accept).toBe(false);
  137. });
  138. it('should accept on enter key', async () => {
  139. const prompt = dialog.launch();
  140. await waitForDialog();
  141. simulate(dialog.node, 'keydown', { keyCode: 13 });
  142. expect((await prompt).button.accept).toBe(true);
  143. });
  144. it('should resolve with currently focused button', async () => {
  145. const dialog = new TestDialog({
  146. buttons: [
  147. Dialog.createButton({ label: 'first' }),
  148. Dialog.createButton({ label: 'second' }),
  149. Dialog.createButton({ label: 'third' }),
  150. Dialog.createButton({ label: 'fourth' })
  151. ],
  152. // focus on "first"
  153. defaultButton: 0
  154. });
  155. const prompt = dialog.launch();
  156. await waitForDialog();
  157. // press right arrow twice (focusing on "third")
  158. simulate(dialog.node, 'keydown', { keyCode: 39 });
  159. simulate(dialog.node, 'keydown', { keyCode: 39 });
  160. // press enter
  161. simulate(dialog.node, 'keydown', { keyCode: 13 });
  162. expect((await prompt).button.label).toBe('third');
  163. dialog.dispose();
  164. });
  165. it('should cycle to the first button on a tab key', async () => {
  166. const prompt = dialog.launch();
  167. await waitForDialog();
  168. expect(document.activeElement!.className).toContain('jp-mod-accept');
  169. simulate(dialog.node, 'keydown', { keyCode: 9 });
  170. expect(document.activeElement!.className).toContain('jp-mod-reject');
  171. simulate(document.activeElement!, 'click');
  172. expect((await prompt).button.accept).toBe(false);
  173. });
  174. });
  175. describe('contextmenu', () => {
  176. it('should cancel context menu events', async () => {
  177. const prompt = dialog.launch();
  178. await waitForDialog();
  179. const canceled = !dialog.node.dispatchEvent(generate('contextmenu'));
  180. expect(canceled).toBe(true);
  181. simulate(dialog.node, 'keydown', { keyCode: 27 });
  182. expect((await prompt).button.accept).toBe(false);
  183. });
  184. });
  185. describe('click', () => {
  186. it('should prevent clicking outside of the content area', async () => {
  187. const prompt = dialog.launch();
  188. await waitForDialog();
  189. const canceled = !dialog.node.dispatchEvent(generate('click'));
  190. expect(canceled).toBe(true);
  191. dialog.resolve();
  192. await prompt;
  193. });
  194. it('should resolve a clicked button', async () => {
  195. const prompt = dialog.launch();
  196. await waitForDialog();
  197. simulate(dialog.node.querySelector('.jp-mod-reject')!, 'click');
  198. expect((await prompt).button.accept).toBe(false);
  199. });
  200. });
  201. describe('focus', () => {
  202. it('should focus the default button when focus leaves the dialog', async () => {
  203. const host = document.createElement('div');
  204. const target = document.createElement('div');
  205. target.tabIndex = 0; // Make the div element focusable
  206. const dialog = new TestDialog({ host });
  207. document.body.appendChild(target);
  208. document.body.appendChild(host);
  209. target.focus();
  210. expect(document.activeElement).toBe(target);
  211. const prompt = dialog.launch();
  212. await waitForDialog();
  213. simulate(target, 'focus');
  214. expect(document.activeElement).not.toBe(target);
  215. expect(document.activeElement!.className).toContain('jp-mod-accept');
  216. dialog.resolve();
  217. await prompt;
  218. dialog.dispose();
  219. });
  220. });
  221. });
  222. describe('#onAfterAttach()', () => {
  223. it('should attach event listeners', () => {
  224. Widget.attach(dialog, document.body);
  225. expect(dialog.methods).toContain('onAfterAttach');
  226. ['keydown', 'contextmenu', 'click', 'focus'].forEach(event => {
  227. simulate(dialog.node, event);
  228. expect(dialog.events).toContain(event);
  229. });
  230. });
  231. it('should focus the default button', () => {
  232. Widget.attach(dialog, document.body);
  233. expect(document.activeElement!.className).toContain('jp-mod-accept');
  234. });
  235. it('should focus the primary element', () => {
  236. const body = (
  237. <div>
  238. <input type={'text'} />
  239. </div>
  240. );
  241. const dialog = new TestDialog({ body, focusNodeSelector: 'input' });
  242. Widget.attach(dialog, document.body);
  243. expect(document.activeElement!.localName).toBe('input');
  244. dialog.dispose();
  245. });
  246. });
  247. describe('#onAfterDetach()', () => {
  248. it('should remove event listeners', () => {
  249. Widget.attach(dialog, document.body);
  250. Widget.detach(dialog);
  251. expect(dialog.methods).toContain('onAfterDetach');
  252. dialog.events = [];
  253. ['keydown', 'contextmenu', 'click', 'focus'].forEach(event => {
  254. simulate(dialog.node, event);
  255. expect(dialog.events).not.toContain(event);
  256. });
  257. });
  258. it('should return focus to the original focused element', () => {
  259. const input = document.createElement('input');
  260. document.body.appendChild(input);
  261. input.focus();
  262. Widget.attach(dialog, document.body);
  263. Widget.detach(dialog);
  264. expect(document.activeElement).toBe(input);
  265. document.body.removeChild(input);
  266. });
  267. });
  268. describe('#onCloseRequest()', () => {
  269. it('should reject an existing promise', async () => {
  270. const prompt = dialog.launch();
  271. await waitForDialog();
  272. dialog.close();
  273. expect((await prompt).button.accept).toBe(false);
  274. });
  275. });
  276. describe('.defaultRenderer', () => {
  277. it('should be an instance of a Renderer', () => {
  278. expect(Dialog.defaultRenderer).toBeInstanceOf(Dialog.Renderer);
  279. });
  280. });
  281. describe('.Renderer', () => {
  282. const renderer = Dialog.defaultRenderer;
  283. const data: Dialog.IButton = {
  284. label: 'foo',
  285. iconClass: 'bar',
  286. iconLabel: 'foo',
  287. caption: 'hello',
  288. className: 'baz',
  289. accept: false,
  290. displayType: 'warn',
  291. actions: []
  292. };
  293. describe('#createHeader()', () => {
  294. it('should create the header of the dialog', () => {
  295. const widget = renderer.createHeader('foo');
  296. expect(widget.hasClass('jp-Dialog-header')).toBe(true);
  297. });
  298. });
  299. describe('#createBody()', () => {
  300. it('should create the body from a string', () => {
  301. const widget = renderer.createBody('foo');
  302. expect(widget.hasClass('jp-Dialog-body')).toBe(true);
  303. expect(widget.node.firstChild!.textContent).toBe('foo');
  304. });
  305. it('should create the body from a virtual node', () => {
  306. const vnode = (
  307. <div>
  308. <input type={'text'} />
  309. <select>
  310. <option value={'foo'}>foo</option>
  311. </select>
  312. <button />
  313. </div>
  314. );
  315. const widget = renderer.createBody(vnode);
  316. const button = widget.node.querySelector('button')!;
  317. const input = widget.node.querySelector('input')!;
  318. const select = widget.node.querySelector('select')!;
  319. Widget.attach(widget, document.body);
  320. expect(button.className).toContain('jp-mod-styled');
  321. expect(input.className).toContain('jp-mod-styled');
  322. expect(select.className).toContain('jp-mod-styled');
  323. widget.dispose();
  324. });
  325. it('should create the body from a widget', () => {
  326. const body = new Widget();
  327. renderer.createBody(body);
  328. expect(body.hasClass('jp-Dialog-body')).toBe(true);
  329. });
  330. });
  331. describe('#createFooter()', () => {
  332. it('should create the footer of the dialog', () => {
  333. const buttons = [Dialog.okButton, { label: 'foo' }];
  334. const nodes = buttons.map((button: Dialog.IButton) => {
  335. return renderer.createButtonNode(button);
  336. });
  337. const footer = renderer.createFooter(nodes);
  338. const buttonNodes = footer.node.querySelectorAll('button');
  339. expect(footer.hasClass('jp-Dialog-footer')).toBe(true);
  340. expect(footer.node.contains(nodes[0])).toBe(true);
  341. expect(footer.node.contains(nodes[1])).toBe(true);
  342. expect(buttonNodes.length).toBeGreaterThan(0);
  343. each(buttonNodes, (node: Element) => {
  344. expect(node.className).toContain('jp-mod-styled');
  345. });
  346. });
  347. });
  348. describe('#createButtonNode()', () => {
  349. it('should create a button node for the dialog', () => {
  350. const node = renderer.createButtonNode(data);
  351. expect(node.className).toContain('jp-Dialog-button');
  352. expect(node.querySelector('.jp-Dialog-buttonIcon')).toBeTruthy();
  353. expect(node.querySelector('.jp-Dialog-buttonLabel')).toBeTruthy();
  354. });
  355. });
  356. describe('#renderIcon()', () => {
  357. it('should render an icon element for a dialog item', () => {
  358. const node = renderer.renderIcon(data);
  359. expect(node.className).toContain('jp-Dialog-buttonIcon');
  360. expect(node.textContent).toBe('foo');
  361. });
  362. });
  363. describe('#createItemClass()', () => {
  364. it('should create the class name for the button', () => {
  365. const value = renderer.createItemClass(data);
  366. expect(value).toContain('jp-Dialog-button');
  367. expect(value).toContain('jp-mod-reject');
  368. expect(value).toContain(data.className);
  369. });
  370. });
  371. describe('#createIconClass()', () => {
  372. it('should create the class name for the button icon', () => {
  373. const value = renderer.createIconClass(data);
  374. expect(value).toContain('jp-Dialog-buttonIcon');
  375. expect(value).toContain(data.iconClass);
  376. });
  377. });
  378. describe('#renderLabel()', () => {
  379. it('should render a label element for a button', () => {
  380. const node = renderer.renderLabel(data);
  381. expect(node.className).toBe('jp-Dialog-buttonLabel');
  382. expect(node.title).toBe(data.caption);
  383. expect(node.textContent).toBe(data.label);
  384. });
  385. });
  386. });
  387. });
  388. describe('showDialog()', () => {
  389. it('should accept zero arguments', async () => {
  390. const dialog = showDialog();
  391. await dismissDialog();
  392. expect((await dialog).button.accept).toBe(false);
  393. });
  394. it('should accept dialog options', async () => {
  395. const node = document.createElement('div');
  396. document.body.appendChild(node);
  397. const prompt = showDialog({
  398. title: 'foo',
  399. body: 'Hello',
  400. host: node,
  401. defaultButton: 0,
  402. buttons: [Dialog.cancelButton(), Dialog.okButton()]
  403. });
  404. await acceptDialog();
  405. const result = await prompt;
  406. expect(result.button.accept).toBe(false);
  407. expect(result.value).toBe(null);
  408. document.body.removeChild(node);
  409. });
  410. it('should accept a virtualdom body', async () => {
  411. const body = (
  412. <div>
  413. <input />
  414. <select />
  415. </div>
  416. );
  417. const prompt = showDialog({ body });
  418. await acceptDialog();
  419. const result = await prompt;
  420. expect(result.button.accept).toBe(true);
  421. expect(result.value).toBe(null);
  422. });
  423. it('should accept a widget body', async () => {
  424. const prompt = showDialog({ body: new Widget() });
  425. await acceptDialog();
  426. const result = await prompt;
  427. expect(result.button.accept).toBe(true);
  428. expect(result.value).toBe(null);
  429. });
  430. it('should give the value from the widget', async () => {
  431. const prompt = showDialog({ body: new ValueWidget() });
  432. await acceptDialog();
  433. const result = await prompt;
  434. expect(result.button.accept).toBe(true);
  435. expect(result.value).toBe('foo');
  436. });
  437. it('should not create a close button by default', async () => {
  438. const node = document.createElement('div');
  439. document.body.appendChild(node);
  440. const prompt = showDialog({
  441. title: 'foo',
  442. body: 'Hello',
  443. host: node,
  444. defaultButton: 0,
  445. buttons: [Dialog.cancelButton(), Dialog.okButton()]
  446. });
  447. await waitForDialog();
  448. expect(node.querySelector('.jp-Dialog-close-button')).toBeFalsy();
  449. await acceptDialog();
  450. const result = await prompt;
  451. expect(result.button.accept).toBe(false);
  452. expect(result.button.actions).toEqual([]);
  453. expect(result.value).toBe(null);
  454. document.body.removeChild(node);
  455. });
  456. it('should create a close button', async () => {
  457. const node = document.createElement('div');
  458. document.body.appendChild(node);
  459. const prompt = showDialog({
  460. title: 'foo',
  461. body: 'Hello',
  462. host: node,
  463. defaultButton: 0,
  464. buttons: [Dialog.cancelButton(), Dialog.okButton()],
  465. hasClose: true
  466. });
  467. await waitForDialog();
  468. expect(node.querySelector('.jp-Dialog-close-button')).toBeTruthy();
  469. await acceptDialog();
  470. const result = await prompt;
  471. expect(result.button.accept).toBe(false);
  472. expect(result.button.actions).toEqual([]);
  473. expect(result.value).toBe(null);
  474. document.body.removeChild(node);
  475. });
  476. it('should accept the dialog reload options', async () => {
  477. const node = document.createElement('div');
  478. document.body.appendChild(node);
  479. const prompt = showDialog({
  480. title: 'foo',
  481. body: 'Hello',
  482. host: node,
  483. defaultButton: 0,
  484. buttons: [
  485. Dialog.cancelButton({ actions: ['reload'] }),
  486. Dialog.okButton()
  487. ],
  488. hasClose: true
  489. });
  490. await acceptDialog();
  491. const result = await prompt;
  492. expect(result.button.accept).toBe(false);
  493. expect(result.button.actions).toEqual(['reload']);
  494. expect(result.value).toBe(null);
  495. document.body.removeChild(node);
  496. });
  497. });
  498. });