123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import { Dialog, showDialog } from '@jupyterlab/apputils';
- import {
- acceptDialog,
- dismissDialog,
- waitForDialog
- } from '@jupyterlab/testutils';
- import { each } from '@lumino/algorithm';
- import { Message } from '@lumino/messaging';
- import { Widget } from '@lumino/widgets';
- import * as React from 'react';
- import { generate, simulate } from 'simulate-event';
- class TestDialog extends Dialog<any> {
- methods: string[] = [];
- events: string[] = [];
- handleEvent(event: Event): void {
- super.handleEvent(event);
- this.events.push(event.type);
- }
- protected onAfterAttach(msg: Message): void {
- super.onAfterAttach(msg);
- this.methods.push('onAfterAttach');
- }
- protected onAfterDetach(msg: Message): void {
- super.onAfterDetach(msg);
- this.methods.push('onAfterDetach');
- }
- protected onCloseRequest(msg: Message): void {
- super.onCloseRequest(msg);
- this.methods.push('onCloseRequest');
- }
- }
- class ValueWidget extends Widget {
- getValue(): string {
- return 'foo';
- }
- }
- describe('@jupyterlab/apputils', () => {
- describe('Dialog', () => {
- let dialog: TestDialog;
- beforeEach(() => {
- dialog = new TestDialog();
- });
- afterEach(() => {
- dialog.dispose();
- });
- describe('#constructor()', () => {
- it('should create a new dialog', () => {
- expect(dialog).toBeInstanceOf(Dialog);
- });
- it('should accept options', () => {
- const dialog = new TestDialog({
- title: 'foo',
- body: 'Hello',
- buttons: [Dialog.okButton()],
- hasClose: false
- });
- expect(dialog).toBeInstanceOf(Dialog);
- dialog.dispose();
- });
- });
- describe('#launch()', () => {
- it.skip('should attach the dialog to the host', async () => {
- const host = document.createElement('div');
- const dialog = new TestDialog({ host });
- document.body.appendChild(host);
- void dialog.launch();
- await waitForDialog(host);
- expect(host.contains(dialog.node)).toBe(true);
- dialog.dispose();
- document.body.removeChild(host);
- });
- it('should resolve with `true` when accepted', async () => {
- const prompt = dialog.launch();
- await waitForDialog();
- dialog.resolve();
- expect((await prompt).button.accept).toBe(true);
- });
- it('should resolve with `false` when rejected', async () => {
- const prompt = dialog.launch();
- await waitForDialog();
- dialog.reject();
- expect((await prompt).button.accept).toBe(false);
- });
- it('should resolve with `false` when closed', async () => {
- const prompt = dialog.launch();
- await waitForDialog();
- dialog.close();
- expect((await prompt).button.accept).toBe(false);
- });
- it('should return focus to the original focused element', async () => {
- const input = document.createElement('input');
- document.body.appendChild(input);
- input.focus();
- expect(document.activeElement).toBe(input);
- const prompt = dialog.launch();
- await waitForDialog();
- expect(document.activeElement).not.toBe(input);
- dialog.resolve();
- await prompt;
- expect(document.activeElement).toBe(input);
- document.body.removeChild(input);
- });
- });
- describe('#resolve()', () => {
- it('should resolve with the default item', async () => {
- const prompt = dialog.launch();
- await waitForDialog();
- dialog.resolve();
- expect((await prompt).button.accept).toBe(true);
- });
- it('should resolve with the item at the given index', async () => {
- const prompt = dialog.launch();
- await waitForDialog();
- dialog.resolve(0);
- expect((await prompt).button.accept).toBe(false);
- });
- });
- describe('#reject()', () => {
- it('should reject with the default reject item', async () => {
- const prompt = dialog.launch();
- await waitForDialog();
- dialog.reject();
- const result = await prompt;
- expect(result.button.label).toBe('Cancel');
- expect(result.button.accept).toBe(false);
- });
- });
- describe('#handleEvent()', () => {
- describe('keydown', () => {
- it('should reject on escape key', async () => {
- const prompt = dialog.launch();
- await waitForDialog();
- simulate(dialog.node, 'keydown', { keyCode: 27 });
- expect((await prompt).button.accept).toBe(false);
- });
- it('should accept on enter key', async () => {
- const prompt = dialog.launch();
- await waitForDialog();
- simulate(dialog.node, 'keydown', { keyCode: 13 });
- expect((await prompt).button.accept).toBe(true);
- });
- it('should resolve with currently focused button', async () => {
- const dialog = new TestDialog({
- buttons: [
- Dialog.createButton({ label: 'first' }),
- Dialog.createButton({ label: 'second' }),
- Dialog.createButton({ label: 'third' }),
- Dialog.createButton({ label: 'fourth' })
- ],
- // focus on "first"
- defaultButton: 0
- });
- const prompt = dialog.launch();
- await waitForDialog();
- // press right arrow twice (focusing on "third")
- simulate(dialog.node, 'keydown', { keyCode: 39 });
- simulate(dialog.node, 'keydown', { keyCode: 39 });
- // press enter
- simulate(dialog.node, 'keydown', { keyCode: 13 });
- expect((await prompt).button.label).toBe('third');
- dialog.dispose();
- });
- it('should cycle to the first button on a tab key', async () => {
- const prompt = dialog.launch();
- await waitForDialog();
- expect(document.activeElement!.className).toContain('jp-mod-accept');
- simulate(dialog.node, 'keydown', { keyCode: 9 });
- expect(document.activeElement!.className).toContain('jp-mod-reject');
- simulate(document.activeElement!, 'click');
- expect((await prompt).button.accept).toBe(false);
- });
- });
- describe('contextmenu', () => {
- it('should cancel context menu events', async () => {
- const prompt = dialog.launch();
- await waitForDialog();
- const canceled = !dialog.node.dispatchEvent(generate('contextmenu'));
- expect(canceled).toBe(true);
- simulate(dialog.node, 'keydown', { keyCode: 27 });
- expect((await prompt).button.accept).toBe(false);
- });
- });
- describe('click', () => {
- it('should prevent clicking outside of the content area', async () => {
- const prompt = dialog.launch();
- await waitForDialog();
- const canceled = !dialog.node.dispatchEvent(generate('click'));
- expect(canceled).toBe(true);
- dialog.resolve();
- await prompt;
- });
- it('should resolve a clicked button', async () => {
- const prompt = dialog.launch();
- await waitForDialog();
- simulate(dialog.node.querySelector('.jp-mod-reject')!, 'click');
- expect((await prompt).button.accept).toBe(false);
- });
- });
- describe('focus', () => {
- it('should focus the default button when focus leaves the dialog', async () => {
- const host = document.createElement('div');
- const target = document.createElement('div');
- target.tabIndex = 0; // Make the div element focusable
- const dialog = new TestDialog({ host });
- document.body.appendChild(target);
- document.body.appendChild(host);
- target.focus();
- expect(document.activeElement).toBe(target);
- const prompt = dialog.launch();
- await waitForDialog();
- simulate(target, 'focus');
- expect(document.activeElement).not.toBe(target);
- expect(document.activeElement!.className).toContain('jp-mod-accept');
- dialog.resolve();
- await prompt;
- dialog.dispose();
- });
- });
- });
- describe('#onAfterAttach()', () => {
- it('should attach event listeners', () => {
- Widget.attach(dialog, document.body);
- expect(dialog.methods).toContain('onAfterAttach');
- ['keydown', 'contextmenu', 'click', 'focus'].forEach(event => {
- simulate(dialog.node, event);
- expect(dialog.events).toContain(event);
- });
- });
- it('should focus the default button', () => {
- Widget.attach(dialog, document.body);
- expect(document.activeElement!.className).toContain('jp-mod-accept');
- });
- it('should focus the primary element', () => {
- const body = (
- <div>
- <input type={'text'} />
- </div>
- );
- const dialog = new TestDialog({ body, focusNodeSelector: 'input' });
- Widget.attach(dialog, document.body);
- expect(document.activeElement!.localName).toBe('input');
- dialog.dispose();
- });
- });
- describe('#onAfterDetach()', () => {
- it('should remove event listeners', () => {
- Widget.attach(dialog, document.body);
- Widget.detach(dialog);
- expect(dialog.methods).toContain('onAfterDetach');
- dialog.events = [];
- ['keydown', 'contextmenu', 'click', 'focus'].forEach(event => {
- simulate(dialog.node, event);
- expect(dialog.events).not.toContain(event);
- });
- });
- it('should return focus to the original focused element', () => {
- const input = document.createElement('input');
- document.body.appendChild(input);
- input.focus();
- Widget.attach(dialog, document.body);
- Widget.detach(dialog);
- expect(document.activeElement).toBe(input);
- document.body.removeChild(input);
- });
- });
- describe('#onCloseRequest()', () => {
- it('should reject an existing promise', async () => {
- const prompt = dialog.launch();
- await waitForDialog();
- dialog.close();
- expect((await prompt).button.accept).toBe(false);
- });
- });
- describe('.defaultRenderer', () => {
- it('should be an instance of a Renderer', () => {
- expect(Dialog.defaultRenderer).toBeInstanceOf(Dialog.Renderer);
- });
- });
- describe('.Renderer', () => {
- const renderer = Dialog.defaultRenderer;
- const data: Dialog.IButton = {
- label: 'foo',
- iconClass: 'bar',
- iconLabel: 'foo',
- caption: 'hello',
- className: 'baz',
- accept: false,
- displayType: 'warn',
- actions: []
- };
- describe('#createHeader()', () => {
- it('should create the header of the dialog', () => {
- const widget = renderer.createHeader('foo');
- expect(widget.hasClass('jp-Dialog-header')).toBe(true);
- });
- });
- describe('#createBody()', () => {
- it('should create the body from a string', () => {
- const widget = renderer.createBody('foo');
- expect(widget.hasClass('jp-Dialog-body')).toBe(true);
- expect(widget.node.firstChild!.textContent).toBe('foo');
- });
- it('should create the body from a virtual node', () => {
- const vnode = (
- <div>
- <input type={'text'} />
- <select>
- <option value={'foo'}>foo</option>
- </select>
- <button />
- </div>
- );
- const widget = renderer.createBody(vnode);
- const button = widget.node.querySelector('button')!;
- const input = widget.node.querySelector('input')!;
- const select = widget.node.querySelector('select')!;
- Widget.attach(widget, document.body);
- expect(button.className).toContain('jp-mod-styled');
- expect(input.className).toContain('jp-mod-styled');
- expect(select.className).toContain('jp-mod-styled');
- widget.dispose();
- });
- it('should create the body from a widget', () => {
- const body = new Widget();
- renderer.createBody(body);
- expect(body.hasClass('jp-Dialog-body')).toBe(true);
- });
- });
- describe('#createFooter()', () => {
- it('should create the footer of the dialog', () => {
- const buttons = [Dialog.okButton, { label: 'foo' }];
- const nodes = buttons.map((button: Dialog.IButton) => {
- return renderer.createButtonNode(button);
- });
- const footer = renderer.createFooter(nodes);
- const buttonNodes = footer.node.querySelectorAll('button');
- expect(footer.hasClass('jp-Dialog-footer')).toBe(true);
- expect(footer.node.contains(nodes[0])).toBe(true);
- expect(footer.node.contains(nodes[1])).toBe(true);
- expect(buttonNodes.length).toBeGreaterThan(0);
- each(buttonNodes, (node: Element) => {
- expect(node.className).toContain('jp-mod-styled');
- });
- });
- });
- describe('#createButtonNode()', () => {
- it('should create a button node for the dialog', () => {
- const node = renderer.createButtonNode(data);
- expect(node.className).toContain('jp-Dialog-button');
- expect(node.querySelector('.jp-Dialog-buttonIcon')).toBeTruthy();
- expect(node.querySelector('.jp-Dialog-buttonLabel')).toBeTruthy();
- });
- });
- describe('#renderIcon()', () => {
- it('should render an icon element for a dialog item', () => {
- const node = renderer.renderIcon(data);
- expect(node.className).toContain('jp-Dialog-buttonIcon');
- expect(node.textContent).toBe('foo');
- });
- });
- describe('#createItemClass()', () => {
- it('should create the class name for the button', () => {
- const value = renderer.createItemClass(data);
- expect(value).toContain('jp-Dialog-button');
- expect(value).toContain('jp-mod-reject');
- expect(value).toContain(data.className);
- });
- });
- describe('#createIconClass()', () => {
- it('should create the class name for the button icon', () => {
- const value = renderer.createIconClass(data);
- expect(value).toContain('jp-Dialog-buttonIcon');
- expect(value).toContain(data.iconClass);
- });
- });
- describe('#renderLabel()', () => {
- it('should render a label element for a button', () => {
- const node = renderer.renderLabel(data);
- expect(node.className).toBe('jp-Dialog-buttonLabel');
- expect(node.title).toBe(data.caption);
- expect(node.textContent).toBe(data.label);
- });
- });
- });
- });
- describe('showDialog()', () => {
- it('should accept zero arguments', async () => {
- const dialog = showDialog();
- await dismissDialog();
- expect((await dialog).button.accept).toBe(false);
- });
- it('should accept dialog options', async () => {
- const node = document.createElement('div');
- document.body.appendChild(node);
- const prompt = showDialog({
- title: 'foo',
- body: 'Hello',
- host: node,
- defaultButton: 0,
- buttons: [Dialog.cancelButton(), Dialog.okButton()]
- });
- await acceptDialog();
- const result = await prompt;
- expect(result.button.accept).toBe(false);
- expect(result.value).toBe(null);
- document.body.removeChild(node);
- });
- it('should accept a virtualdom body', async () => {
- const body = (
- <div>
- <input />
- <select />
- </div>
- );
- const prompt = showDialog({ body });
- await acceptDialog();
- const result = await prompt;
- expect(result.button.accept).toBe(true);
- expect(result.value).toBe(null);
- });
- it('should accept a widget body', async () => {
- const prompt = showDialog({ body: new Widget() });
- await acceptDialog();
- const result = await prompt;
- expect(result.button.accept).toBe(true);
- expect(result.value).toBe(null);
- });
- it('should give the value from the widget', async () => {
- const prompt = showDialog({ body: new ValueWidget() });
- await acceptDialog();
- const result = await prompt;
- expect(result.button.accept).toBe(true);
- expect(result.value).toBe('foo');
- });
- it('should not create a close button by default', async () => {
- const node = document.createElement('div');
- document.body.appendChild(node);
- const prompt = showDialog({
- title: 'foo',
- body: 'Hello',
- host: node,
- defaultButton: 0,
- buttons: [Dialog.cancelButton(), Dialog.okButton()]
- });
- await waitForDialog();
- expect(node.querySelector('.jp-Dialog-close-button')).toBeFalsy();
- await acceptDialog();
- const result = await prompt;
- expect(result.button.accept).toBe(false);
- expect(result.button.actions).toEqual([]);
- expect(result.value).toBe(null);
- document.body.removeChild(node);
- });
- it('should create a close button', async () => {
- const node = document.createElement('div');
- document.body.appendChild(node);
- const prompt = showDialog({
- title: 'foo',
- body: 'Hello',
- host: node,
- defaultButton: 0,
- buttons: [Dialog.cancelButton(), Dialog.okButton()],
- hasClose: true
- });
- await waitForDialog();
- expect(node.querySelector('.jp-Dialog-close-button')).toBeTruthy();
- await acceptDialog();
- const result = await prompt;
- expect(result.button.accept).toBe(false);
- expect(result.button.actions).toEqual([]);
- expect(result.value).toBe(null);
- document.body.removeChild(node);
- });
- it('should accept the dialog reload options', async () => {
- const node = document.createElement('div');
- document.body.appendChild(node);
- const prompt = showDialog({
- title: 'foo',
- body: 'Hello',
- host: node,
- defaultButton: 0,
- buttons: [
- Dialog.cancelButton({ actions: ['reload'] }),
- Dialog.okButton()
- ],
- hasClose: true
- });
- await acceptDialog();
- const result = await prompt;
- expect(result.button.accept).toBe(false);
- expect(result.button.actions).toEqual(['reload']);
- expect(result.value).toBe(null);
- document.body.removeChild(node);
- });
- });
- });
|