preact-custom-element
Version:
Wrap your component up as a custom element
283 lines (227 loc) • 7.73 kB
JSX
import { assert } from '@open-wc/testing';
import { h, createContext } from 'preact';
import { useContext } from 'preact/hooks';
import { act } from 'preact/test-utils';
import registerElement from './index';
describe('web components', () => {
/** @type {HTMLDivElement} */
let root;
beforeEach(() => {
root = document.createElement('div');
document.body.appendChild(root);
});
afterEach(() => {
document.body.removeChild(root);
});
function Clock({ time }) {
return <span>{time}</span>;
}
registerElement(Clock, 'x-clock', ['time', 'custom-date']);
it('renders ok, updates on attr change', () => {
const el = document.createElement('x-clock');
el.setAttribute('time', '10:28:57 PM');
root.appendChild(el);
assert.equal(
root.innerHTML,
'<x-clock time="10:28:57 PM"><span>10:28:57 PM</span></x-clock>'
);
el.setAttribute('time', '11:01:10 AM');
assert.equal(
root.innerHTML,
'<x-clock time="11:01:10 AM"><span>11:01:10 AM</span></x-clock>'
);
});
function NullProps({ size = 'md' }) {
return <div>{size.toUpperCase()}</div>;
}
registerElement(NullProps, 'x-null-props', ['size'], { shadow: true });
// #50
it('remove attributes without crashing', () => {
const el = document.createElement('x-null-props');
assert.doesNotThrow(() => (el.size = 'foo'));
root.appendChild(el);
assert.doesNotThrow(() => el.removeAttribute('size'));
});
describe('DOM properties', () => {
it('passes property changes to props', () => {
const el = document.createElement('x-clock');
el.time = '10:28:57 PM';
assert.equal(el.time, '10:28:57 PM');
root.appendChild(el);
assert.equal(
root.innerHTML,
'<x-clock time="10:28:57 PM"><span>10:28:57 PM</span></x-clock>'
);
el.time = '11:01:10 AM';
assert.equal(el.time, '11:01:10 AM');
assert.equal(
root.innerHTML,
'<x-clock time="11:01:10 AM"><span>11:01:10 AM</span></x-clock>'
);
});
function DummyButton({ onClick, text = 'click' }) {
return <button onClick={onClick}>{text}</button>;
}
registerElement(DummyButton, 'x-dummy-button', ['onClick', 'text']);
it('passes simple properties changes to props', () => {
const el = document.createElement('x-dummy-button');
el.text = 'foo';
assert.equal(el.text, 'foo');
root.appendChild(el);
assert.equal(
root.innerHTML,
'<x-dummy-button text="foo"><button>foo</button></x-dummy-button>'
);
// Update
el.text = 'bar';
assert.equal(
root.innerHTML,
'<x-dummy-button text="bar"><button>bar</button></x-dummy-button>'
);
});
it('passes complex properties changes to props', () => {
const el = document.createElement('x-dummy-button');
let clicks = 0;
const onClick = () => clicks++;
el.onClick = onClick;
assert.equal(el.onClick, onClick);
root.appendChild(el);
assert.equal(
root.innerHTML,
'<x-dummy-button><button>click</button></x-dummy-button>'
);
act(() => {
el.querySelector('button').click();
});
assert.equal(clicks, 1);
// Update
let other = 0;
el.onClick = () => other++;
act(() => {
el.querySelector('button').click();
});
assert.equal(other, 1);
});
});
function Foo({ text, children }) {
return (
<span class="wrapper">
<div class="children">{children}</div>
<div class="slotted">{text}</div>
</span>
);
}
registerElement(Foo, 'x-foo', [], { shadow: true });
it('renders slots as props with shadow DOM', () => {
const el = document.createElement('x-foo');
// <span slot="text">here is a slot</span>
const slot = document.createElement('span');
slot.textContent = 'here is a slot';
slot.slot = 'text';
el.appendChild(slot);
// <div>no slot</div>
const noSlot = document.createElement('div');
noSlot.textContent = 'no slot';
el.appendChild(noSlot);
el.appendChild(slot);
root.appendChild(el);
assert.equal(
root.innerHTML,
'<x-foo><div>no slot</div><span slot="text">here is a slot</span></x-foo>'
);
const shadowHTML = document.querySelector('x-foo').shadowRoot.innerHTML;
assert.equal(
shadowHTML,
'<span class="wrapper"><div class="children"><slot><div>no slot</div></slot></div><div class="slotted"><slot name="text"><span>here is a slot</span></slot></div></span>'
);
});
const kebabName = 'custom-date-long-name';
const camelName = 'customDateLongName';
const lowerName = camelName.toLowerCase();
function PropNameTransform(props) {
return (
<span>
{props[kebabName]} {props[lowerName]} {props[camelName]}
</span>
);
}
registerElement(PropNameTransform, 'x-prop-name-transform', [
kebabName,
camelName,
]);
it('handles kebab-case attributes with passthrough', () => {
const el = document.createElement('x-prop-name-transform');
el.setAttribute(kebabName, '11/11/2011');
el.setAttribute(camelName, 'pretended to be camel');
root.appendChild(el);
assert.equal(
root.innerHTML,
`<x-prop-name-transform ${kebabName}="11/11/2011" ${lowerName}="pretended to be camel"><span>11/11/2011 pretended to be camel 11/11/2011</span></x-prop-name-transform>`
);
el.setAttribute(kebabName, '01/01/2001');
assert.equal(
root.innerHTML,
`<x-prop-name-transform ${kebabName}="01/01/2001" ${lowerName}="pretended to be camel"><span>01/01/2001 pretended to be camel 01/01/2001</span></x-prop-name-transform>`
);
});
const Theme = createContext('light');
function DisplayTheme() {
const theme = useContext(Theme);
return <p>Active theme: {theme}</p>;
}
registerElement(DisplayTheme, 'x-display-theme', [], { shadow: true });
function Parent({ children, theme = 'dark' }) {
return (
<Theme.Provider value={theme}>
<div class="children">{children}</div>
</Theme.Provider>
);
}
registerElement(Parent, 'x-parent', ['theme'], { shadow: true });
it('passes context over custom element boundaries', async () => {
const el = document.createElement('x-parent');
const noSlot = document.createElement('x-display-theme');
el.appendChild(noSlot);
root.appendChild(el);
assert.equal(
root.innerHTML,
'<x-parent><x-display-theme></x-display-theme></x-parent>'
);
const getShadowHTML = () =>
document.querySelector('x-display-theme').shadowRoot.innerHTML;
assert.equal(getShadowHTML(), '<p>Active theme: dark</p>');
// Trigger context update
act(() => {
el.setAttribute('theme', 'sunny');
});
assert.equal(getShadowHTML(), '<p>Active theme: sunny</p>');
});
it('renders element in shadow dom open mode', async () => {
function ShadowDomOpen() {
return <div className="shadow-child">Shadow DOM Open</div>;
}
registerElement(ShadowDomOpen, 'x-shadowdom-open', [], {
shadow: true,
mode: 'open',
});
const el = document.createElement('x-shadowdom-open');
root.appendChild(el);
const shadowRoot = el.shadowRoot;
assert.isTrue(!!shadowRoot);
const child = shadowRoot.querySelector('.shadow-child');
assert.isTrue(!!child);
assert.equal(child.textContent, 'Shadow DOM Open');
});
it('renders element in shadow dom closed mode', async () => {
function ShadowDomClosed() {
return <div className="shadow-child">Shadow DOM Closed</div>;
}
registerElement(ShadowDomClosed, 'x-shadowdom-closed', [], {
shadow: true,
mode: 'closed',
});
const el = document.createElement('x-shadowdom-closed');
root.appendChild(el);
assert.isTrue(el.shadowRoot === null);
});
});