toolbar.spec.ts 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. CommandToolbarButton,
  5. createToolbarFactory,
  6. ReactiveToolbar,
  7. SessionContext,
  8. Toolbar,
  9. ToolbarButton,
  10. ToolbarRegistry,
  11. ToolbarWidgetRegistry
  12. } from '@jupyterlab/apputils';
  13. import { ISettingRegistry, SettingRegistry } from '@jupyterlab/settingregistry';
  14. import { IDataConnector } from '@jupyterlab/statedb';
  15. import {
  16. createSessionContext,
  17. framePromise,
  18. JupyterServer
  19. } from '@jupyterlab/testutils';
  20. import { ITranslator } from '@jupyterlab/translation';
  21. import {
  22. blankIcon,
  23. bugDotIcon,
  24. bugIcon,
  25. jupyterIcon
  26. } from '@jupyterlab/ui-components';
  27. import { toArray } from '@lumino/algorithm';
  28. import { CommandRegistry } from '@lumino/commands';
  29. import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
  30. import { PanelLayout, Widget } from '@lumino/widgets';
  31. import { simulate } from 'simulate-event';
  32. const server = new JupyterServer();
  33. beforeAll(async () => {
  34. await server.start();
  35. });
  36. afterAll(async () => {
  37. await server.shutdown();
  38. });
  39. describe('@jupyterlab/apputils', () => {
  40. describe('CommandToolbarButton', () => {
  41. let commands: CommandRegistry;
  42. const id = 'test-command';
  43. const options: CommandRegistry.ICommandOptions = {
  44. execute: jest.fn()
  45. };
  46. beforeEach(() => {
  47. commands = new CommandRegistry();
  48. });
  49. it('should render a command', async () => {
  50. commands.addCommand(id, options);
  51. const button = new CommandToolbarButton({
  52. commands,
  53. id
  54. });
  55. Widget.attach(button, document.body);
  56. await framePromise();
  57. expect(button.hasClass('jp-CommandToolbarButton')).toBe(true);
  58. simulate(button.node.firstElementChild!, 'mousedown');
  59. expect(options.execute).toBeCalledTimes(1);
  60. });
  61. it('should render the label command', async () => {
  62. const label = 'This is a test label';
  63. commands.addCommand(id, { ...options, label });
  64. const button = new CommandToolbarButton({
  65. commands,
  66. id
  67. });
  68. Widget.attach(button, document.body);
  69. await framePromise();
  70. expect(button.node.textContent).toMatch(label);
  71. });
  72. it('should render the customized label command', async () => {
  73. const label = 'This is a test label';
  74. const buttonLabel = 'This is the button label';
  75. commands.addCommand(id, { ...options, label });
  76. const button = new CommandToolbarButton({
  77. commands,
  78. id,
  79. label: buttonLabel
  80. });
  81. Widget.attach(button, document.body);
  82. await framePromise();
  83. expect(button.node.textContent).toMatch(buttonLabel);
  84. expect(button.node.textContent).not.toMatch(label);
  85. });
  86. it('should render the icon command', async () => {
  87. const icon = jupyterIcon;
  88. commands.addCommand(id, { ...options, icon });
  89. const button = new CommandToolbarButton({
  90. commands,
  91. id
  92. });
  93. Widget.attach(button, document.body);
  94. await framePromise();
  95. expect(button.node.getElementsByTagName('svg')[0].dataset.icon).toMatch(
  96. icon.name
  97. );
  98. });
  99. it('should render the customized icon command', async () => {
  100. const icon = jupyterIcon;
  101. const buttonIcon = blankIcon;
  102. commands.addCommand(id, { ...options, icon });
  103. const button = new CommandToolbarButton({
  104. commands,
  105. id,
  106. icon: buttonIcon
  107. });
  108. Widget.attach(button, document.body);
  109. await framePromise();
  110. const iconSVG = button.node.getElementsByTagName('svg')[0];
  111. expect(iconSVG.dataset.icon).toMatch(buttonIcon.name);
  112. expect(iconSVG.dataset.icon).not.toMatch(icon.name);
  113. });
  114. });
  115. describe('Toolbar', () => {
  116. let widget: Toolbar<Widget>;
  117. beforeEach(async () => {
  118. jest.setTimeout(20000);
  119. widget = new Toolbar();
  120. });
  121. afterEach(async () => {
  122. widget.dispose();
  123. });
  124. describe('#constructor()', () => {
  125. it('should construct a new toolbar widget', () => {
  126. const widget = new Toolbar();
  127. expect(widget).toBeInstanceOf(Toolbar);
  128. });
  129. it('should add the `jp-Toolbar` class', () => {
  130. const widget = new Toolbar();
  131. expect(widget.hasClass('jp-Toolbar')).toBe(true);
  132. });
  133. });
  134. describe('#names()', () => {
  135. it('should get an ordered list the toolbar item names', () => {
  136. widget.addItem('foo', new Widget());
  137. widget.addItem('bar', new Widget());
  138. widget.addItem('baz', new Widget());
  139. expect(toArray(widget.names())).toEqual(['foo', 'bar', 'baz']);
  140. });
  141. });
  142. describe('#addItem()', () => {
  143. it('should add an item to the toolbar', () => {
  144. const item = new Widget();
  145. expect(widget.addItem('test', item)).toBe(true);
  146. expect(toArray(widget.names())).toContain('test');
  147. });
  148. it('should add the `jp-Toolbar-item` class to the widget', () => {
  149. const item = new Widget();
  150. widget.addItem('test', item);
  151. expect(item.hasClass('jp-Toolbar-item')).toBe(true);
  152. });
  153. it('should return false if the name is already used', () => {
  154. widget.addItem('test', new Widget());
  155. expect(widget.addItem('test', new Widget())).toBe(false);
  156. });
  157. });
  158. describe('#insertItem()', () => {
  159. it('should insert the item into the toolbar', () => {
  160. widget.addItem('a', new Widget());
  161. widget.addItem('b', new Widget());
  162. widget.insertItem(1, 'c', new Widget());
  163. expect(toArray(widget.names())).toEqual(['a', 'c', 'b']);
  164. });
  165. it('should clamp the bounds', () => {
  166. widget.addItem('a', new Widget());
  167. widget.addItem('b', new Widget());
  168. widget.insertItem(10, 'c', new Widget());
  169. expect(toArray(widget.names())).toEqual(['a', 'b', 'c']);
  170. });
  171. });
  172. describe('#insertAfter()', () => {
  173. it('should insert an item into the toolbar after `c`', () => {
  174. widget.addItem('a', new Widget());
  175. widget.addItem('b', new Widget());
  176. widget.insertItem(1, 'c', new Widget());
  177. widget.insertAfter('c', 'd', new Widget());
  178. expect(toArray(widget.names())).toEqual(['a', 'c', 'd', 'b']);
  179. });
  180. it('should return false if the target item does not exist', () => {
  181. widget.addItem('a', new Widget());
  182. widget.addItem('b', new Widget());
  183. const value = widget.insertAfter('c', 'd', new Widget());
  184. expect(value).toBe(false);
  185. });
  186. });
  187. describe('#insertBefore()', () => {
  188. it('should insert an item into the toolbar before `c`', () => {
  189. widget.addItem('a', new Widget());
  190. widget.addItem('b', new Widget());
  191. widget.insertItem(1, 'c', new Widget());
  192. widget.insertBefore('c', 'd', new Widget());
  193. expect(toArray(widget.names())).toEqual(['a', 'd', 'c', 'b']);
  194. });
  195. it('should return false if the target item does not exist', () => {
  196. widget.addItem('a', new Widget());
  197. widget.addItem('b', new Widget());
  198. const value = widget.insertBefore('c', 'd', new Widget());
  199. expect(value).toBe(false);
  200. });
  201. });
  202. describe('.createFromCommand', () => {
  203. const commands = new CommandRegistry();
  204. const testLogCommandId = 'test:toolbar-log';
  205. const logArgs: ReadonlyPartialJSONObject[] = [];
  206. let enabled = false;
  207. let toggled = true;
  208. let visible = false;
  209. commands.addCommand(testLogCommandId, {
  210. execute: args => {
  211. logArgs.push(args);
  212. },
  213. label: 'Test log command label',
  214. caption: 'Test log command caption',
  215. usage: 'Test log command usage',
  216. iconClass: 'test-icon-class',
  217. className: 'test-log-class',
  218. isEnabled: () => enabled,
  219. isToggled: () => toggled,
  220. isVisible: () => visible
  221. });
  222. async function render(button: CommandToolbarButton) {
  223. button.update();
  224. await framePromise();
  225. expect(button.renderPromise).toBeDefined();
  226. await button.renderPromise;
  227. }
  228. it('should create a button', () => {
  229. const button = new CommandToolbarButton({
  230. commands,
  231. id: testLogCommandId
  232. });
  233. expect(button).toBeInstanceOf(CommandToolbarButton);
  234. button.dispose();
  235. });
  236. it('should add main class', async () => {
  237. const button = new CommandToolbarButton({
  238. commands,
  239. id: testLogCommandId
  240. });
  241. await render(button);
  242. const buttonNode = button.node.firstChild as HTMLButtonElement;
  243. expect(buttonNode.classList.contains('test-log-class')).toBe(true);
  244. button.dispose();
  245. });
  246. it('should add an icon with icon class and label', async () => {
  247. const button = new CommandToolbarButton({
  248. commands,
  249. id: testLogCommandId
  250. });
  251. await render(button);
  252. const buttonNode = button.node.firstChild as HTMLButtonElement;
  253. expect(buttonNode.title).toBe('Test log command caption');
  254. const wrapperNode = buttonNode.firstChild as HTMLElement;
  255. const iconNode = wrapperNode.firstChild as HTMLElement;
  256. expect(iconNode.classList.contains('test-icon-class')).toBe(true);
  257. button.dispose();
  258. });
  259. it('should apply state classes', async () => {
  260. enabled = false;
  261. toggled = true;
  262. visible = false;
  263. const button = new CommandToolbarButton({
  264. commands,
  265. id: testLogCommandId
  266. });
  267. await render(button);
  268. const buttonNode = button.node.firstChild as HTMLButtonElement;
  269. expect(buttonNode.disabled).toBe(true);
  270. expect(buttonNode.classList.contains('lm-mod-toggled')).toBe(true);
  271. expect(buttonNode.classList.contains('lm-mod-hidden')).toBe(true);
  272. button.dispose();
  273. });
  274. it('should update state classes', async () => {
  275. enabled = false;
  276. toggled = true;
  277. visible = false;
  278. const button = new CommandToolbarButton({
  279. commands,
  280. id: testLogCommandId
  281. });
  282. await render(button);
  283. const buttonNode = button.node.firstChild as HTMLButtonElement;
  284. expect(buttonNode.disabled).toBe(true);
  285. expect(buttonNode.classList.contains('lm-mod-toggled')).toBe(true);
  286. expect(buttonNode.classList.contains('lm-mod-hidden')).toBe(true);
  287. enabled = true;
  288. visible = true;
  289. commands.notifyCommandChanged(testLogCommandId);
  290. expect(buttonNode.disabled).toBe(false);
  291. expect(buttonNode.classList.contains('lm-mod-toggled')).toBe(true);
  292. expect(buttonNode.classList.contains('lm-mod-hidden')).toBe(false);
  293. enabled = false;
  294. visible = false;
  295. button.dispose();
  296. });
  297. it('should use the command label if no icon class/label', async () => {
  298. const id = 'to-be-removed';
  299. const cmd = commands.addCommand(id, {
  300. execute: () => {
  301. return;
  302. },
  303. label: 'Label-only button'
  304. });
  305. const button = new CommandToolbarButton({
  306. commands,
  307. id
  308. });
  309. await render(button);
  310. const buttonNode = button.node.firstChild as HTMLButtonElement;
  311. expect(buttonNode.textContent).toBe('Label-only button');
  312. cmd.dispose();
  313. });
  314. it('should update the node content on command change event', async () => {
  315. const id = 'to-be-removed';
  316. let iconClassValue: string = '';
  317. const cmd = commands.addCommand(id, {
  318. execute: () => {
  319. /* no op */
  320. },
  321. label: 'Label-only button',
  322. iconClass: () => iconClassValue ?? ''
  323. });
  324. const button = new CommandToolbarButton({
  325. commands,
  326. id
  327. });
  328. await render(button);
  329. const buttonNode = button.node.firstChild as HTMLButtonElement;
  330. expect(buttonNode.textContent).toBe('Label-only button');
  331. expect(buttonNode.classList.contains(iconClassValue)).toBe(false);
  332. iconClassValue = 'updated-icon-class';
  333. commands.notifyCommandChanged(id);
  334. await render(button);
  335. const wrapperNode = buttonNode.firstChild as HTMLElement;
  336. const iconNode = wrapperNode.firstChild as HTMLElement;
  337. expect(iconNode.classList.contains(iconClassValue)).toBe(true);
  338. cmd.dispose();
  339. });
  340. });
  341. describe('Kernel buttons', () => {
  342. let sessionContext: SessionContext;
  343. beforeEach(async () => {
  344. sessionContext = await createSessionContext();
  345. });
  346. afterEach(async () => {
  347. await sessionContext.shutdown();
  348. sessionContext.dispose();
  349. });
  350. describe('.createInterruptButton()', () => {
  351. it("should add an inline svg node with the 'stop' icon", async () => {
  352. const button = Toolbar.createInterruptButton(sessionContext);
  353. Widget.attach(button, document.body);
  354. await framePromise();
  355. expect(
  356. button.node.querySelector("[data-icon$='stop']")
  357. ).toBeDefined();
  358. });
  359. });
  360. describe('.createRestartButton()', () => {
  361. it("should add an inline svg node with the 'refresh' icon", async () => {
  362. const button = Toolbar.createRestartButton(sessionContext);
  363. Widget.attach(button, document.body);
  364. await framePromise();
  365. expect(
  366. button.node.querySelector("[data-icon$='refresh']")
  367. ).toBeDefined();
  368. });
  369. });
  370. describe('.createKernelNameItem()', () => {
  371. it("should display the `'display_name'` of the kernel", async () => {
  372. const item = Toolbar.createKernelNameItem(sessionContext);
  373. await sessionContext.initialize();
  374. Widget.attach(item, document.body);
  375. await framePromise();
  376. const node = item.node.querySelector(
  377. '.jp-ToolbarButtonComponent-label'
  378. )!;
  379. expect(node.textContent).toBe(sessionContext.kernelDisplayName);
  380. });
  381. });
  382. describe('.createKernelStatusItem()', () => {
  383. beforeEach(async () => {
  384. await sessionContext.initialize();
  385. await sessionContext.session?.kernel?.info;
  386. });
  387. it('should display a busy status if the kernel status is busy', async () => {
  388. const item = Toolbar.createKernelStatusItem(sessionContext);
  389. let called = false;
  390. sessionContext.statusChanged.connect((_, status) => {
  391. if (status === 'busy') {
  392. expect(
  393. item.node.querySelector("[data-icon$='circle']")
  394. ).toBeDefined();
  395. called = true;
  396. }
  397. });
  398. const future = sessionContext.session?.kernel?.requestExecute({
  399. code: 'a = 109\na'
  400. })!;
  401. await future.done;
  402. expect(called).toBe(true);
  403. });
  404. it('should show the current status in the node title', async () => {
  405. const item = Toolbar.createKernelStatusItem(sessionContext);
  406. const status = sessionContext.session?.kernel?.status;
  407. expect(item.node.title.toLowerCase()).toContain(status);
  408. let called = false;
  409. const future = sessionContext.session?.kernel?.requestExecute({
  410. code: 'a = 1'
  411. })!;
  412. future.onIOPub = msg => {
  413. if (sessionContext.session?.kernel?.status === 'busy') {
  414. expect(item.node.title.toLowerCase()).toContain('busy');
  415. called = true;
  416. }
  417. };
  418. await future.done;
  419. expect(called).toBe(true);
  420. });
  421. it('should handle a starting session', async () => {
  422. await sessionContext.session?.kernel?.info;
  423. await sessionContext.shutdown();
  424. sessionContext = await createSessionContext();
  425. await sessionContext.initialize();
  426. const item = Toolbar.createKernelStatusItem(sessionContext);
  427. expect(item.node.title).toBe('Kernel Connecting');
  428. expect(
  429. item.node.querySelector("[data-icon$='circle-empty']")
  430. ).toBeDefined();
  431. await sessionContext.initialize();
  432. await sessionContext.session?.kernel?.info;
  433. });
  434. });
  435. });
  436. });
  437. describe('ReactiveToolbar', () => {
  438. let toolbar: ReactiveToolbar;
  439. beforeEach(() => {
  440. toolbar = new ReactiveToolbar();
  441. Widget.attach(toolbar, document.body);
  442. });
  443. afterEach(() => {
  444. toolbar.dispose();
  445. });
  446. describe('#constructor()', () => {
  447. it('should append a node to body for the pop-up', () => {
  448. const popup = document.body.querySelector(
  449. '.jp-Toolbar-responsive-popup'
  450. );
  451. expect(popup).toBeDefined();
  452. expect(popup!.parentNode!.nodeName).toEqual('BODY');
  453. });
  454. });
  455. describe('#addItem()', () => {
  456. it('should insert item before the toolbar pop-up button', () => {
  457. const w = new Widget();
  458. toolbar.addItem('test', w);
  459. expect(
  460. (toolbar.layout as PanelLayout).widgets.findIndex(v => v === w)
  461. ).toEqual((toolbar.layout as PanelLayout).widgets.length - 2);
  462. });
  463. });
  464. describe('#insertItem()', () => {
  465. it('should insert item before the toolbar pop-up button', () => {
  466. const w = new Widget();
  467. toolbar.insertItem(2, 'test', w);
  468. expect(
  469. (toolbar.layout as PanelLayout).widgets.findIndex(v => v === w)
  470. ).toEqual((toolbar.layout as PanelLayout).widgets.length - 2);
  471. });
  472. });
  473. describe('#insertAfter()', () => {
  474. it('should not insert item after the toolbar pop-up button', () => {
  475. const w = new Widget();
  476. const r = toolbar.insertAfter('toolbar-popup-opener', 'test', w);
  477. expect(r).toEqual(false);
  478. expect(
  479. (toolbar.layout as PanelLayout).widgets.findIndex(v => v === w)
  480. ).toEqual(-1);
  481. });
  482. });
  483. });
  484. describe('ToolbarButton', () => {
  485. describe('#constructor()', () => {
  486. it('should accept no arguments', () => {
  487. const widget = new ToolbarButton();
  488. expect(widget).toBeInstanceOf(ToolbarButton);
  489. });
  490. it('should accept options', async () => {
  491. const widget = new ToolbarButton({
  492. className: 'foo',
  493. iconClass: 'iconFoo',
  494. onClick: () => {
  495. return void 0;
  496. },
  497. tooltip: 'bar'
  498. });
  499. Widget.attach(widget, document.body);
  500. await framePromise();
  501. const button = widget.node.firstChild as HTMLElement;
  502. expect(button.classList.contains('foo')).toBe(true);
  503. expect(button.querySelector('.iconFoo')).toBeDefined();
  504. expect(button.title).toBe('bar');
  505. });
  506. });
  507. describe('#dispose()', () => {
  508. it('should dispose of the resources used by the widget', () => {
  509. const button = new ToolbarButton();
  510. button.dispose();
  511. expect(button.isDisposed).toBe(true);
  512. });
  513. it('should be safe to call more than once', () => {
  514. const button = new ToolbarButton();
  515. button.dispose();
  516. button.dispose();
  517. expect(button.isDisposed).toBe(true);
  518. });
  519. });
  520. describe('#handleEvent()', () => {
  521. describe('click', () => {
  522. it('should activate the callback', async () => {
  523. let called = false;
  524. const button = new ToolbarButton({
  525. onClick: () => {
  526. called = true;
  527. }
  528. });
  529. Widget.attach(button, document.body);
  530. await framePromise();
  531. simulate(button.node.firstChild as HTMLElement, 'mousedown');
  532. expect(called).toBe(true);
  533. button.dispose();
  534. });
  535. });
  536. describe('keydown', () => {
  537. it('Enter should activate the callback', async () => {
  538. let called = false;
  539. const button = new ToolbarButton({
  540. onClick: () => {
  541. called = true;
  542. }
  543. });
  544. Widget.attach(button, document.body);
  545. await framePromise();
  546. simulate(button.node.firstChild as HTMLElement, 'keydown', {
  547. key: 'Enter'
  548. });
  549. expect(called).toBe(true);
  550. button.dispose();
  551. });
  552. it('Space should activate the callback', async () => {
  553. let called = false;
  554. const button = new ToolbarButton({
  555. onClick: () => {
  556. called = true;
  557. }
  558. });
  559. Widget.attach(button, document.body);
  560. await framePromise();
  561. simulate(button.node.firstChild as HTMLElement, 'keydown', {
  562. key: ' '
  563. });
  564. expect(called).toBe(true);
  565. button.dispose();
  566. });
  567. });
  568. });
  569. describe('#pressed()', () => {
  570. it('should update the pressed state', async () => {
  571. const widget = new ToolbarButton({
  572. icon: bugIcon,
  573. tooltip: 'tooltip',
  574. pressedTooltip: 'pressed tooltip',
  575. pressedIcon: bugDotIcon
  576. });
  577. Widget.attach(widget, document.body);
  578. await framePromise();
  579. const button = widget.node.firstChild as HTMLElement;
  580. expect(widget.pressed).toBe(false);
  581. expect(button.title).toBe('tooltip');
  582. expect(button.getAttribute('aria-pressed')).toEqual('false');
  583. let icon = button.querySelectorAll('svg');
  584. expect(icon[0].getAttribute('data-icon')).toEqual('ui-components:bug');
  585. widget.pressed = true;
  586. await framePromise();
  587. expect(widget.pressed).toBe(true);
  588. expect(button.title).toBe('pressed tooltip');
  589. expect(button.getAttribute('aria-pressed')).toEqual('true');
  590. icon = button.querySelectorAll('svg');
  591. expect(icon[0].getAttribute('data-icon')).toEqual(
  592. 'ui-components:bug-dot'
  593. );
  594. widget.dispose();
  595. });
  596. it('should not have the pressed state when not enabled', async () => {
  597. const widget = new ToolbarButton({
  598. icon: bugIcon,
  599. tooltip: 'tooltip',
  600. pressedTooltip: 'pressed tooltip',
  601. disabledTooltip: 'disabled tooltip',
  602. pressedIcon: bugDotIcon,
  603. enabled: false
  604. });
  605. Widget.attach(widget, document.body);
  606. await framePromise();
  607. const button = widget.node.firstChild as HTMLElement;
  608. expect(widget.pressed).toBe(false);
  609. expect(button.title).toBe('disabled tooltip');
  610. expect(button.getAttribute('aria-pressed')).toEqual('false');
  611. widget.pressed = true;
  612. await framePromise();
  613. expect(widget.pressed).toBe(false);
  614. expect(button.title).toBe('disabled tooltip');
  615. expect(button.getAttribute('aria-pressed')).toEqual('false');
  616. const icon = button.querySelectorAll('svg');
  617. expect(icon[0].getAttribute('data-icon')).toEqual('ui-components:bug');
  618. widget.dispose();
  619. });
  620. });
  621. describe('#enabled()', () => {
  622. it('should update the enabled state', async () => {
  623. const widget = new ToolbarButton({
  624. icon: bugIcon,
  625. tooltip: 'tooltip',
  626. pressedTooltip: 'pressed tooltip',
  627. disabledTooltip: 'disabled tooltip',
  628. pressedIcon: bugDotIcon
  629. });
  630. Widget.attach(widget, document.body);
  631. await framePromise();
  632. const button = widget.node.firstChild as HTMLElement;
  633. expect(widget.enabled).toBe(true);
  634. expect(widget.pressed).toBe(false);
  635. expect(button.getAttribute('aria-disabled')).toEqual('false');
  636. widget.pressed = true;
  637. await framePromise();
  638. expect(widget.pressed).toBe(true);
  639. widget.enabled = false;
  640. await framePromise();
  641. expect(widget.enabled).toBe(false);
  642. expect(widget.pressed).toBe(false);
  643. expect(button.getAttribute('aria-disabled')).toEqual('true');
  644. widget.dispose();
  645. });
  646. });
  647. describe('#onClick()', () => {
  648. it('should update the onClick state', async () => {
  649. let mockCalled = false;
  650. const mockOnClick = () => {
  651. mockCalled = true;
  652. };
  653. const widget = new ToolbarButton({
  654. icon: bugIcon,
  655. tooltip: 'tooltip',
  656. onClick: mockOnClick
  657. });
  658. Widget.attach(widget, document.body);
  659. await framePromise();
  660. simulate(widget.node.firstChild as HTMLElement, 'mousedown');
  661. expect(mockCalled).toBe(true);
  662. mockCalled = false;
  663. let mockUpdatedCalled = false;
  664. const mockOnClickUpdated = () => {
  665. mockUpdatedCalled = true;
  666. };
  667. widget.onClick = mockOnClickUpdated;
  668. await framePromise();
  669. simulate(widget.node.firstChild as HTMLElement, 'mousedown');
  670. expect(mockCalled).toBe(false);
  671. expect(mockUpdatedCalled).toBe(true);
  672. widget.dispose();
  673. });
  674. });
  675. });
  676. describe('ToolbarWidgetRegistry', () => {
  677. describe('#constructor', () => {
  678. it('should set a default factory', () => {
  679. const dummy = jest.fn();
  680. const registry = new ToolbarWidgetRegistry({
  681. defaultFactory: dummy
  682. });
  683. expect(registry.defaultFactory).toBe(dummy);
  684. });
  685. });
  686. describe('#defaultFactory', () => {
  687. it('should set a default factory', () => {
  688. const dummy = jest.fn();
  689. const dummy2 = jest.fn();
  690. const registry = new ToolbarWidgetRegistry({
  691. defaultFactory: dummy
  692. });
  693. registry.defaultFactory = dummy2;
  694. expect(registry.defaultFactory).toBe(dummy2);
  695. });
  696. });
  697. describe('#createWidget', () => {
  698. it('should call the default factory as fallback', () => {
  699. const documentWidget = new Widget();
  700. const dummyWidget = new Widget();
  701. const dummy = jest.fn().mockReturnValue(dummyWidget);
  702. const registry = new ToolbarWidgetRegistry({
  703. defaultFactory: dummy
  704. });
  705. const item: ToolbarRegistry.IWidget = {
  706. name: 'test'
  707. };
  708. const widget = registry.createWidget('factory', documentWidget, item);
  709. expect(widget).toBe(dummyWidget);
  710. expect(dummy).toBeCalledWith('factory', documentWidget, item);
  711. });
  712. it('should call the registered factory', () => {
  713. const documentWidget = new Widget();
  714. const dummyWidget = new Widget();
  715. const defaultFactory = jest.fn().mockReturnValue(dummyWidget);
  716. const dummy = jest.fn().mockReturnValue(dummyWidget);
  717. const registry = new ToolbarWidgetRegistry({
  718. defaultFactory
  719. });
  720. const item: ToolbarRegistry.IWidget = {
  721. name: 'test'
  722. };
  723. registry.registerFactory('factory', item.name, dummy);
  724. const widget = registry.createWidget('factory', documentWidget, item);
  725. expect(widget).toBe(dummyWidget);
  726. expect(dummy).toBeCalledWith(documentWidget);
  727. expect(defaultFactory).toBeCalledTimes(0);
  728. });
  729. });
  730. describe('#registerFactory', () => {
  731. it('should return the previous registered factory', () => {
  732. const defaultFactory = jest.fn();
  733. const dummy = jest.fn();
  734. const dummy2 = jest.fn();
  735. const registry = new ToolbarWidgetRegistry({
  736. defaultFactory
  737. });
  738. const item: ToolbarRegistry.IWidget = {
  739. name: 'test'
  740. };
  741. expect(
  742. registry.registerFactory('factory', item.name, dummy)
  743. ).toBeUndefined();
  744. expect(registry.registerFactory('factory', item.name, dummy2)).toBe(
  745. dummy
  746. );
  747. });
  748. });
  749. });
  750. describe('createToolbarFactory', () => {
  751. it('should return the toolbar items', async () => {
  752. const factoryName = 'dummyFactory';
  753. const pluginId = 'test-plugin:settings';
  754. const toolbarRegistry = new ToolbarWidgetRegistry({
  755. defaultFactory: jest.fn()
  756. });
  757. const bar: ISettingRegistry.IPlugin = {
  758. data: {
  759. composite: {},
  760. user: {}
  761. },
  762. id: pluginId,
  763. raw: '{}',
  764. schema: {
  765. 'jupyter.lab.toolbars': {
  766. dummyFactory: [
  767. {
  768. name: 'insert',
  769. command: 'notebook:insert-cell-below',
  770. rank: 20
  771. },
  772. { name: 'spacer', type: 'spacer', rank: 100 },
  773. { name: 'cut', command: 'notebook:cut-cell', rank: 21 },
  774. {
  775. name: 'clear-all',
  776. command: 'notebook:clear-all-cell-outputs',
  777. rank: 60,
  778. disabled: true
  779. }
  780. ]
  781. },
  782. 'jupyter.lab.transform': true,
  783. properties: {
  784. toolbar: {
  785. type: 'array'
  786. }
  787. },
  788. type: 'object'
  789. },
  790. version: 'test'
  791. };
  792. const connector: IDataConnector<
  793. ISettingRegistry.IPlugin,
  794. string,
  795. string,
  796. string
  797. > = {
  798. fetch: jest.fn().mockImplementation((id: string) => {
  799. switch (id) {
  800. case bar.id:
  801. return bar;
  802. default:
  803. return {};
  804. }
  805. }),
  806. list: jest.fn(),
  807. save: jest.fn(),
  808. remove: jest.fn()
  809. };
  810. const settingRegistry = new SettingRegistry({
  811. connector
  812. });
  813. const translator: ITranslator = {
  814. load: jest.fn()
  815. };
  816. const factory = createToolbarFactory(
  817. toolbarRegistry,
  818. settingRegistry,
  819. factoryName,
  820. pluginId,
  821. translator
  822. );
  823. await settingRegistry.load(bar.id);
  824. // Trick push this test after all other promise in the hope they get resolve
  825. // before going further - in particular we are looking at the update of the items
  826. // factory in `createToolbarFactory`
  827. await Promise.resolve();
  828. const items = factory(new Widget());
  829. expect(items).toHaveLength(3);
  830. expect(items.get(0).name).toEqual('insert');
  831. expect(items.get(1).name).toEqual('cut');
  832. expect(items.get(2).name).toEqual('spacer');
  833. });
  834. it('should update the toolbar items with late settings load', async () => {
  835. const factoryName = 'dummyFactory';
  836. const pluginId = 'test-plugin:settings';
  837. const toolbarRegistry = new ToolbarWidgetRegistry({
  838. defaultFactory: jest.fn()
  839. });
  840. const foo: ISettingRegistry.IPlugin = {
  841. data: {
  842. composite: {},
  843. user: {}
  844. },
  845. id: 'foo',
  846. raw: '{}',
  847. schema: {
  848. 'jupyter.lab.toolbars': {
  849. dummyFactory: [
  850. { name: 'cut', command: 'notebook:cut-cell', rank: 21 },
  851. { name: 'insert', rank: 40 },
  852. {
  853. name: 'clear-all',
  854. disabled: true
  855. }
  856. ]
  857. },
  858. type: 'object'
  859. },
  860. version: 'test'
  861. };
  862. const bar: ISettingRegistry.IPlugin = {
  863. data: {
  864. composite: {},
  865. user: {}
  866. },
  867. id: pluginId,
  868. raw: '{}',
  869. schema: {
  870. 'jupyter.lab.toolbars': {
  871. dummyFactory: [
  872. {
  873. name: 'insert',
  874. command: 'notebook:insert-cell-below',
  875. rank: 20
  876. },
  877. {
  878. name: 'clear-all',
  879. command: 'notebook:clear-all-cell-outputs',
  880. rank: 60
  881. }
  882. ]
  883. },
  884. 'jupyter.lab.transform': true,
  885. properties: {
  886. toolbar: {
  887. type: 'array'
  888. }
  889. },
  890. type: 'object'
  891. },
  892. version: 'test'
  893. };
  894. const connector: IDataConnector<
  895. ISettingRegistry.IPlugin,
  896. string,
  897. string,
  898. string
  899. > = {
  900. fetch: jest.fn().mockImplementation((id: string) => {
  901. switch (id) {
  902. case bar.id:
  903. return bar;
  904. case foo.id:
  905. return foo;
  906. default:
  907. return {};
  908. }
  909. }),
  910. list: jest.fn(),
  911. save: jest.fn(),
  912. remove: jest.fn()
  913. };
  914. const settingRegistry = new SettingRegistry({
  915. connector
  916. });
  917. const translator: ITranslator = {
  918. load: jest.fn()
  919. };
  920. const factory = createToolbarFactory(
  921. toolbarRegistry,
  922. settingRegistry,
  923. factoryName,
  924. pluginId,
  925. translator
  926. );
  927. await settingRegistry.load(bar.id);
  928. // Trick push this test after all other promise in the hope they get resolve
  929. // before going further - in particular we are looking at the update of the items
  930. // factory in `createToolbarFactory`
  931. await Promise.resolve();
  932. await settingRegistry.load(foo.id);
  933. const items = factory(new Widget());
  934. expect(items).toHaveLength(2);
  935. expect(items.get(0).name).toEqual('cut');
  936. expect(items.get(1).name).toEqual('insert');
  937. });
  938. });
  939. });