import {
unsafeStatic,
fixtureSync,
nextFrame,
aTimeout,
defineCE,
fixture,
expect,
html,
} from '@open-wc/testing';
import { overlays as overlaysManager, OverlayController } from '@lion/ui/overlays.js';
import { sendKeys } from '@web/test-runner-commands';
import { browserDetection } from '@lion/ui/core.js';
import { cache } from 'lit/directives/cache.js';
import '@lion/ui/define/lion-dialog.js';
import { LitElement } from 'lit';
import sinon from 'sinon';
/**
* @typedef {import('../types/OverlayMixinTypes.js').DefineOverlayConfig} DefineOverlayConfig
* @typedef {LitElement & OverlayHost & {_overlayCtrl:OverlayController}} OverlayEl
* @typedef {import('../types/OverlayMixinTypes.js').OverlayMixin} OverlayMixin
* @typedef {import('../types/OverlayMixinTypes.js').OverlayHost} OverlayHost
* @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig
*/
function getGlobalOverlayCtrls() {
return overlaysManager.list;
}
function resetOverlaysManager() {
overlaysManager.list.forEach(overlayCtrl => overlaysManager.remove(overlayCtrl));
}
/**
* A small wrapper function that closely mimics an escape press from a user
* (prevents common mistakes like no bubbling or keydown)
* @param {HTMLElement|Document} element
*/
async function mimicEscapePress(element) {
// Make sure that the element inside the dialog is focusable (and cleanup after)
if (element instanceof HTMLElement) {
const { tabIndex: tabIndexBefore } = element;
// eslint-disable-next-line no-param-reassign
element.tabIndex = -1;
element.focus();
// eslint-disable-next-line no-param-reassign
element.tabIndex = tabIndexBefore; // make sure element is focusable
}
// Send the event
await sendKeys({ press: 'Escape' });
// Wait for at least a microtask, so that possible property effects are performed
await aTimeout(0);
}
/**
* @param {{tagString:string, tag: object, suffix?:string}} config
*/
export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
describe(`OverlayMixin${suffix}`, () => {
it('should not be opened by default', async () => {
const el = /** @type {OverlayEl} */ (
await fixture(html`
<${tag}>
content of the overlay
${tag}>
`)
);
expect(el.opened).to.be.false;
expect(el._overlayCtrl.isShown).to.be.false;
});
it('syncs opened to overlayController', async () => {
const el = /** @type {OverlayEl} */ (
await fixture(html`
<${tag}>
${tag}>
`)
);
expect(el.opened).to.be.false;
await el._overlayCtrl.show();
expect(el.opened).to.be.true;
await el._overlayCtrl.hide();
expect(el.opened).to.be.false;
});
// TODO: put this tests in OverlayController.test.js instead?
it('does not change the body size when opened', async () => {
const parentNode = document.createElement('div');
parentNode.setAttribute('style', 'height: 10000px; width: 10000px;');
const elWithBigParent = /** @type {OverlayEl} */ (
await fixture(
html`
<${tag}>
content of the overlay
${tag}>
`,
{ parentNode },
)
);
// For now, we skip this test for MacSafari, since the body.overlays-scroll-lock-ios-fix
// class results in a scrollbar when preventsScroll is true.
// However, fully functioning interacive elements (input fields) in the dialog are more important
if (browserDetection.isMacSafari && elWithBigParent._overlayCtrl.preventsScroll) {
return;
}
const elWithBigParentOffsetParent = /** @type {HTMLElement} */ (
elWithBigParent?.offsetParent
);
const { offsetWidth, offsetHeight } = elWithBigParentOffsetParent;
await elWithBigParent._overlayCtrl.show();
expect(elWithBigParent.opened).to.be.true;
expect(elWithBigParentOffsetParent.offsetWidth).to.equal(offsetWidth);
expect(elWithBigParentOffsetParent.offsetHeight).to.equal(offsetHeight);
await elWithBigParent._overlayCtrl.hide();
expect(elWithBigParentOffsetParent.offsetWidth).to.equal(offsetWidth);
expect(elWithBigParentOffsetParent.offsetHeight).to.equal(offsetHeight);
});
it('should respond to initially and dynamically setting the config', async () => {
const itEl = /** @type {OverlayEl} */ (
await fixture(html`
<${tag} .config=${{ trapsKeyboardFocus: false, viewportConfig: { placement: 'top' } }}>
${tag}>
`)
);
/** @type {HTMLElement} */ (el.querySelector('[slot="invoker"]')).click();
await nextFrame();
expect(el.opened).to.be.false;
// Also, the opened state should be synced back to that of the OverlayController
el.opened = true;
expect(el.opened).to.be.true;
await nextFrame();
expect(el.opened).to.be.false;
});
it('hides content on "close-overlay" event within the content ', async () => {
function sendCloseEvent(/** @type {Event} */ e) {
e.target?.dispatchEvent(new Event('close-overlay', { bubbles: true }));
}
const el = /** @type {OverlayEl} */ (
await fixture(html`
<${tag} opened>
${tag}>
`)
);
if (el._overlayCtrl.placementMode === 'global') {
// Find the outlets that are not backdrop outlets
expect(getGlobalOverlayCtrls().length).to.equal(2);
// Check that the last container is the nested one with the intended content
expect(el.contains(nestedEl)).to.be.true;
} else {
const contentNode = /** @type {HTMLElement} */ (
// @ts-ignore [allow-protected] in tests
el._overlayContentNode.querySelector('#nestedContent')
);
expect(contentNode).to.not.be.null;
expect(contentNode.innerText).to.equal('content of the nested overlay');
}
});
});
}