toolbar.spec.ts 35 KB

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