toolbar.spec.ts 17 KB

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