import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing'; import sinon from 'sinon'; import { LitElement } from '../index.js'; import { DelegateMixin } from '../src/DelegateMixin.js'; describe('DelegateMixin', () => { afterEach(() => { sinon.restore(); }); it('delegates events', async () => { const tag = defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, target: () => this.shadowRoot.getElementById('button1'), events: ['click'], }; } render() { return html``; } }, ); const element = await fixture(`<${tag}>`); const cb = sinon.spy(); element.addEventListener('click', cb); element.shadowRoot.getElementById('button1').click(); expect(cb.callCount).to.equal(1); }); it('delegates events before delegation target is attached to DOM', async () => { const tag = defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, target: () => this.shadowRoot.getElementById('button1'), events: ['click'], }; } render() { return html``; } }, ); const element = document.createElement(tag); const cb = sinon.spy(); element.addEventListener('click', cb); document.body.appendChild(element); await element.updateComplete; element.shadowRoot.getElementById('button1').click(); expect(cb.callCount).to.equal(1); // cleanup document.body.removeChild(element); }); it('delegates if light and shadow dom is used at the same time', async () => { const tag = defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, target: () => Array.from(this.children).find(child => child.slot === 'button'), events: ['click'], methods: ['click'], }; } render() { return html` Outside `; } }, ); const element = await fixture(` <${tag}>`); const cb = sinon.spy(); element.addEventListener('click', cb); Array.from(element.children) .find(child => child.slot === 'button') .click(); expect(cb.callCount).to.equal(1); }); it('will still support other events', async () => { const tag = defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, target: () => this.shadowRoot.getElementById('button1'), events: ['click'], }; } render() { return html``; } foo() { this.dispatchEvent(new CustomEvent('foo-event', { bubbles: true, composed: true })); } }, ); const element = await fixture(`<${tag}>`); const cb = sinon.spy(); element.addEventListener('foo-event', cb); element.foo(); expect(cb.callCount).to.equal(1); }); it('will call delegated methods', async () => { const tag = defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, target: () => this.shadowRoot.getElementById('button1'), methods: ['click'], }; } render() { return html``; } }, ); const element = await fixture(`<${tag}>`); const cb = sinon.spy(); element.shadowRoot.getElementById('button1').addEventListener('click', cb); element.click(); expect(cb.callCount).to.equal(1); }); it('supports arguments for delegated methods', async () => { class DelegateArgumentSub extends LitElement { constructor() { super(); this.foo = { a: 'a', b: 'b' }; } setFooAandB(a, b) { this.foo.a = a; this.foo.b = b; } } customElements.define('delegate-argument-sub', DelegateArgumentSub); const tag = defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, target: () => this.shadowRoot.getElementById('sub'), methods: ['setFooAandB'], }; } render() { return html``; } }, ); const element = await fixture(`<${tag}>`); element.disabled = true; element.setFooAandB('newA', 'newB'); expect(element.shadowRoot.getElementById('sub').foo.a).to.equal('newA'); expect(element.shadowRoot.getElementById('sub').foo.b).to.equal('newB'); }); it('will set delegated properties', async () => { const tag = defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, target: () => this.shadowRoot.getElementById('button1'), properties: ['disabled'], }; } render() { return html``; } }, ); const element = await fixture(`<${tag}>`); element.disabled = true; await element.updateComplete; expect(element.shadowRoot.getElementById('button1').disabled).to.equal(true); expect(element.shadowRoot.getElementById('button1').hasAttribute('disabled')).to.equal(true); }); it('delegates properties before delegation target is attached to DOM', async () => { const tag = defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, target: () => this.shadowRoot.getElementById('button1'), properties: ['disabled'], }; } render() { return html``; } }, ); const element = document.createElement(tag); element.disabled = true; document.body.appendChild(element); await element.updateComplete; expect(element.shadowRoot.getElementById('button1').disabled).to.equal(true); // cleanup document.body.removeChild(element); }); it('will delegate setAttribute', async () => { const tag = defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, target: () => this.shadowRoot.getElementById('button1'), attributes: ['disabled'], }; } render() { return html``; } }, ); const element = await fixture(`<${tag}>`); element.setAttribute('disabled', ''); await element.updateComplete; expect(element.hasAttribute('disabled')).to.equal(false); expect(element.shadowRoot.getElementById('button1').hasAttribute('disabled')).to.equal(true); }); it('will read inital attributes', async () => { const tag = defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, target: () => this.shadowRoot.getElementById('button1'), attributes: ['disabled'], }; } render() { return html``; } }, ); const element = await fixture(`<${tag} disabled>`); expect(element.hasAttribute('disabled')).to.equal(false); expect(element.shadowRoot.getElementById('button1').hasAttribute('disabled')).to.equal(true); }); it('will delegate removeAttribute', async () => { const tag = defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, target: () => this.shadowRoot.getElementById('button1'), attributes: ['disabled'], }; } render() { return html``; } }, ); const element = await fixture(`<${tag} disabled>`); element.removeAttribute('disabled', ''); await element.updateComplete; expect(element.hasAttribute('disabled')).to.equal(false); expect(element.shadowRoot.getElementById('button1').hasAttribute('disabled')).to.equal(false); }); it('respects user defined values for delegated attributes and properties', async () => { const tag = defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, // this just means itÅ› config is set to the queue when called before connectedCallback target: () => this.scheduledElement, attributes: ['type'], properties: ['type'], }; } get scheduledElement() { return this.querySelector('input'); } constructor() { super(); this.type = 'email'; // 1. here we set the delegated prop and it should be scheduled } connectedCallback() { // 2. this is where we add teh delegation target (so after 1) this.appendChild(document.createElement('input')); super.connectedCallback(); // let the DelegateMixin do its work } }, ); const tagName = unsafeStatic(tag); // Here, the Application Developerd tries to set the type via attribute const elementAttr = await fixture(`<${tag} type="radio">`); expect(elementAttr.scheduledElement.type).to.equal('radio'); // Here, the Application Developer tries to set the type via property const elementProp = await fixture(html`<${tagName} .type=${'radio'}>`); expect(elementProp.scheduledElement.type).to.equal('radio'); }); it(`uses attribute value as a fallback for delegated property getter when property not set by user and delegationTarget not ready`, async () => { const tag = defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, target: () => this.delegatedEl, properties: ['type'], attributes: ['type'], }; } get delegatedEl() { // returns null, so we can test that "cached" attr is used as fallback return null; } }, ); const element = await fixture(`<${tag} type="radio">`); expect(element.delegatedEl).to.equal(null); expect(element.type).to.equal('radio'); // value retrieved from host instead of delegatedTarget }); it('works with connectedCallback', async () => { const tag = await defineCE( class extends DelegateMixin(HTMLElement) { get delegations() { return { ...super.delegations, target: () => this.querySelector('div'), properties: ['foo'], }; } }, ); const element = await fixture(`<${tag}>
`); element.foo = 'new'; expect(element.querySelector('div').foo).to.equal('new'); }); it('works with shadow dom', async () => { const tag = await defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, target: () => this.shadowRoot.querySelector('div'), properties: ['foo'], }; } render() { return html`
`; } }, ); const element = await fixture(`<${tag}>`); element.foo = 'new'; expect(element.shadowRoot.querySelector('div').foo).to.equal('new'); }); it('works with light dom', async () => { const tag = await defineCE( class extends DelegateMixin(LitElement) { get delegations() { return { ...super.delegations, target: () => this.querySelector('div'), properties: ['foo'], }; } createRenderRoot() { return this; } render() { return html`
`; } }, ); const element = await fixture(`<${tag}>`); element.foo = 'new'; expect(element.querySelector('div').foo).to.equal('new'); }); });