lion/packages/core/test/DelegateMixin.test.js

449 lines
15 KiB
JavaScript

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`<button id="button1">with delegation</button>`;
}
},
);
const element = await fixture(`<${tag}></${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`<button id="button1">with delegation</button>`;
}
},
);
const element = /** @type {LitElement} */ (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`
<span>Outside</span>
<slot name="button2"></slot>
`;
}
},
);
const element = await fixture(`<${tag}><button slot="button">click me</button></${tag}>`);
const cb = sinon.spy();
element.addEventListener('click', cb);
const childEl = /** @type {HTMLElement} */ (Array.from(element.children)?.find(
child => child.slot === 'button',
));
childEl?.click();
expect(cb.callCount).to.equal(1);
});
it('will still support other events', async () => {
class FooDelegate extends DelegateMixin(LitElement) {
get delegations() {
return {
...super.delegations,
target: () => this.shadowRoot?.getElementById('button1'),
events: ['click'],
};
}
render() {
return html`<button id="button1">with delegation</button>`;
}
foo() {
this.dispatchEvent(new CustomEvent('foo-event', { bubbles: true, composed: true }));
}
}
const tag = defineCE(FooDelegate);
const element = /** @type {FooDelegate} */ (await fixture(`<${tag}></${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`<button id="button1">with delegation</button>`;
}
},
);
const element = /** @type {HTMLElement} */ (await fixture(`<${tag}></${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' };
}
/**
* @param {?} a
* @param {?} b
*/
setFooAandB(a, b) {
this.foo.a = a;
this.foo.b = b;
}
}
customElements.define('delegate-argument-sub', DelegateArgumentSub);
class DelegateArgumentParent extends DelegateMixin(LitElement) {
get delegations() {
return {
...super.delegations,
target: () => this.shadowRoot?.getElementById('sub'),
methods: ['setFooAandB'],
};
}
render() {
return html`<delegate-argument-sub id="sub"></delegate-argument-sub>`;
}
}
const tag = defineCE(DelegateArgumentParent);
const element = /** @type {DelegateArgumentParent} */ (await fixture(`<${tag}></${tag}>`));
// @ts-ignore because this method, even though it doesn't exist on the parent, gets delegated through delegations to the child, where it does exist!
element.setFooAandB('newA', 'newB');
const sub = /** @type {DelegateArgumentSub} */ (element.shadowRoot?.getElementById('sub'));
expect(sub.foo.a).to.equal('newA');
expect(sub.foo.b).to.equal('newB');
});
it('will set delegated properties', async () => {
class PropDelegate extends DelegateMixin(LitElement) {
get delegations() {
return {
...super.delegations,
target: () => this.shadowRoot?.getElementById('button1'),
properties: ['disabled'],
};
}
render() {
return html`<button id="button1">with delegation</button>`;
}
}
const tag = defineCE(PropDelegate);
const element = /** @type {PropDelegate} */ (await fixture(`<${tag}></${tag}>`));
// @ts-ignore ignoring this one, because disabled is delegated through target so it indeed does not inherently exist on the div element
element.disabled = true;
await element.updateComplete;
/** @typedef {Object.<string,boolean>} Btn */
/** @typedef {Btn & HTMLElement} DelegatedBtn */
const btn = /** @type {DelegatedBtn} */ (element.shadowRoot?.getElementById('button1'));
expect(btn?.disabled).to.equal(true);
expect(btn?.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`<button id="button1">with delegation</button>`;
}
},
);
/** @typedef {Object.<string,boolean>} Btn */
/** @typedef {Btn & LitElement} DelegatedEl */
const element = /** @type {DelegatedEl} */ (document.createElement(tag));
element.disabled = true;
document.body.appendChild(element);
await element.updateComplete;
/** @typedef {Btn & HTMLElement} DelegatedBtn */
const btn = /** @type {DelegatedBtn} */ (element.shadowRoot?.getElementById('button1'));
expect(btn?.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`<button id="button1">with delegation</button>`;
}
},
);
const element = /** @type {LitElement} */ (await fixture(`<${tag}></${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`<button id="button1">with delegation</button>`;
}
},
);
const element = await fixture(`<${tag} disabled></${tag}>`);
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`<button id="button1">with delegation</button>`;
}
},
);
const element = /** @type {LitElement} */ (await fixture(`<${tag} disabled></${tag}>`));
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 () => {
class ScheduledElement 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 tag = defineCE(ScheduledElement);
const tagName = unsafeStatic(tag);
// Here, the Application Developerd tries to set the type via attribute
const elementAttr = /** @type {ScheduledElement} */ (await fixture(
`<${tag} type="radio"></${tag}>`,
));
expect(elementAttr.scheduledElement?.type).to.equal('radio');
// Here, the Application Developer tries to set the type via property
const elementProp = /** @type {ScheduledElement} */ (await fixture(
html`<${tagName} .type=${'radio'}></${tagName}>`,
));
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 () => {
class FallbackEl 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 tag = defineCE(FallbackEl);
const element = /** @type {FallbackEl} */ (await fixture(`<${tag} type="radio"></${tag}>`));
expect(element.delegatedEl).to.equal(null);
// @ts-ignore ignoring this one, because type is delegated through target so it indeed does not inherently exist on the div element
expect(element.type).to.equal('radio'); // value retrieved from host instead of delegatedTarget
});
it('works with connectedCallback', async () => {
class ConnectedElement extends DelegateMixin(HTMLElement) {
get delegations() {
return {
...super.delegations,
target: () => this.querySelector('div'),
properties: ['foo'],
};
}
}
const tag = await defineCE(ConnectedElement);
const element = /** @type {ConnectedElement} */ (await fixture(`<${tag}><div></div></${tag}>`));
// @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element
element.foo = 'new';
// @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element
expect(element.querySelector('div')?.foo).to.equal('new');
});
it('works with shadow dom', async () => {
class A extends DelegateMixin(LitElement) {
get delegations() {
return {
...super.delegations,
target: () => this.shadowRoot?.querySelector('div'),
properties: ['foo'],
};
}
render() {
return html`<div></div>`;
}
}
const tag = await defineCE(A);
const element = await fixture(`<${tag}></${tag}>`);
// @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element
element.foo = 'new';
// @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element
expect(element.shadowRoot?.querySelector('div')?.foo).to.equal('new');
});
it('works with light dom', async () => {
class A extends DelegateMixin(LitElement) {
get delegations() {
return {
...super.delegations,
target: () => this.querySelector('div'),
properties: ['foo'],
};
}
createRenderRoot() {
return this;
}
render() {
return html`<div></div>`;
}
}
const tag = await defineCE(A);
const element = await fixture(`<${tag}></${tag}>`);
// @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element
element.foo = 'new';
// @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element
expect(element.querySelector('div')?.foo).to.equal('new');
});
});