lion/packages/ui/components/dialog/test/lion-dialog.test.js

413 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* eslint-disable lit-a11y/no-autofocus */
import {
expect,
fixture as _fixture,
html,
unsafeStatic,
aTimeout,
defineCE,
waitUntil,
} from '@open-wc/testing';
import { cache } from 'lit/directives/cache.js';
import { LitElement, nothing } from 'lit';
import { runOverlayMixinSuite } from '../../overlays/test-suites/OverlayMixin.suite.js';
import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
import '../test-helpers/test-router.js';
import '@lion/ui/define/lion-dialog.js';
import '@lion/ui/define/lion-tabs.js';
/**
* @typedef {import('../src/LionDialog.js').LionDialog} LionDialog
* @typedef {import('lit').TemplateResult} TemplateResult
*/
const fixture = /** @type {(arg: TemplateResult) => Promise<LionDialog>} */ (_fixture);
describe('lion-dialog', () => {
// For some reason, globalRootNode is not cleared properly on disconnectedCallback from previous overlay test fixtures...
// Not sure why this "bug" happens...
beforeEach(() => {
const globalRootNode = document.querySelector('.overlays');
if (globalRootNode) {
globalRootNode.innerHTML = '';
}
});
describe('Integration tests', () => {
const tagString = 'lion-dialog';
const tag = unsafeStatic(tagString);
runOverlayMixinSuite({
tagString,
tag,
suffix: ' for lion-dialog',
});
});
describe('Basic', () => {
it('should show content on invoker click', async () => {
const el = await fixture(html`
<lion-dialog>
<div slot="content" class="dialog">Hey there</div>
<button slot="invoker">Popup button</button>
</lion-dialog>
`);
const invoker = /** @type {HTMLElement} */ (el.querySelector('[slot="invoker"]'));
invoker.click();
// @ts-expect-error [allow-protected-in-tests]
await el._overlayCtrl._showComplete;
expect(el.opened).to.be.true;
});
it('supports nested overlays', async () => {
const el = await fixture(html`
<lion-dialog>
<div slot="content">
open nested overlay:
<lion-dialog>
<div slot="content">Nested content</div>
<button slot="invoker">nested invoker button</button>
</lion-dialog>
</div>
<button slot="invoker">invoker button</button>
</lion-dialog>
`);
// @ts-expect-error [allow-protected-in-tests]
el._overlayInvokerNode.click();
// @ts-expect-error [allow-protected-in-tests]
await el._overlayCtrl._showComplete;
expect(el.opened).to.be.true;
const nestedDialogEl = /** @type {LionDialog} */ (el.querySelector('lion-dialog'));
// @ts-expect-error you're not allowed to call protected _overlayInvokerNode in public context, but for testing it's okay
nestedDialogEl?._overlayInvokerNode.click();
// @ts-expect-error [allow-protected-in-tests]
await nestedDialogEl._overlayCtrl._showComplete;
expect(nestedDialogEl.opened).to.be.true;
});
});
describe('focus', () => {
it('sets focus on contentSlot by default', async () => {
const el = await fixture(html`
<lion-dialog>
<button slot="invoker">invoker button</button>
<div slot="content">
<label for="myInput">Label</label>
<input id="myInput" />
</div>
</lion-dialog>
`);
// @ts-expect-error [allow-protected-in-tests]
const invokerNode = el._overlayInvokerNode;
invokerNode.focus();
invokerNode.click();
const contentNode = /** @type {Element} */ (el.querySelector('[slot="content"]'));
expect(isActiveElement(contentNode)).to.be.true;
});
it('sets focus on autofocused element', async () => {
const el = await fixture(html`
<lion-dialog>
<button slot="invoker">invoker button</button>
<div slot="content">
<label for="myInput">Label</label>
<input id="myInput" autofocus />
</div>
</lion-dialog>
`);
// @ts-expect-error [allow-protected-in-tests]
const invokerNode = el._overlayInvokerNode;
invokerNode.focus();
invokerNode.click();
const input = /** @type {Element} */ (el.querySelector('input'));
expect(isActiveElement(input)).to.be.true;
});
it('with trapsKeyboardFocus set to false the focus stays on the invoker', async () => {
const el = /** @type {LionDialog} */ await fixture(html`
<lion-dialog .config=${{ trapsKeyboardFocus: false }}>
<button slot="invoker">invoker button</button>
<div slot="content">
<label for="myInput">Label</label>
<input id="myInput" autofocus />
</div>
</lion-dialog>
`);
// @ts-expect-error [allow-protected-in-tests]
const invokerNode = el._overlayInvokerNode;
invokerNode.focus();
invokerNode.click();
expect(isActiveElement(invokerNode)).to.be.true;
});
it('opened-changed event should send detail object with opened state', async () => {
const el = /** @type {LionDialog} */ await fixture(html`
<lion-dialog .config=${{ trapsKeyboardFocus: false }}>
<button slot="invoker">invoker button</button>
<div slot="content">
<label for="myInput">Label</label>
<input id="myInput" autofocus />
</div>
</lion-dialog>
`);
el.setAttribute('opened', '');
expect(el.opened).to.be.true;
el.addEventListener('opened-changed', e => {
// @ts-expect-error [allow-detail-since-custom-event]
expect(e.detail.opened).to.be.false;
});
el.removeAttribute('opened');
});
it("opened-changed event's target should point to lion-dialog", async () => {
const el = /** @type {LionDialog} */ await fixture(html`
<lion-dialog .config=${{ trapsKeyboardFocus: false }}>
<button slot="invoker">invoker button</button>
<div slot="content">
<label for="myInput">Label</label>
<input id="myInput" autofocus />
</div>
</lion-dialog>
`);
el.addEventListener('opened-changed', e => {
expect(e.target).to.equal(el);
});
el.setAttribute('opened', '');
});
});
describe('Accessibility', () => {
it('passes a11y audit', async () => {
const el = await fixture(html`
<lion-dialog>
<button slot="invoker">Invoker</button>
<div slot="content" class="dialog" aria-label="Dialog">Hey there</div>
</lion-dialog>
`);
await expect(el).to.be.accessible();
});
it('passes a11y audit when opened', async () => {
const el = await fixture(html`
<lion-dialog opened>
<button slot="invoker">Invoker</button>
<div slot="content" class="dialog" aria-label="Dialog">Hey there</div>
</lion-dialog>
`);
// error expected since we put role="none" on the dialog itself, which is valid but not recognized by Axe
await expect(el).to.be.accessible({ ignoredRules: ['aria-allowed-role'] });
});
it('does not add [aria-expanded] to invoker button', async () => {
const el = await fixture(
html` <lion-dialog>
<div slot="content" class="dialog">Hey there</div>
<button slot="invoker">Popup button</button>
</lion-dialog>`,
);
const invokerButton = /** @type {HTMLElement} */ (el.querySelector('[slot="invoker"]'));
expect(invokerButton.getAttribute('aria-expanded')).to.equal(null);
await invokerButton.click();
await aTimeout(0);
expect(invokerButton.getAttribute('aria-expanded')).to.equal(null);
await invokerButton.click();
await aTimeout(0);
expect(invokerButton.getAttribute('aria-expanded')).to.equal(null);
});
it('has role="dialog" by default', async () => {
const el = await fixture(
html` <lion-dialog>
<div slot="content" class="dialog">Hey there</div>
<button slot="invoker">Popup button</button>
</lion-dialog>`,
);
const contentNode = /** @type {HTMLElement} */ (el.querySelector('[slot="content"]'));
expect(contentNode.getAttribute('role')).to.equal('dialog');
});
it('has role="alertdialog" by when "is-alert-dialog" is set', async () => {
const el = await fixture(
html` <lion-dialog is-alert-dialog>
<div slot="content" class="dialog">Hey there</div>
<button slot="invoker">Popup button</button>
</lion-dialog>`,
);
const contentNode = /** @type {HTMLElement} */ (el.querySelector('[slot="content"]'));
expect(contentNode.getAttribute('role')).to.equal('alertdialog');
});
it('passes a11y audit when opened and role="alertdialog"', async () => {
const el = await fixture(html`
<lion-dialog opened is-alert-dialog>
<button slot="invoker">Invoker</button>
<div slot="content" class="dialog" aria-label="Dialog">Hey there</div>
</lion-dialog>
`);
// error expected since we put role="none" on the dialog itself, which is valid but not recognized by Axe
await expect(el).to.be.accessible({ ignoredRules: ['aria-allowed-role'] });
});
});
describe('Edge cases', () => {
it('does not lose click event handler when was detached and reattached', async () => {
const el = await fixture(html`
<test-router
.routingMap="${{
a: html`
<lion-dialog>
<div slot="content" class="dialog">Hey there</div>
<button slot="invoker">Popup button</button>
</lion-dialog>
`,
b: html` <div>B</div> `,
}}"
path="a"
></test-router>
`);
const getDialog = () =>
/** @type {LionDialog} */ (el.shadowRoot?.querySelector('lion-dialog'));
const getInvoker = () =>
/** @type {HTMLElement} */ (getDialog().querySelector('[slot="invoker"]'));
getInvoker().click();
const dialog = getDialog();
// @ts-expect-error [allow-protected-in-tests]
await dialog._overlayCtrl._showComplete;
expect(dialog.opened).to.be.true;
dialog.close();
// @ts-expect-error [allow-protected-in-tests]
await dialog._overlayCtrl._hideComplete;
expect(dialog.opened).to.be.false;
const buttonA = /** @type {HTMLElement} */ (el.shadowRoot?.querySelector('#path-a'));
const buttonB = /** @type {HTMLElement} */ (el.shadowRoot?.querySelector('#path-b'));
buttonB.click();
await el.updateComplete;
buttonA.click();
await el.updateComplete;
await el.updateComplete;
getInvoker().click();
const dialogAfterRouteChange = getDialog();
// @ts-expect-error [allow-protected-in-tests]
expect(dialogAfterRouteChange._overlayCtrl).not.to.be.undefined;
// @ts-expect-error [allow-protected-in-tests]
await dialogAfterRouteChange._overlayCtrl._showComplete;
expect(dialogAfterRouteChange.opened).to.be.true;
});
it('should close the popup dialog after rendered from cache', async () => {
/**
*
* @param {Event} e
* @returns
*/
const closeButtonHandler = e =>
e.target?.dispatchEvent(new Event('close-overlay', { bubbles: true }));
const dialog = html` <lion-dialog>
<button slot="invoker" class="invoker-button">Click me to open dialog</button>
<div slot="content" class="demo-dialog-content">
Hello! You can close this dialog here:
<button class="close-button" @click="${closeButtonHandler}"></button>
</div>
</lion-dialog>`;
/**
* Note, inactive tab content is **destroyed** on every tab switch.
*/
class Wrapper extends LitElement {
static properties = {
...super.properties,
activeTabIndex: { type: Number },
};
constructor() {
super();
this.activeTabIndex = 0;
}
/**
* @param {number} index
*/
changeActiveTabIndex(index) {
this.activeTabIndex = index;
}
render() {
const changeActiveTabIndexRef = this.changeActiveTabIndex.bind(this);
return html`
<lion-tabs>
<button slot="tab" class="first-button" @click=${() => changeActiveTabIndexRef(0)}>
First
</button>
<p slot="panel">${cache(this.activeTabIndex === 0 ? dialog : nothing)}</p>
<button slot="tab" class="second-button" @click=${() => changeActiveTabIndexRef(1)}>
Second
</button>
<p slot="panel">Info page with lots of information about us.</p>
</lion-tabs>
`;
}
}
const wrapperFixture = /** @type {(arg: TemplateResult) => Promise<Wrapper>} */ (_fixture);
const tagString = defineCE(Wrapper);
const wrapperTag = unsafeStatic(tagString);
const wrapperElement = /** @type {Wrapper} */ (
await wrapperFixture(html`<${wrapperTag}></${wrapperTag}>`)
);
await wrapperElement.updateComplete;
const wrapperElementShadowRoot = wrapperElement.shadowRoot;
/**
* @returns { HTMLElement | null | undefined }
*/
const getFirstButton = () => wrapperElementShadowRoot?.querySelector('.first-button');
/**
* @returns { HTMLElement | null | undefined }
*/
const getSecondButton = () => wrapperElementShadowRoot?.querySelector('.second-button');
/**
* @returns { HTMLElement | null | undefined }
*/
const getInvokerButton = () => wrapperElementShadowRoot?.querySelector('.invoker-button');
/**
* @returns { HTMLElement | null | undefined }
*/
const getCloseButton = () => wrapperElementShadowRoot?.querySelector('.close-button');
/**
* @returns { Element | null | undefined }
*/
const getDialog = () =>
wrapperElementShadowRoot?.querySelector('lion-dialog')?.shadowRoot?.querySelector('dialog');
// @ts-ignore
const isDialogVisible = () => getDialog()?.checkVisibility() === true;
const isDialogRendered = () =>
!!wrapperElement.shadowRoot?.querySelector('lion-dialog')?.shadowRoot?.childNodes.length;
getInvokerButton()?.click();
await waitUntil(isDialogVisible);
getCloseButton()?.click();
await waitUntil(() => !isDialogVisible());
getSecondButton()?.click();
await waitUntil(() => !isDialogRendered());
getFirstButton()?.click();
await waitUntil(isDialogRendered);
getInvokerButton()?.click();
await waitUntil(isDialogVisible);
getCloseButton()?.click();
await waitUntil(() => !isDialogVisible());
expect(isDialogVisible()).to.equal(false);
});
});
});