dialog.spec.tsx 18 KB

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