// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { CommandToolbarButton, createToolbarFactory, ReactiveToolbar, SessionContext, Toolbar, ToolbarButton, ToolbarRegistry, ToolbarWidgetRegistry } from '@jupyterlab/apputils'; import { ISettingRegistry, SettingRegistry } from '@jupyterlab/settingregistry'; import { IDataConnector } from '@jupyterlab/statedb'; import { createSessionContext, framePromise, JupyterServer } from '@jupyterlab/testutils'; import { ITranslator } from '@jupyterlab/translation'; import { blankIcon, bugDotIcon, bugIcon, jupyterIcon } from '@jupyterlab/ui-components'; import { toArray } from '@lumino/algorithm'; import { CommandRegistry } from '@lumino/commands'; import { JSONExt, PromiseDelegate, ReadonlyPartialJSONObject } from '@lumino/coreutils'; import { PanelLayout, Widget } from '@lumino/widgets'; import { simulate } from 'simulate-event'; const server = new JupyterServer(); beforeAll(async () => { await server.start(); }); afterAll(async () => { await server.shutdown(); }); describe('@jupyterlab/apputils', () => { describe('CommandToolbarButton', () => { let commands: CommandRegistry; const id = 'test-command'; const options: CommandRegistry.ICommandOptions = { execute: jest.fn() }; beforeEach(() => { commands = new CommandRegistry(); }); it('should render a command', async () => { commands.addCommand(id, options); const button = new CommandToolbarButton({ commands, id }); Widget.attach(button, document.body); await framePromise(); expect(button.hasClass('jp-CommandToolbarButton')).toBe(true); simulate(button.node.firstElementChild!, 'mousedown'); expect(options.execute).toBeCalledTimes(1); }); it('should render the label command', async () => { const label = 'This is a test label'; commands.addCommand(id, { ...options, label }); const button = new CommandToolbarButton({ commands, id }); Widget.attach(button, document.body); await framePromise(); expect(button.node.textContent).toMatch(label); }); it('should render the customized label command', async () => { const label = 'This is a test label'; const buttonLabel = 'This is the button label'; commands.addCommand(id, { ...options, label }); const button = new CommandToolbarButton({ commands, id, label: buttonLabel }); Widget.attach(button, document.body); await framePromise(); expect(button.node.textContent).toMatch(buttonLabel); expect(button.node.textContent).not.toMatch(label); }); it('should render the icon command', async () => { const icon = jupyterIcon; commands.addCommand(id, { ...options, icon }); const button = new CommandToolbarButton({ commands, id }); Widget.attach(button, document.body); await framePromise(); expect(button.node.getElementsByTagName('svg')[0].dataset.icon).toMatch( icon.name ); }); it('should render the customized icon command', async () => { const icon = jupyterIcon; const buttonIcon = blankIcon; commands.addCommand(id, { ...options, icon }); const button = new CommandToolbarButton({ commands, id, icon: buttonIcon }); Widget.attach(button, document.body); await framePromise(); const iconSVG = button.node.getElementsByTagName('svg')[0]; expect(iconSVG.dataset.icon).toMatch(buttonIcon.name); expect(iconSVG.dataset.icon).not.toMatch(icon.name); }); }); describe('Toolbar', () => { let widget: Toolbar; beforeEach(async () => { jest.setTimeout(20000); widget = new Toolbar(); }); afterEach(async () => { widget.dispose(); }); describe('#constructor()', () => { it('should construct a new toolbar widget', () => { const widget = new Toolbar(); expect(widget).toBeInstanceOf(Toolbar); }); it('should add the `jp-Toolbar` class', () => { const widget = new Toolbar(); expect(widget.hasClass('jp-Toolbar')).toBe(true); }); }); describe('#names()', () => { it('should get an ordered list the toolbar item names', () => { widget.addItem('foo', new Widget()); widget.addItem('bar', new Widget()); widget.addItem('baz', new Widget()); expect(toArray(widget.names())).toEqual(['foo', 'bar', 'baz']); }); }); describe('#addItem()', () => { it('should add an item to the toolbar', () => { const item = new Widget(); expect(widget.addItem('test', item)).toBe(true); expect(toArray(widget.names())).toContain('test'); }); it('should add the `jp-Toolbar-item` class to the widget', () => { const item = new Widget(); widget.addItem('test', item); expect(item.hasClass('jp-Toolbar-item')).toBe(true); }); it('should return false if the name is already used', () => { widget.addItem('test', new Widget()); expect(widget.addItem('test', new Widget())).toBe(false); }); }); describe('#insertItem()', () => { it('should insert the item into the toolbar', () => { widget.addItem('a', new Widget()); widget.addItem('b', new Widget()); widget.insertItem(1, 'c', new Widget()); expect(toArray(widget.names())).toEqual(['a', 'c', 'b']); }); it('should clamp the bounds', () => { widget.addItem('a', new Widget()); widget.addItem('b', new Widget()); widget.insertItem(10, 'c', new Widget()); expect(toArray(widget.names())).toEqual(['a', 'b', 'c']); }); }); describe('#insertAfter()', () => { it('should insert an item into the toolbar after `c`', () => { widget.addItem('a', new Widget()); widget.addItem('b', new Widget()); widget.insertItem(1, 'c', new Widget()); widget.insertAfter('c', 'd', new Widget()); expect(toArray(widget.names())).toEqual(['a', 'c', 'd', 'b']); }); it('should return false if the target item does not exist', () => { widget.addItem('a', new Widget()); widget.addItem('b', new Widget()); const value = widget.insertAfter('c', 'd', new Widget()); expect(value).toBe(false); }); }); describe('#insertBefore()', () => { it('should insert an item into the toolbar before `c`', () => { widget.addItem('a', new Widget()); widget.addItem('b', new Widget()); widget.insertItem(1, 'c', new Widget()); widget.insertBefore('c', 'd', new Widget()); expect(toArray(widget.names())).toEqual(['a', 'd', 'c', 'b']); }); it('should return false if the target item does not exist', () => { widget.addItem('a', new Widget()); widget.addItem('b', new Widget()); const value = widget.insertBefore('c', 'd', new Widget()); expect(value).toBe(false); }); }); describe('.createFromCommand', () => { const commands = new CommandRegistry(); const testLogCommandId = 'test:toolbar-log'; const logArgs: ReadonlyPartialJSONObject[] = []; let enabled = false; let toggled = true; let visible = false; commands.addCommand(testLogCommandId, { execute: args => { logArgs.push(args); }, label: 'Test log command label', caption: 'Test log command caption', usage: 'Test log command usage', iconClass: 'test-icon-class', className: 'test-log-class', isEnabled: () => enabled, isToggled: () => toggled, isVisible: () => visible }); async function render(button: CommandToolbarButton) { button.update(); await framePromise(); expect(button.renderPromise).toBeDefined(); await button.renderPromise; } it('should create a button', () => { const button = new CommandToolbarButton({ commands, id: testLogCommandId }); expect(button).toBeInstanceOf(CommandToolbarButton); button.dispose(); }); it('should add main class', async () => { const button = new CommandToolbarButton({ commands, id: testLogCommandId }); await render(button); const buttonNode = button.node.firstChild as HTMLButtonElement; expect(buttonNode.classList.contains('test-log-class')).toBe(true); button.dispose(); }); it('should add an icon with icon class and label', async () => { const button = new CommandToolbarButton({ commands, id: testLogCommandId }); await render(button); const buttonNode = button.node.firstChild as HTMLButtonElement; expect(buttonNode.title).toBe('Test log command caption'); const wrapperNode = buttonNode.firstChild as HTMLElement; const iconNode = wrapperNode.firstChild as HTMLElement; expect(iconNode.classList.contains('test-icon-class')).toBe(true); button.dispose(); }); it('should apply state classes', async () => { enabled = false; toggled = true; visible = false; const button = new CommandToolbarButton({ commands, id: testLogCommandId }); await render(button); const buttonNode = button.node.firstChild as HTMLButtonElement; expect(buttonNode.disabled).toBe(true); expect(buttonNode.classList.contains('lm-mod-toggled')).toBe(true); expect(buttonNode.classList.contains('lm-mod-hidden')).toBe(true); button.dispose(); }); it('should update state classes', async () => { enabled = false; toggled = true; visible = false; const button = new CommandToolbarButton({ commands, id: testLogCommandId }); await render(button); const buttonNode = button.node.firstChild as HTMLButtonElement; expect(buttonNode.disabled).toBe(true); expect(buttonNode.classList.contains('lm-mod-toggled')).toBe(true); expect(buttonNode.classList.contains('lm-mod-hidden')).toBe(true); enabled = true; visible = true; commands.notifyCommandChanged(testLogCommandId); expect(buttonNode.disabled).toBe(false); expect(buttonNode.classList.contains('lm-mod-toggled')).toBe(true); expect(buttonNode.classList.contains('lm-mod-hidden')).toBe(false); enabled = false; visible = false; button.dispose(); }); it('should use the command label if no icon class/label', async () => { const id = 'to-be-removed'; const cmd = commands.addCommand(id, { execute: () => { return; }, label: 'Label-only button' }); const button = new CommandToolbarButton({ commands, id }); await render(button); const buttonNode = button.node.firstChild as HTMLButtonElement; expect(buttonNode.textContent).toBe('Label-only button'); cmd.dispose(); }); it('should update the node content on command change event', async () => { const id = 'to-be-removed'; let iconClassValue: string = ''; const cmd = commands.addCommand(id, { execute: () => { /* no op */ }, label: 'Label-only button', iconClass: () => iconClassValue ?? '' }); const button = new CommandToolbarButton({ commands, id }); await render(button); const buttonNode = button.node.firstChild as HTMLButtonElement; expect(buttonNode.textContent).toBe('Label-only button'); expect(buttonNode.classList.contains(iconClassValue)).toBe(false); iconClassValue = 'updated-icon-class'; commands.notifyCommandChanged(id); await render(button); const wrapperNode = buttonNode.firstChild as HTMLElement; const iconNode = wrapperNode.firstChild as HTMLElement; expect(iconNode.classList.contains(iconClassValue)).toBe(true); cmd.dispose(); }); }); describe('Kernel buttons', () => { let sessionContext: SessionContext; beforeEach(async () => { sessionContext = await createSessionContext(); }); afterEach(async () => { await sessionContext.shutdown(); sessionContext.dispose(); }); describe('.createInterruptButton()', () => { it("should add an inline svg node with the 'stop' icon", async () => { const button = Toolbar.createInterruptButton(sessionContext); Widget.attach(button, document.body); await framePromise(); expect( button.node.querySelector("[data-icon$='stop']") ).toBeDefined(); }); }); describe('.createRestartButton()', () => { it("should add an inline svg node with the 'refresh' icon", async () => { const button = Toolbar.createRestartButton(sessionContext); Widget.attach(button, document.body); await framePromise(); expect( button.node.querySelector("[data-icon$='refresh']") ).toBeDefined(); }); }); describe('.createKernelNameItem()', () => { it("should display the `'display_name'` of the kernel", async () => { const item = Toolbar.createKernelNameItem(sessionContext); await sessionContext.initialize(); Widget.attach(item, document.body); await framePromise(); const node = item.node.querySelector( '.jp-ToolbarButtonComponent-label' )!; expect(node.textContent).toBe(sessionContext.kernelDisplayName); }); }); describe('.createKernelStatusItem()', () => { beforeEach(async () => { await sessionContext.initialize(); await sessionContext.session?.kernel?.info; }); it('should display a busy status if the kernel status is busy', async () => { const item = Toolbar.createKernelStatusItem(sessionContext); let called = false; sessionContext.statusChanged.connect((_, status) => { if (status === 'busy') { expect( item.node.querySelector("[data-icon$='circle']") ).toBeDefined(); called = true; } }); const future = sessionContext.session?.kernel?.requestExecute({ code: 'a = 109\na' })!; await future.done; expect(called).toBe(true); }); it('should show the current status in the node title', async () => { const item = Toolbar.createKernelStatusItem(sessionContext); const status = sessionContext.session?.kernel?.status; expect(item.node.title.toLowerCase()).toContain(status); let called = false; const future = sessionContext.session?.kernel?.requestExecute({ code: 'a = 1' })!; future.onIOPub = msg => { if (sessionContext.session?.kernel?.status === 'busy') { expect(item.node.title.toLowerCase()).toContain('busy'); called = true; } }; await future.done; expect(called).toBe(true); }); it('should handle a starting session', async () => { await sessionContext.session?.kernel?.info; await sessionContext.shutdown(); sessionContext = await createSessionContext(); await sessionContext.initialize(); const item = Toolbar.createKernelStatusItem(sessionContext); expect(item.node.title).toBe('Kernel Connecting'); expect( item.node.querySelector("[data-icon$='circle-empty']") ).toBeDefined(); await sessionContext.initialize(); await sessionContext.session?.kernel?.info; }); }); }); }); describe('ReactiveToolbar', () => { let toolbar: ReactiveToolbar; beforeEach(() => { toolbar = new ReactiveToolbar(); Widget.attach(toolbar, document.body); }); afterEach(() => { toolbar.dispose(); }); describe('#constructor()', () => { it('should append a node to body for the pop-up', () => { const popup = document.body.querySelector( '.jp-Toolbar-responsive-popup' ); expect(popup).toBeDefined(); expect(popup!.parentNode!.nodeName).toEqual('BODY'); }); }); describe('#addItem()', () => { it('should insert item before the toolbar pop-up button', () => { const w = new Widget(); toolbar.addItem('test', w); expect( (toolbar.layout as PanelLayout).widgets.findIndex(v => v === w) ).toEqual((toolbar.layout as PanelLayout).widgets.length - 2); }); }); describe('#insertItem()', () => { it('should insert item before the toolbar pop-up button', () => { const w = new Widget(); toolbar.insertItem(2, 'test', w); expect( (toolbar.layout as PanelLayout).widgets.findIndex(v => v === w) ).toEqual((toolbar.layout as PanelLayout).widgets.length - 2); }); }); describe('#insertAfter()', () => { it('should not insert item after the toolbar pop-up button', () => { const w = new Widget(); const r = toolbar.insertAfter('toolbar-popup-opener', 'test', w); expect(r).toEqual(false); expect( (toolbar.layout as PanelLayout).widgets.findIndex(v => v === w) ).toEqual(-1); }); }); }); describe('ToolbarButton', () => { describe('#constructor()', () => { it('should accept no arguments', () => { const widget = new ToolbarButton(); expect(widget).toBeInstanceOf(ToolbarButton); }); it('should accept options', async () => { const widget = new ToolbarButton({ className: 'foo', iconClass: 'iconFoo', onClick: () => { return void 0; }, tooltip: 'bar' }); Widget.attach(widget, document.body); await framePromise(); const button = widget.node.firstChild as HTMLElement; expect(button.classList.contains('foo')).toBe(true); expect(button.querySelector('.iconFoo')).toBeDefined(); expect(button.title).toBe('bar'); }); }); describe('#dispose()', () => { it('should dispose of the resources used by the widget', () => { const button = new ToolbarButton(); button.dispose(); expect(button.isDisposed).toBe(true); }); it('should be safe to call more than once', () => { const button = new ToolbarButton(); button.dispose(); button.dispose(); expect(button.isDisposed).toBe(true); }); }); describe('#handleEvent()', () => { describe('click', () => { it('should activate the callback', async () => { let called = false; const button = new ToolbarButton({ onClick: () => { called = true; } }); Widget.attach(button, document.body); await framePromise(); simulate(button.node.firstChild as HTMLElement, 'mousedown'); expect(called).toBe(true); button.dispose(); }); }); describe('keydown', () => { it('Enter should activate the callback', async () => { let called = false; const button = new ToolbarButton({ onClick: () => { called = true; } }); Widget.attach(button, document.body); await framePromise(); simulate(button.node.firstChild as HTMLElement, 'keydown', { key: 'Enter' }); expect(called).toBe(true); button.dispose(); }); it('Space should activate the callback', async () => { let called = false; const button = new ToolbarButton({ onClick: () => { called = true; } }); Widget.attach(button, document.body); await framePromise(); simulate(button.node.firstChild as HTMLElement, 'keydown', { key: ' ' }); expect(called).toBe(true); button.dispose(); }); }); }); describe('#pressed()', () => { it('should update the pressed state', async () => { const widget = new ToolbarButton({ icon: bugIcon, tooltip: 'tooltip', pressedTooltip: 'pressed tooltip', pressedIcon: bugDotIcon }); Widget.attach(widget, document.body); await framePromise(); const button = widget.node.firstChild as HTMLElement; expect(widget.pressed).toBe(false); expect(button.title).toBe('tooltip'); expect(button.getAttribute('aria-pressed')).toEqual('false'); let icon = button.querySelectorAll('svg'); expect(icon[0].getAttribute('data-icon')).toEqual('ui-components:bug'); widget.pressed = true; await framePromise(); expect(widget.pressed).toBe(true); expect(button.title).toBe('pressed tooltip'); expect(button.getAttribute('aria-pressed')).toEqual('true'); icon = button.querySelectorAll('svg'); expect(icon[0].getAttribute('data-icon')).toEqual( 'ui-components:bug-dot' ); widget.dispose(); }); it('should not have the pressed state when not enabled', async () => { const widget = new ToolbarButton({ icon: bugIcon, tooltip: 'tooltip', pressedTooltip: 'pressed tooltip', disabledTooltip: 'disabled tooltip', pressedIcon: bugDotIcon, enabled: false }); Widget.attach(widget, document.body); await framePromise(); const button = widget.node.firstChild as HTMLElement; expect(widget.pressed).toBe(false); expect(button.title).toBe('disabled tooltip'); expect(button.getAttribute('aria-pressed')).toEqual('false'); widget.pressed = true; await framePromise(); expect(widget.pressed).toBe(false); expect(button.title).toBe('disabled tooltip'); expect(button.getAttribute('aria-pressed')).toEqual('false'); const icon = button.querySelectorAll('svg'); expect(icon[0].getAttribute('data-icon')).toEqual('ui-components:bug'); widget.dispose(); }); }); describe('#enabled()', () => { it('should update the enabled state', async () => { const widget = new ToolbarButton({ icon: bugIcon, tooltip: 'tooltip', pressedTooltip: 'pressed tooltip', disabledTooltip: 'disabled tooltip', pressedIcon: bugDotIcon }); Widget.attach(widget, document.body); await framePromise(); const button = widget.node.firstChild as HTMLElement; expect(widget.enabled).toBe(true); expect(widget.pressed).toBe(false); expect(button.getAttribute('aria-disabled')).toEqual('false'); widget.pressed = true; await framePromise(); expect(widget.pressed).toBe(true); widget.enabled = false; await framePromise(); expect(widget.enabled).toBe(false); expect(widget.pressed).toBe(false); expect(button.getAttribute('aria-disabled')).toEqual('true'); widget.dispose(); }); }); describe('#onClick()', () => { it('should update the onClick state', async () => { let mockCalled = false; const mockOnClick = () => { mockCalled = true; }; const widget = new ToolbarButton({ icon: bugIcon, tooltip: 'tooltip', onClick: mockOnClick }); Widget.attach(widget, document.body); await framePromise(); simulate(widget.node.firstChild as HTMLElement, 'mousedown'); expect(mockCalled).toBe(true); mockCalled = false; let mockUpdatedCalled = false; const mockOnClickUpdated = () => { mockUpdatedCalled = true; }; widget.onClick = mockOnClickUpdated; await framePromise(); simulate(widget.node.firstChild as HTMLElement, 'mousedown'); expect(mockCalled).toBe(false); expect(mockUpdatedCalled).toBe(true); widget.dispose(); }); }); }); describe('ToolbarWidgetRegistry', () => { describe('#constructor', () => { it('should set a default factory', () => { const dummy = jest.fn(); const registry = new ToolbarWidgetRegistry({ defaultFactory: dummy }); expect(registry.defaultFactory).toBe(dummy); }); }); describe('#defaultFactory', () => { it('should set a default factory', () => { const dummy = jest.fn(); const dummy2 = jest.fn(); const registry = new ToolbarWidgetRegistry({ defaultFactory: dummy }); registry.defaultFactory = dummy2; expect(registry.defaultFactory).toBe(dummy2); }); }); describe('#createWidget', () => { it('should call the default factory as fallback', () => { const documentWidget = new Widget(); const dummyWidget = new Widget(); const dummy = jest.fn().mockReturnValue(dummyWidget); const registry = new ToolbarWidgetRegistry({ defaultFactory: dummy }); const item: ToolbarRegistry.IWidget = { name: 'test' }; const widget = registry.createWidget('factory', documentWidget, item); expect(widget).toBe(dummyWidget); expect(dummy).toBeCalledWith('factory', documentWidget, item); }); it('should call the registered factory', () => { const documentWidget = new Widget(); const dummyWidget = new Widget(); const defaultFactory = jest.fn().mockReturnValue(dummyWidget); const dummy = jest.fn().mockReturnValue(dummyWidget); const registry = new ToolbarWidgetRegistry({ defaultFactory }); const item: ToolbarRegistry.IWidget = { name: 'test' }; registry.registerFactory('factory', item.name, dummy); const widget = registry.createWidget('factory', documentWidget, item); expect(widget).toBe(dummyWidget); expect(dummy).toBeCalledWith(documentWidget); expect(defaultFactory).toBeCalledTimes(0); }); }); describe('#registerFactory', () => { it('should return the previous registered factory', () => { const defaultFactory = jest.fn(); const dummy = jest.fn(); const dummy2 = jest.fn(); const registry = new ToolbarWidgetRegistry({ defaultFactory }); const item: ToolbarRegistry.IWidget = { name: 'test' }; expect( registry.registerFactory('factory', item.name, dummy) ).toBeUndefined(); expect(registry.registerFactory('factory', item.name, dummy2)).toBe( dummy ); }); }); }); describe('createToolbarFactory', () => { it('should return the toolbar items', async () => { const factoryName = 'dummyFactory'; const pluginId = 'test-plugin:settings'; const toolbarRegistry = new ToolbarWidgetRegistry({ defaultFactory: jest.fn() }); const bar: ISettingRegistry.IPlugin = { data: { composite: {}, user: {} }, id: pluginId, raw: '{}', schema: { 'jupyter.lab.toolbars': { dummyFactory: [ { name: 'insert', command: 'notebook:insert-cell-below', rank: 20 }, { name: 'spacer', type: 'spacer', rank: 100 }, { name: 'cut', command: 'notebook:cut-cell', rank: 21 }, { name: 'clear-all', command: 'notebook:clear-all-cell-outputs', rank: 60, disabled: true } ] }, 'jupyter.lab.transform': true, properties: { toolbar: { type: 'array' } }, type: 'object' }, version: 'test' }; const connector: IDataConnector< ISettingRegistry.IPlugin, string, string, string > = { fetch: jest.fn().mockImplementation((id: string) => { switch (id) { case bar.id: return bar; default: return {}; } }), list: jest.fn(), save: jest.fn(), remove: jest.fn() }; const settingRegistry = new SettingRegistry({ connector }); const translator: ITranslator = { load: jest.fn() }; const factory = createToolbarFactory( toolbarRegistry, settingRegistry, factoryName, pluginId, translator ); await settingRegistry.load(bar.id); const items = factory(new Widget()); expect(items).toHaveLength(3); expect(items.get(0).name).toEqual('insert'); expect(items.get(1).name).toEqual('cut'); expect(items.get(2).name).toEqual('spacer'); }); it('should update the toolbar items with late settings load', async () => { const factoryName = 'dummyFactory'; const pluginId = 'test-plugin:settings'; const toolbarRegistry = new ToolbarWidgetRegistry({ defaultFactory: jest.fn() }); const foo: ISettingRegistry.IPlugin = { data: { composite: {}, user: {} }, id: 'foo', raw: '{}', schema: { 'jupyter.lab.toolbars': { dummyFactory: [ { name: 'cut', command: 'notebook:cut-cell', rank: 21 }, { name: 'insert', rank: 40 }, { name: 'clear-all', disabled: true } ] }, type: 'object' }, version: 'test' }; const bar: ISettingRegistry.IPlugin = { data: { composite: {}, user: {} }, id: pluginId, raw: '{}', schema: { 'jupyter.lab.toolbars': { dummyFactory: [ { name: 'insert', command: 'notebook:insert-cell-below', rank: 20 }, { name: 'clear-all', command: 'notebook:clear-all-cell-outputs', rank: 60 } ] }, 'jupyter.lab.transform': true, properties: { toolbar: { type: 'array' } }, type: 'object' }, version: 'test' }; const connector: IDataConnector< ISettingRegistry.IPlugin, string, string, string > = { fetch: jest.fn().mockImplementation((id: string) => { switch (id) { case bar.id: return bar; case foo.id: return foo; default: return {}; } }), list: jest.fn(), save: jest.fn(), remove: jest.fn() }; const settingRegistry = new SettingRegistry({ connector }); const translator: ITranslator = { load: jest.fn() }; const factory = createToolbarFactory( toolbarRegistry, settingRegistry, factoryName, pluginId, translator ); const barPlugin = await settingRegistry.load(bar.id); const baseToolbar = JSONExt.deepCopy( barPlugin.composite['toolbar'] as any ); let waitForChange = new PromiseDelegate(); barPlugin.changed.connect(() => { if ( !JSONExt.deepEqual(baseToolbar, barPlugin.composite['toolbar'] as any) ) { waitForChange.resolve(); } }); await settingRegistry.load(foo.id); await waitForChange.promise; const items = factory(new Widget()); expect(items).toHaveLength(2); expect(items.get(0).name).toEqual('cut'); expect(items.get(1).name).toEqual('insert'); }); it('should be callable multiple times', async () => { const factoryName = 'dummyFactory'; const pluginId = 'test-plugin:settings'; const toolbarRegistry = new ToolbarWidgetRegistry({ defaultFactory: jest.fn() }); const bar: ISettingRegistry.IPlugin = { data: { composite: {}, user: {} }, id: pluginId, raw: '{}', schema: { 'jupyter.lab.toolbars': { dummyFactory: [ { name: 'insert', command: 'notebook:insert-cell-below', rank: 20 }, { name: 'spacer', type: 'spacer', rank: 100 }, { name: 'cut', command: 'notebook:cut-cell', rank: 21 }, { name: 'clear-all', command: 'notebook:clear-all-cell-outputs', rank: 60, disabled: true } ] }, 'jupyter.lab.transform': true, properties: { toolbar: { type: 'array' } }, type: 'object' }, version: 'test' }; const connector: IDataConnector< ISettingRegistry.IPlugin, string, string, string > = { fetch: jest.fn().mockImplementation((id: string) => { switch (id) { case bar.id: return bar; default: return {}; } }), list: jest.fn(), save: jest.fn(), remove: jest.fn() }; const settingRegistry = new SettingRegistry({ connector }); const translator: ITranslator = { load: jest.fn() }; const factory = createToolbarFactory( toolbarRegistry, settingRegistry, factoryName, pluginId, translator ); const factory2 = createToolbarFactory( toolbarRegistry, settingRegistry, factoryName, pluginId, translator ); await settingRegistry.load(bar.id); expect(factory(new Widget())).toHaveLength(3); expect(factory2(new Widget())).toHaveLength(3); }); }); });