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(); }); }); });