import { expect, fixture } from '@open-wc/testing';
import {
ssrNonHydratedFixture,
ssrHydratedFixture,
csrFixture,
} from '@lit-labs/testing/fixtures.js';
import { LitElement, html } from 'lit';
import sinon from 'sinon';
import { ScopedElementsMixin, supportsScopedRegistry } from '../src/ScopedElementsMixin.js';
import { browserDetection } from '../src/browserDetection.js';
const hasRealScopedRegistrySupport = supportsScopedRegistry();
const originalShadowRootProps = {
// @ts-expect-error
createElement: globalThis.ShadowRoot?.prototype.createElement,
// @ts-expect-error
importNode: globalThis.ShadowRoot?.prototype.importNode,
};
// Even though the polyfill might be loaded in this test or we run it in a browser supporting these features,
// we mock "no support", so that `supportsScopedRegistry()` returns false inside ScopedElementsMixin..
function mockNoRegistrySupport() {
// Are we on a server or do we have no polyfill? Nothing to be done here...
if (!hasRealScopedRegistrySupport) return;
// This will be enough to make the `supportsScopedRegistry()` check fail inside ScopedElementsMixin and bypass scoped registries
globalThis.ShadowRoot = globalThis.ShadowRoot || { prototype: {} };
// @ts-expect-error
globalThis.ShadowRoot.prototype.createElement = null;
}
mockNoRegistrySupport.restore = () => {
// Are we on a server or do we have no polyfill? Nothing to be done here...
if (!hasRealScopedRegistrySupport) return;
// @ts-expect-error
globalThis.ShadowRoot.prototype.createElement = originalShadowRootProps.createElement;
// @ts-expect-error
globalThis.ShadowRoot.prototype.importNode = originalShadowRootProps.importNode;
};
class ScopedElementsChild extends LitElement {
render() {
return html`I'm a child`;
}
}
class ScopedElementsHost extends ScopedElementsMixin(LitElement) {
static scopedElements = { 'scoped-elements-child': ScopedElementsChild };
render() {
return html``;
}
}
customElements.define('scoped-elements-host', ScopedElementsHost);
describe('ScopedElementsMixin', () => {
it('renders child elements correctly (that were not registered yet on global registry)', async () => {
// customElements.define('scoped-elements-child', ScopedElementsChild);
for (const _fixture of [csrFixture, ssrNonHydratedFixture, ssrHydratedFixture]) {
const el = await _fixture(html``, {
// we must provide modules atm
modules: ['./ssr-definitions/ScopedElementsHost.define.js'],
});
// Wait for FF support
if (!browserDetection.isFirefox) {
expect(
el.shadowRoot?.querySelector('scoped-elements-child')?.shadowRoot?.innerHTML,
).to.contain("I'm a child");
}
// @ts-expect-error
expect(el.registry.get('scoped-elements-child')).to.not.be.undefined;
}
});
describe('When scoped registries are supported', () => {
it('registers elements on local registry', async () => {
if (!hasRealScopedRegistrySupport) return;
const ceDefineSpy = sinon.spy(customElements, 'define');
const el = /** @type {ScopedElementsHost} */ (
await fixture(html``)
);
// @ts-expect-error
expect(el.registry.get('scoped-elements-child')).to.equal(ScopedElementsChild);
expect(el.registry).to.not.equal(customElements);
expect(ceDefineSpy.calledWith('scoped-elements-child')).to.be.false;
ceDefineSpy.restore();
});
});
describe('When scoped registries are not supported', () => {
class ScopedElementsChildNoReg extends LitElement {
render() {
return html`I'm a child`;
}
}
class ScopedElementsHostNoReg extends ScopedElementsMixin(LitElement) {
static scopedElements = { 'scoped-elements-child-no-reg': ScopedElementsChildNoReg };
render() {
return html``;
}
}
before(() => {
mockNoRegistrySupport();
customElements.define('scoped-elements-host-no-reg', ScopedElementsHostNoReg);
});
after(() => {
mockNoRegistrySupport.restore();
});
it('registers elements', async () => {
const ceDefineSpy = sinon.spy(customElements, 'define');
const el = /** @type {ScopedElementsHostNoReg} */ (
await fixture(html``)
);
expect(el.registry).to.equal(customElements);
expect(ceDefineSpy.calledWith('scoped-elements-child-no-reg')).to.be.true;
ceDefineSpy.restore();
});
it('fails when different classes are registered under different name', async () => {
class ScopedElementsHostNoReg2 extends ScopedElementsMixin(LitElement) {
static scopedElements = { 'scoped-elements-child-no-reg': class extends HTMLElement {} };
render() {
return html``;
}
}
customElements.define('scoped-elements-host-no-reg-2', ScopedElementsHostNoReg2);
const errorStub = sinon.stub(console, 'error');
/** @type {ScopedElementsHostNoReg2} */ (
await fixture(html``)
);
/** @type {ScopedElementsHostNoReg2} */ (
await fixture(html``)
);
expect(errorStub.args[0][0]).to.equal(
[
'You are trying to re-register the "scoped-elements-child-no-reg" custom element with a different class via ScopedElementsMixin.',
'This is only possible with a CustomElementRegistry.',
'Your browser does not support this feature so you will need to load a polyfill for it.',
'Load "@webcomponents/scoped-custom-element-registry" before you register ANY web component to the global customElements registry.',
'e.g. add "" as your first script tag.',
'For more details you can visit https://open-wc.org/docs/development/scoped-elements/',
].join('\n'),
);
errorStub.restore();
});
});
});