toolbar.spec.ts 17 KB

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