dialog.spec.ts 16 KB

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