toolbar.spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { expect } from 'chai';
  4. import {
  5. ClientSession,
  6. Toolbar,
  7. ToolbarButton,
  8. CommandToolbarButton
  9. } from '@jupyterlab/apputils';
  10. import { toArray } from '@lumino/algorithm';
  11. import { CommandRegistry } from '@lumino/commands';
  12. import { ReadonlyJSONObject } from '@lumino/coreutils';
  13. import { Widget } from '@lumino/widgets';
  14. import { simulate } from 'simulate-event';
  15. import { createClientSession, framePromise } from '@jupyterlab/testutils';
  16. describe('@jupyterlab/apputils', () => {
  17. let widget: Toolbar<Widget>;
  18. beforeEach(async () => {
  19. widget = new Toolbar();
  20. });
  21. afterEach(async () => {
  22. widget.dispose();
  23. });
  24. describe('Toolbar', () => {
  25. describe('#constructor()', () => {
  26. it('should construct a new toolbar widget', () => {
  27. const widget = new Toolbar();
  28. expect(widget).to.be.an.instanceof(Toolbar);
  29. });
  30. it('should add the `jp-Toolbar` class', () => {
  31. const widget = new Toolbar();
  32. expect(widget.hasClass('jp-Toolbar')).to.equal(true);
  33. });
  34. });
  35. describe('#names()', () => {
  36. it('should get an ordered list the toolbar item names', () => {
  37. widget.addItem('foo', new Widget());
  38. widget.addItem('bar', new Widget());
  39. widget.addItem('baz', new Widget());
  40. expect(toArray(widget.names())).to.deep.equal(['foo', 'bar', 'baz']);
  41. });
  42. });
  43. describe('#addItem()', () => {
  44. it('should add an item to the toolbar', () => {
  45. const item = new Widget();
  46. expect(widget.addItem('test', item)).to.equal(true);
  47. expect(toArray(widget.names())).to.contain('test');
  48. });
  49. it('should add the `jp-Toolbar-item` class to the widget', () => {
  50. const item = new Widget();
  51. widget.addItem('test', item);
  52. expect(item.hasClass('jp-Toolbar-item')).to.equal(true);
  53. });
  54. it('should return false if the name is already used', () => {
  55. widget.addItem('test', new Widget());
  56. expect(widget.addItem('test', new Widget())).to.equal(false);
  57. });
  58. });
  59. describe('#insertItem()', () => {
  60. it('should insert the item into the toolbar', () => {
  61. widget.addItem('a', new Widget());
  62. widget.addItem('b', new Widget());
  63. widget.insertItem(1, 'c', new Widget());
  64. expect(toArray(widget.names())).to.deep.equal(['a', 'c', 'b']);
  65. });
  66. it('should clamp the bounds', () => {
  67. widget.addItem('a', new Widget());
  68. widget.addItem('b', new Widget());
  69. widget.insertItem(10, 'c', new Widget());
  70. expect(toArray(widget.names())).to.deep.equal(['a', 'b', 'c']);
  71. });
  72. });
  73. describe('#insertAfter()', () => {
  74. it('should insert an item into the toolbar after `c`', () => {
  75. widget.addItem('a', new Widget());
  76. widget.addItem('b', new Widget());
  77. widget.insertItem(1, 'c', new Widget());
  78. widget.insertAfter('c', 'd', new Widget());
  79. expect(toArray(widget.names())).to.deep.equal(['a', 'c', 'd', 'b']);
  80. });
  81. it('should return false if the target item does not exist', () => {
  82. widget.addItem('a', new Widget());
  83. widget.addItem('b', new Widget());
  84. let value = widget.insertAfter('c', 'd', new Widget());
  85. expect(value).to.be.false;
  86. });
  87. });
  88. describe('#insertBefore()', () => {
  89. it('should insert an item into the toolbar before `c`', () => {
  90. widget.addItem('a', new Widget());
  91. widget.addItem('b', new Widget());
  92. widget.insertItem(1, 'c', new Widget());
  93. widget.insertBefore('c', 'd', new Widget());
  94. expect(toArray(widget.names())).to.deep.equal(['a', 'd', 'c', 'b']);
  95. });
  96. it('should return false if the target item does not exist', () => {
  97. widget.addItem('a', new Widget());
  98. widget.addItem('b', new Widget());
  99. let value = widget.insertBefore('c', 'd', new Widget());
  100. expect(value).to.be.false;
  101. });
  102. });
  103. describe('.createFromCommand', () => {
  104. const commands = new CommandRegistry();
  105. const testLogCommandId = 'test:toolbar-log';
  106. const logArgs: ReadonlyJSONObject[] = [];
  107. let enabled = false;
  108. let toggled = true;
  109. let visible = false;
  110. commands.addCommand(testLogCommandId, {
  111. execute: args => {
  112. logArgs.push(args);
  113. },
  114. label: 'Test log command label',
  115. caption: 'Test log command caption',
  116. usage: 'Test log command usage',
  117. iconClass: 'test-icon-class',
  118. className: 'test-log-class',
  119. isEnabled: () => enabled,
  120. isToggled: () => toggled,
  121. isVisible: () => visible
  122. });
  123. async function render(button: CommandToolbarButton) {
  124. button.update();
  125. await framePromise();
  126. expect(button.renderPromise).to.exist;
  127. await button.renderPromise;
  128. }
  129. it('should create a button', () => {
  130. const button = new CommandToolbarButton({
  131. commands,
  132. id: testLogCommandId
  133. });
  134. expect(button).to.be.an.instanceof(CommandToolbarButton);
  135. button.dispose();
  136. });
  137. it('should add main class', async () => {
  138. const button = new CommandToolbarButton({
  139. commands,
  140. id: testLogCommandId
  141. });
  142. await render(button);
  143. const buttonNode = button.node.firstChild as HTMLButtonElement;
  144. expect(buttonNode.classList.contains('test-log-class')).to.equal(true);
  145. button.dispose();
  146. });
  147. it('should add an icon with icon class and label', async () => {
  148. const button = new CommandToolbarButton({
  149. commands,
  150. id: testLogCommandId
  151. });
  152. await render(button);
  153. const buttonNode = button.node.firstChild as HTMLButtonElement;
  154. expect(buttonNode.title).to.equal('Test log command caption');
  155. const wrapperNode = buttonNode.firstChild as HTMLElement;
  156. const iconNode = wrapperNode.firstChild as HTMLElement;
  157. expect(iconNode.classList.contains('test-icon-class')).to.equal(true);
  158. button.dispose();
  159. });
  160. it('should apply state classes', async () => {
  161. enabled = false;
  162. toggled = true;
  163. visible = false;
  164. const button = new CommandToolbarButton({
  165. commands,
  166. id: testLogCommandId
  167. });
  168. await render(button);
  169. const buttonNode = button.node.firstChild as HTMLButtonElement;
  170. expect(buttonNode.disabled).to.equal(true);
  171. expect(buttonNode.classList.contains('p-mod-toggled')).to.equal(true);
  172. expect(buttonNode.classList.contains('p-mod-hidden')).to.equal(true);
  173. button.dispose();
  174. });
  175. it('should update state classes', async () => {
  176. enabled = false;
  177. toggled = true;
  178. visible = false;
  179. const button = new CommandToolbarButton({
  180. commands,
  181. id: testLogCommandId
  182. });
  183. await render(button);
  184. const buttonNode = button.node.firstChild as HTMLButtonElement;
  185. expect(buttonNode.disabled).to.equal(true);
  186. expect(buttonNode.classList.contains('p-mod-toggled')).to.equal(true);
  187. expect(buttonNode.classList.contains('p-mod-hidden')).to.equal(true);
  188. enabled = true;
  189. visible = true;
  190. commands.notifyCommandChanged(testLogCommandId);
  191. expect(buttonNode.disabled).to.equal(false);
  192. expect(buttonNode.classList.contains('p-mod-toggled')).to.equal(true);
  193. expect(buttonNode.classList.contains('p-mod-hidden')).to.equal(false);
  194. enabled = false;
  195. visible = false;
  196. button.dispose();
  197. });
  198. it('should use the command label if no icon class/label', async () => {
  199. const id = 'to-be-removed';
  200. const cmd = commands.addCommand(id, {
  201. execute: () => {
  202. return;
  203. },
  204. label: 'Label-only button'
  205. });
  206. const button = new CommandToolbarButton({
  207. commands,
  208. id
  209. });
  210. await render(button);
  211. const buttonNode = button.node.firstChild as HTMLButtonElement;
  212. expect(buttonNode.textContent).to.equal('Label-only button');
  213. cmd.dispose();
  214. });
  215. it('should update the node content on command change event', async () => {
  216. const id = 'to-be-removed';
  217. let iconClassValue: string | null = null;
  218. const cmd = commands.addCommand(id, {
  219. execute: () => {
  220. /* no op */
  221. },
  222. label: 'Label-only button',
  223. iconClass: () => iconClassValue
  224. });
  225. const button = new CommandToolbarButton({
  226. commands,
  227. id
  228. });
  229. await render(button);
  230. const buttonNode = button.node.firstChild as HTMLButtonElement;
  231. expect(buttonNode.textContent).to.equal('Label-only button');
  232. expect(buttonNode.classList.contains(iconClassValue)).to.equal(false);
  233. iconClassValue = 'updated-icon-class';
  234. commands.notifyCommandChanged(id);
  235. await render(button);
  236. const wrapperNode = buttonNode.firstChild as HTMLElement;
  237. const iconNode = wrapperNode.firstChild as HTMLElement;
  238. expect(iconNode.classList.contains(iconClassValue)).to.equal(true);
  239. cmd.dispose();
  240. });
  241. });
  242. describe('Kernel buttons', () => {
  243. let session: ClientSession;
  244. beforeEach(async () => {
  245. session = await createClientSession();
  246. });
  247. afterEach(async () => {
  248. await session.shutdown();
  249. session.dispose();
  250. });
  251. describe('.createInterruptButton()', () => {
  252. it("should add an inline svg node with the 'stop' icon", async () => {
  253. const button = Toolbar.createInterruptButton(session);
  254. Widget.attach(button, document.body);
  255. await framePromise();
  256. expect(button.node.querySelector("[data-icon='stop']")).to.exist;
  257. });
  258. });
  259. describe('.createRestartButton()', () => {
  260. it("should add an inline svg node with the 'refresh' icon", async () => {
  261. const button = Toolbar.createRestartButton(session);
  262. Widget.attach(button, document.body);
  263. await framePromise();
  264. expect(button.node.querySelector("[data-icon='refresh']")).to.exist;
  265. });
  266. });
  267. describe('.createKernelNameItem()', () => {
  268. it("should display the `'display_name'` of the kernel", async () => {
  269. const item = Toolbar.createKernelNameItem(session);
  270. await session.initialize();
  271. Widget.attach(item, document.body);
  272. await framePromise();
  273. expect(
  274. (item.node.firstChild.lastChild as HTMLElement).textContent
  275. ).to.equal(session.kernelDisplayName);
  276. });
  277. it("should display `'No Kernel!'` if there is no kernel", async () => {
  278. const item = Toolbar.createKernelNameItem(session);
  279. Widget.attach(item, document.body);
  280. await framePromise();
  281. expect(
  282. (item.node.firstChild.lastChild as HTMLElement).textContent
  283. ).to.equal('No Kernel!');
  284. });
  285. });
  286. describe('.createKernelStatusItem()', () => {
  287. beforeEach(async () => {
  288. await session.initialize();
  289. await session.kernel.ready;
  290. });
  291. it('should display a busy status if the kernel status is busy', async () => {
  292. const item = Toolbar.createKernelStatusItem(session);
  293. let called = false;
  294. session.statusChanged.connect((_, status) => {
  295. if (status === 'busy') {
  296. expect(item.hasClass('jp-FilledCircleIcon')).to.equal(true);
  297. called = true;
  298. }
  299. });
  300. const future = session.kernel.requestExecute({ code: 'a = 109\na' });
  301. await future.done;
  302. expect(called).to.equal(true);
  303. });
  304. it('should show the current status in the node title', async () => {
  305. const item = Toolbar.createKernelStatusItem(session);
  306. const status = session.status;
  307. expect(item.node.title.toLowerCase()).to.contain(status);
  308. let called = false;
  309. const future = session.kernel.requestExecute({ code: 'a = 1' });
  310. future.onIOPub = msg => {
  311. if (session.status === 'busy') {
  312. expect(item.node.title.toLowerCase()).to.contain('busy');
  313. called = true;
  314. }
  315. };
  316. await future.done;
  317. expect(called).to.equal(true);
  318. });
  319. it('should handle a starting session', async () => {
  320. await session.kernel.ready;
  321. await session.shutdown();
  322. session = await createClientSession();
  323. const item = Toolbar.createKernelStatusItem(session);
  324. expect(item.node.title).to.equal('Kernel Starting');
  325. expect(item.hasClass('jp-FilledCircleIcon')).to.equal(true);
  326. await session.initialize();
  327. await session.kernel.ready;
  328. });
  329. });
  330. });
  331. });
  332. describe('ToolbarButton', () => {
  333. describe('#constructor()', () => {
  334. it('should accept no arguments', () => {
  335. const widget = new ToolbarButton();
  336. expect(widget).to.be.an.instanceof(ToolbarButton);
  337. });
  338. it('should accept options', async () => {
  339. const widget = new ToolbarButton({
  340. className: 'foo',
  341. iconClassName: 'iconFoo',
  342. onClick: () => {
  343. return void 0;
  344. },
  345. tooltip: 'bar'
  346. });
  347. Widget.attach(widget, document.body);
  348. await framePromise();
  349. const button = widget.node.firstChild as HTMLElement;
  350. expect(button.classList.contains('foo')).to.equal(true);
  351. expect(button.querySelector('.iconFoo')).to.exist;
  352. expect(button.title).to.equal('bar');
  353. });
  354. });
  355. describe('#dispose()', () => {
  356. it('should dispose of the resources used by the widget', () => {
  357. const button = new ToolbarButton();
  358. button.dispose();
  359. expect(button.isDisposed).to.equal(true);
  360. });
  361. it('should be safe to call more than once', () => {
  362. const button = new ToolbarButton();
  363. button.dispose();
  364. button.dispose();
  365. expect(button.isDisposed).to.equal(true);
  366. });
  367. });
  368. describe('#handleEvent()', () => {
  369. describe('click', () => {
  370. it('should activate the callback', async () => {
  371. let called = false;
  372. const button = new ToolbarButton({
  373. onClick: () => {
  374. called = true;
  375. }
  376. });
  377. Widget.attach(button, document.body);
  378. await framePromise();
  379. simulate(button.node.firstChild as HTMLElement, 'mousedown');
  380. expect(called).to.equal(true);
  381. button.dispose();
  382. });
  383. });
  384. describe('keydown', () => {
  385. it('Enter should activate the callback', async () => {
  386. let called = false;
  387. const button = new ToolbarButton({
  388. onClick: () => {
  389. called = true;
  390. }
  391. });
  392. Widget.attach(button, document.body);
  393. await framePromise();
  394. simulate(button.node.firstChild as HTMLElement, 'keydown', {
  395. key: 'Enter'
  396. });
  397. expect(called).to.equal(true);
  398. button.dispose();
  399. });
  400. it('Space should activate the callback', async () => {
  401. let called = false;
  402. const button = new ToolbarButton({
  403. onClick: () => {
  404. called = true;
  405. }
  406. });
  407. Widget.attach(button, document.body);
  408. await framePromise();
  409. simulate(button.node.firstChild as HTMLElement, 'keydown', {
  410. key: ' '
  411. });
  412. expect(called).to.equal(true);
  413. button.dispose();
  414. });
  415. });
  416. });
  417. // describe('#onAfterAttach()', () => {
  418. // it('should add event listeners to the node', () => {
  419. // const button = new LogToolbarButton();
  420. // Widget.attach(button, document.body);
  421. // expect(button.methods).to.contain('onAfterAttach');
  422. // simulate(button.node, 'click');
  423. // expect(button.events).to.contain('click');
  424. // button.dispose();
  425. // });
  426. // });
  427. // describe('#onBeforeDetach()', () => {
  428. // it('should remove event listeners from the node', async () => {
  429. // const button = new LogToolbarButton();
  430. // Widget.attach(button, document.body);
  431. // await framePromise();
  432. // Widget.detach(button);
  433. // expect(button.methods).to.contain('onBeforeDetach');
  434. // simulate(button.node, 'click');
  435. // expect(button.events).to.not.contain('click');
  436. // button.dispose();
  437. // });
  438. // });
  439. });
  440. });