From f000aa254680241a56091c9ef4f5c22e3f20d041 Mon Sep 17 00:00:00 2001 From: Ryan Christian Date: Fri, 24 Oct 2025 19:46:57 -0500 Subject: [PATCH 1/4] test: Create dedicated options & static props test sections --- test/index.test.jsx | 353 ++++++++++++++++++++++++++++---------------- 1 file changed, 223 insertions(+), 130 deletions(-) diff --git a/test/index.test.jsx b/test/index.test.jsx index 734ab6c..5d51333 100644 --- a/test/index.test.jsx +++ b/test/index.test.jsx @@ -29,6 +29,229 @@ describe('web components', () => { document.body.removeChild(root); }); + describe('options bag', () => { + it('supports `shadow`', () => { + function ShadowDom() { + return
Shadow DOM
; + } + + registerElement(ShadowDom, 'x-shadowdom', [], { shadow: true }); + const el = document.createElement('x-shadowdom'); + root.appendChild(el); + + const shadowRoot = el.shadowRoot; + assert.isTrue(!!shadowRoot); + }); + + it('supports `mode: "open"`', () => { + function ShadowDomOpen() { + return
Shadow DOM Open
; + } + + 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('supports `mode: "closed"`', () => { + function ShadowDomClosed() { + return
Shadow DOM Closed
; + } + + registerElement(ShadowDomClosed, 'x-shadowdom-closed', [], { + shadow: true, + mode: 'closed', + }); + + const el = document.createElement('x-shadowdom-closed'); + root.appendChild(el); + + assert.isTrue(el.shadowRoot === null); + }); + + it('supports `adoptedStyleSheets`', () => { + function AdoptedStyleSheets() { + return
Adopted Style Sheets
; + } + + const sheet = new CSSStyleSheet(); + sheet.replaceSync('.styled-child { color: red; }'); + + registerElement(AdoptedStyleSheets, 'x-adopted-style-sheets', [], { + shadow: true, + adoptedStyleSheets: [sheet], + }); + + root.innerHTML = ``; + + const child = document + .querySelector('x-adopted-style-sheets') + .shadowRoot.querySelector('.styled-child'); + + const style = getComputedStyle(child); + assert.equal(style.color, 'rgb(255, 0, 0)'); + }); + + it('supports `serializable`', async () => { + function SerializableComponent() { + return
Serializable Shadow DOM
; + } + + function NonSerializableComponent() { + return
Non-serializable Shadow DOM
; + } + + registerElement(SerializableComponent, 'x-serializable', [], { + shadow: true, + serializable: true, + }); + + registerElement(NonSerializableComponent, 'x-non-serializable', [], { + shadow: true, + }); + + root.innerHTML = ` + + + `; + + const serializableEl = document.querySelector('x-serializable'); + const nonSerializableEl = document.querySelector('x-non-serializable'); + + assert.isTrue(serializableEl.shadowRoot.serializable); + assert.isFalse(nonSerializableEl.shadowRoot.serializable); + + const serializableHtml = serializableEl.getHTML({ + serializableShadowRoots: true, + }); + const nonSerializableHtml = nonSerializableEl.getHTML({ + serializableShadowRoots: true, + }); + + assert.equal( + serializableHtml, + '' + ); + assert.isEmpty(nonSerializableHtml); + }); + }); + + describe('static properties', () => { + it('supports `tagName`', () => { + class TagNameClass extends Component { + static tagName = 'x-tag-name-class'; + + render() { + return ; + } + } + registerElement(TagNameClass); + + function TagNameFunction() { + return ; + } + TagNameFunction.tagName = 'x-tag-name-function'; + registerElement(TagNameFunction); + + root.innerHTML = ` +
+ + +
+ `; + + assert.isTrue(!!document.querySelector('x-tag-name-class')); + assert.isTrue(!!document.querySelector('x-tag-name-function')); + }); + + it('supports `observedAttributes`', () => { + class ObservedAttributesClass extends Component { + static observedAttributes = ['name']; + + render({ name }) { + return ; + } + } + registerElement(ObservedAttributesClass, 'x-observed-attributes-class'); + + function ObservedAttributesFunction({ name }) { + return ; + } + ObservedAttributesFunction.observedAttributes = ['name']; + registerElement( + ObservedAttributesFunction, + 'x-observed-attributes-function' + ); + + const observedAttributesClassEl = document.createElement( + 'x-observed-attributes-class' + ); + const observedAttributesFunctionEl = document.createElement( + 'x-observed-attributes-function' + ); + + observedAttributesClassEl.setAttribute('name', 'class-name'); + observedAttributesFunctionEl.setAttribute('name', 'function-name'); + + root.appendChild(observedAttributesClassEl); + root.appendChild(observedAttributesFunctionEl); + + assert.equal( + root.innerHTML, + `` + ); + + observedAttributesClassEl.setAttribute('name', 'new-class-name'); + observedAttributesFunctionEl.setAttribute('name', 'new-function-name'); + + assert.equal( + root.innerHTML, + `` + ); + }); + + it('supports `formAssociated`', () => { + class FormAssociatedClass extends Component { + static formAssociated = true; + + render() { + return ; + } + } + registerElement(FormAssociatedClass, 'x-form-associated-class', []); + + function FormAssociatedFunction() { + return ; + } + FormAssociatedFunction.formAssociated = true; + registerElement(FormAssociatedFunction, 'x-form-associated-function', []); + + root.innerHTML = ` +
+ + +
+ `; + + const myForm = document.getElementById('myForm'); + + // The `.elements` property of a form includes all form-associated elements + assert.equal(myForm.elements[0].tagName, 'X-FORM-ASSOCIATED-CLASS'); + assert.equal(myForm.elements[2].tagName, 'X-FORM-ASSOCIATED-FUNCTION'); + }); + }); + function Clock({ time }) { return {time}; } @@ -285,93 +508,6 @@ describe('web components', () => { assert.equal(getShadowHTML(), '

Active theme: sunny

'); }); - it('renders element in shadow dom open mode', async () => { - function ShadowDomOpen() { - return
Shadow DOM Open
; - } - - 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
Shadow DOM Closed
; - } - - registerElement(ShadowDomClosed, 'x-shadowdom-closed', [], { - shadow: true, - mode: 'closed', - }); - - const el = document.createElement('x-shadowdom-closed'); - root.appendChild(el); - assert.isTrue(el.shadowRoot === null); - }); - - it('supports the `formAssociated` property', async () => { - class FormAssociatedClass extends Component { - static formAssociated = true; - - render() { - return ; - } - } - registerElement(FormAssociatedClass, 'x-form-associated-class', []); - - function FormAssociatedFunction() { - return ; - } - FormAssociatedFunction.formAssociated = true; - registerElement(FormAssociatedFunction, 'x-form-associated-function', []); - - root.innerHTML = ` -
- - -
- `; - - const myForm = document.getElementById('myForm'); - - // The `.elements` property of a form includes all form-associated elements - assert.equal(myForm.elements[0].tagName, 'X-FORM-ASSOCIATED-CLASS'); - assert.equal(myForm.elements[2].tagName, 'X-FORM-ASSOCIATED-FUNCTION'); - }); - - it('supports the `adoptedStyleSheets` option', async () => { - function AdoptedStyleSheets() { - return
Adopted Style Sheets
; - } - - const sheet = new CSSStyleSheet(); - sheet.replaceSync('.styled-child { color: red; }'); - - registerElement(AdoptedStyleSheets, 'x-adopted-style-sheets', [], { - shadow: true, - adoptedStyleSheets: [sheet], - }); - - root.innerHTML = ``; - - const child = document - .querySelector('x-adopted-style-sheets') - .shadowRoot.querySelector('.styled-child'); - - const style = getComputedStyle(child); - assert.equal(style.color, 'rgb(255, 0, 0)'); - }); - it('supports controlling light DOM children', async () => { function LightDomChildren({ children }) { return ( @@ -421,47 +557,4 @@ describe('web components', () => { '

Light DOM Children

Child 1

Child 2

' ); }); - - it('supports the `serializable` option', async () => { - function SerializableComponent() { - return
Serializable Shadow DOM
; - } - - function NonSerializableComponent() { - return
Non-serializable Shadow DOM
; - } - - registerElement(SerializableComponent, 'x-serializable', [], { - shadow: true, - serializable: true, - }); - - registerElement(NonSerializableComponent, 'x-non-serializable', [], { - shadow: true, - }); - - root.innerHTML = ` - - - `; - - const serializableEl = document.querySelector('x-serializable'); - const nonSerializableEl = document.querySelector('x-non-serializable'); - - assert.isTrue(serializableEl.shadowRoot.serializable); - assert.isFalse(nonSerializableEl.shadowRoot.serializable); - - const serializableHtml = serializableEl.getHTML({ - serializableShadowRoots: true, - }); - const nonSerializableHtml = nonSerializableEl.getHTML({ - serializableShadowRoots: true, - }); - - assert.equal( - serializableHtml, - '' - ); - assert.isEmpty(nonSerializableHtml); - }); }); From d807e6e208ef9f197084fb960b1ddcaeb6580135 Mon Sep 17 00:00:00 2001 From: Ryan Christian Date: Fri, 24 Oct 2025 19:47:11 -0500 Subject: [PATCH 2/4] test: Remove a few extraneous async's --- test/index.test.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/index.test.jsx b/test/index.test.jsx index 5d51333..e2cd8d3 100644 --- a/test/index.test.jsx +++ b/test/index.test.jsx @@ -485,7 +485,7 @@ describe('web components', () => { registerElement(Parent, 'x-parent', ['theme'], { shadow: true }); - it('passes context over custom element boundaries', async () => { + it('passes context over custom element boundaries', () => { const el = document.createElement('x-parent'); const noSlot = document.createElement('x-display-theme'); @@ -508,7 +508,7 @@ describe('web components', () => { assert.equal(getShadowHTML(), '

Active theme: sunny

'); }); - it('supports controlling light DOM children', async () => { + it('supports controlling light DOM children', () => { function LightDomChildren({ children }) { return ( From 522436a0ab1eab6894e68b0b4cbb64be75060006 Mon Sep 17 00:00:00 2001 From: Ryan Christian Date: Fri, 24 Oct 2025 23:07:14 -0500 Subject: [PATCH 3/4] test: Further suite refactoring --- package.json | 4 +- test/browser/index.test.jsx | 340 +++++++++++ test/browser/options.test.jsx | 133 +++++ test/browser/static-properties.test.jsx | 122 ++++ test/index.test.jsx | 560 ------------------ test/{types.test.tsx => types/index.test.tsx} | 2 +- test/{ => types}/tsconfig.json | 1 - 7 files changed, 598 insertions(+), 564 deletions(-) create mode 100644 test/browser/index.test.jsx create mode 100644 test/browser/options.test.jsx create mode 100644 test/browser/static-properties.test.jsx delete mode 100644 test/index.test.jsx rename test/{types.test.tsx => types/index.test.tsx} (91%) rename test/{ => types}/tsconfig.json (87%) diff --git a/package.json b/package.json index 3bd8b13..6447fd6 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "build": "microbundle -f cjs,es,umd --no-generateTypes", "lint": "eslint src/*.js", "test": "npm run test:types & npm run test:browser", - "test:browser": "wtr test/*.test.{js,jsx}", - "test:types": "tsc -p test/", + "test:browser": "wtr test/browser/*.test.{js,jsx}", + "test:types": "tsc -p test/types/", "prettier": "prettier **/*.{js,jsx} --write", "prepublishOnly": "npm run build && npm run lint && npm run test" }, diff --git a/test/browser/index.test.jsx b/test/browser/index.test.jsx new file mode 100644 index 0000000..1a8c002 --- /dev/null +++ b/test/browser/index.test.jsx @@ -0,0 +1,340 @@ +import { assert } from '@open-wc/testing'; +import { h, createContext, Fragment } from 'preact'; +import { useContext } from 'preact/hooks'; +import { act } from 'preact/test-utils'; +import registerElement from '../../src/index'; + +/** @param {string} name */ +function createTestElement(name) { + const el = document.createElement(name); + const child1 = document.createElement('p'); + child1.textContent = 'Child 1'; + const child2 = document.createElement('p'); + child2.textContent = 'Child 2'; + el.appendChild(child1); + el.appendChild(child2); + return el; +} + +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 {time}; + } + + registerElement(Clock, 'x-clock', ['time']); + + 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, + '10:28:57 PM' + ); + + el.setAttribute('time', '11:01:10 AM'); + assert.equal( + root.innerHTML, + '11:01:10 AM' + ); + }); + + function NullProps({ size = 'md' }) { + return
{size.toUpperCase()}
; + } + + 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, + '10:28:57 PM' + ); + + el.time = '11:01:10 AM'; + assert.equal(el.time, '11:01:10 AM'); + + assert.equal( + root.innerHTML, + '11:01:10 AM' + ); + }); + + function DummyButton({ onClick, text = 'click' }) { + return ; + } + + 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, + '' + ); + + // Update + el.text = 'bar'; + assert.equal( + root.innerHTML, + '' + ); + }); + + 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, + '' + ); + + 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); + }); + + it('sets complex property after other property', () => { + const el = document.createElement('x-dummy-button'); + + // set simple property first + el.text = 'click me'; + + let clicks = 0; + const onClick = () => clicks++; + el.onClick = onClick; + + assert.equal(el.text, 'click me'); + assert.equal(el.onClick, onClick); + + root.appendChild(el); + assert.equal( + root.innerHTML, + '' + ); + + act(() => { + el.querySelector('button').click(); + }); + + assert.equal(el.onClick, onClick); + assert.equal(clicks, 1); + }); + }); + + it('renders slots as props with shadow DOM', () => { + function Slots({ text, children }) { + return ( + +
{children}
+
{text}
+
+ ); + } + + registerElement(Slots, 'x-slots', [], { shadow: true }); + + const el = document.createElement('x-slots'); + + // here is a slot + const slot = document.createElement('span'); + slot.textContent = 'here is a slot'; + slot.slot = 'text'; + el.appendChild(slot); + + //
no slot
+ const noSlot = document.createElement('div'); + noSlot.textContent = 'no slot'; + el.appendChild(noSlot); + el.appendChild(slot); + + root.appendChild(el); + assert.equal( + root.innerHTML, + '
no slot
here is a slot
' + ); + + const shadowHTML = document.querySelector('x-slots').shadowRoot.innerHTML; + assert.equal( + shadowHTML, + '
no slot
here is a slot
' + ); + }); + + const kebabName = 'custom-date-long-name'; + const camelName = 'customDateLongName'; + const lowerName = camelName.toLowerCase(); + function PropNameTransform(props) { + return ( + + {props[kebabName]} {props[lowerName]} {props[camelName]} + + ); + } + 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, + `11/11/2011 pretended to be camel 11/11/2011` + ); + + el.setAttribute(kebabName, '01/01/2001'); + assert.equal( + root.innerHTML, + `01/01/2001 pretended to be camel 01/01/2001` + ); + }); + + describe('children', () => { + it('supports controlling light DOM children', () => { + function LightDomChildren({ children }) { + return ( + +

Light DOM Children

+
{children}
+
+ ); + } + + registerElement(LightDomChildren, 'light-dom-children', []); + registerElement(LightDomChildren, 'light-dom-children-shadow-false', [], { + shadow: false, + }); + + root.appendChild(createTestElement('light-dom-children')); + root.appendChild(createTestElement('light-dom-children-shadow-false')); + + assert.equal( + document.querySelector('light-dom-children').innerHTML, + '

Light DOM Children

Child 1

Child 2

' + ); + assert.equal( + document.querySelector('light-dom-children-shadow-false').innerHTML, + '

Light DOM Children

Child 1

Child 2

' + ); + }); + + it('supports controlling shadow DOM children', () => { + function ShadowDomChildren({ children }) { + return ( + +

Light DOM Children

+
{children}
+
+ ); + } + + registerElement(ShadowDomChildren, 'shadow-dom-children', [], { + shadow: true, + }); + + root.appendChild(createTestElement('shadow-dom-children')); + + assert.equal( + document.querySelector('shadow-dom-children').shadowRoot.innerHTML, + '

Light DOM Children

Child 1

Child 2

' + ); + }); + }); + + describe('context', () => { + const Theme = createContext('light'); + + function DisplayTheme() { + const theme = useContext(Theme); + return

Active theme: {theme}

; + } + + function Parent({ children, theme = 'dark' }) { + return ( + +
{children}
+
+ ); + } + + registerElement(Parent, 'x-parent', ['theme'], { shadow: true }); + registerElement(DisplayTheme, 'x-display-theme', [], { shadow: true }); + + it('passes context over custom element boundaries', () => { + const el = document.createElement('x-parent'); + + const noSlot = document.createElement('x-display-theme'); + el.appendChild(noSlot); + + root.appendChild(el); + assert.equal( + root.innerHTML, + '' + ); + + const getShadowHTML = () => + document.querySelector('x-display-theme').shadowRoot.innerHTML; + assert.equal(getShadowHTML(), '

Active theme: dark

'); + + // Trigger context update + act(() => { + el.setAttribute('theme', 'sunny'); + }); + assert.equal(getShadowHTML(), '

Active theme: sunny

'); + }); + }); +}); diff --git a/test/browser/options.test.jsx b/test/browser/options.test.jsx new file mode 100644 index 0000000..dee9b28 --- /dev/null +++ b/test/browser/options.test.jsx @@ -0,0 +1,133 @@ +import { assert } from '@open-wc/testing'; +import { h } from 'preact'; +import registerElement from '../../src/index'; + +describe('options bag', () => { + /** @type {HTMLDivElement} */ + let root; + + beforeEach(() => { + root = document.createElement('div'); + document.body.appendChild(root); + }); + + afterEach(() => { + document.body.removeChild(root); + }); + + it('supports `shadow`', () => { + function ShadowDom() { + return
Shadow DOM
; + } + + registerElement(ShadowDom, 'x-shadowdom', [], { shadow: true }); + const el = document.createElement('x-shadowdom'); + root.appendChild(el); + + const shadowRoot = el.shadowRoot; + assert.isTrue(!!shadowRoot); + }); + + it('supports `mode: "open"`', () => { + function ShadowDomOpen() { + return
Shadow DOM Open
; + } + + 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('supports `mode: "closed"`', () => { + function ShadowDomClosed() { + return
Shadow DOM Closed
; + } + + registerElement(ShadowDomClosed, 'x-shadowdom-closed', [], { + shadow: true, + mode: 'closed', + }); + + const el = document.createElement('x-shadowdom-closed'); + root.appendChild(el); + + assert.isTrue(el.shadowRoot === null); + }); + + it('supports `adoptedStyleSheets`', () => { + function AdoptedStyleSheets() { + return
Adopted Style Sheets
; + } + + const sheet = new CSSStyleSheet(); + sheet.replaceSync('.styled-child { color: red; }'); + + registerElement(AdoptedStyleSheets, 'x-adopted-style-sheets', [], { + shadow: true, + adoptedStyleSheets: [sheet], + }); + + root.innerHTML = ``; + + const child = document + .querySelector('x-adopted-style-sheets') + .shadowRoot.querySelector('.styled-child'); + + const style = getComputedStyle(child); + assert.equal(style.color, 'rgb(255, 0, 0)'); + }); + + it('supports `serializable`', async () => { + function SerializableComponent() { + return
Serializable Shadow DOM
; + } + + function NonSerializableComponent() { + return
Non-serializable Shadow DOM
; + } + + registerElement(SerializableComponent, 'x-serializable', [], { + shadow: true, + serializable: true, + }); + + registerElement(NonSerializableComponent, 'x-non-serializable', [], { + shadow: true, + }); + + root.innerHTML = ` + + + `; + + const serializableEl = document.querySelector('x-serializable'); + const nonSerializableEl = document.querySelector('x-non-serializable'); + + assert.isTrue(serializableEl.shadowRoot.serializable); + assert.isFalse(nonSerializableEl.shadowRoot.serializable); + + const serializableHtml = serializableEl.getHTML({ + serializableShadowRoots: true, + }); + const nonSerializableHtml = nonSerializableEl.getHTML({ + serializableShadowRoots: true, + }); + + assert.equal( + serializableHtml, + '' + ); + assert.isEmpty(nonSerializableHtml); + }); +}); diff --git a/test/browser/static-properties.test.jsx b/test/browser/static-properties.test.jsx new file mode 100644 index 0000000..4ae91fe --- /dev/null +++ b/test/browser/static-properties.test.jsx @@ -0,0 +1,122 @@ +import { assert } from '@open-wc/testing'; +import { h, Component } from 'preact'; +import registerElement from '../../src/index'; + +describe('static properties', () => { + /** @type {HTMLDivElement} */ + let root; + + beforeEach(() => { + root = document.createElement('div'); + document.body.appendChild(root); + }); + + afterEach(() => { + document.body.removeChild(root); + }); + + it('supports `tagName`', () => { + class TagNameClass extends Component { + static tagName = 'x-tag-name-class'; + + render() { + return ; + } + } + registerElement(TagNameClass); + + function TagNameFunction() { + return ; + } + TagNameFunction.tagName = 'x-tag-name-function'; + registerElement(TagNameFunction); + + root.innerHTML = ` +
+ + +
+ `; + + assert.isTrue(!!document.querySelector('x-tag-name-class')); + assert.isTrue(!!document.querySelector('x-tag-name-function')); + }); + + it('supports `observedAttributes`', () => { + class ObservedAttributesClass extends Component { + static observedAttributes = ['name']; + + render({ name }) { + return ; + } + } + registerElement(ObservedAttributesClass, 'x-observed-attributes-class'); + + function ObservedAttributesFunction({ name }) { + return ; + } + ObservedAttributesFunction.observedAttributes = ['name']; + registerElement( + ObservedAttributesFunction, + 'x-observed-attributes-function' + ); + + const observedAttributesClassEl = document.createElement( + 'x-observed-attributes-class' + ); + const observedAttributesFunctionEl = document.createElement( + 'x-observed-attributes-function' + ); + + observedAttributesClassEl.setAttribute('name', 'class-name'); + observedAttributesFunctionEl.setAttribute('name', 'function-name'); + + root.appendChild(observedAttributesClassEl); + root.appendChild(observedAttributesFunctionEl); + + assert.equal( + root.innerHTML, + `` + ); + + observedAttributesClassEl.setAttribute('name', 'new-class-name'); + observedAttributesFunctionEl.setAttribute('name', 'new-function-name'); + + assert.equal( + root.innerHTML, + `` + ); + }); + + it('supports `formAssociated`', () => { + class FormAssociatedClass extends Component { + static formAssociated = true; + + render() { + return ; + } + } + registerElement(FormAssociatedClass, 'x-form-associated-class', []); + + function FormAssociatedFunction() { + return ; + } + FormAssociatedFunction.formAssociated = true; + registerElement(FormAssociatedFunction, 'x-form-associated-function', []); + + root.innerHTML = ` +
+ + +
+ `; + + const myForm = /** @type {HTMLFormElement} */ ( + document.getElementById('myForm') + ); + + // The `.elements` property of a form includes all form-associated elements + assert.equal(myForm.elements[0].tagName, 'X-FORM-ASSOCIATED-CLASS'); + assert.equal(myForm.elements[2].tagName, 'X-FORM-ASSOCIATED-FUNCTION'); + }); +}); diff --git a/test/index.test.jsx b/test/index.test.jsx deleted file mode 100644 index e2cd8d3..0000000 --- a/test/index.test.jsx +++ /dev/null @@ -1,560 +0,0 @@ -import { assert } from '@open-wc/testing'; -import { h, createContext, Component, Fragment } from 'preact'; -import { useContext } from 'preact/hooks'; -import { act } from 'preact/test-utils'; -import registerElement from '../src/index'; - -/** @param {string} name */ -function createTestElement(name) { - const el = document.createElement(name); - const child1 = document.createElement('p'); - child1.textContent = 'Child 1'; - const child2 = document.createElement('p'); - child2.textContent = 'Child 2'; - el.appendChild(child1); - el.appendChild(child2); - return el; -} - -describe('web components', () => { - /** @type {HTMLDivElement} */ - let root; - - beforeEach(() => { - root = document.createElement('div'); - document.body.appendChild(root); - }); - - afterEach(() => { - document.body.removeChild(root); - }); - - describe('options bag', () => { - it('supports `shadow`', () => { - function ShadowDom() { - return
Shadow DOM
; - } - - registerElement(ShadowDom, 'x-shadowdom', [], { shadow: true }); - const el = document.createElement('x-shadowdom'); - root.appendChild(el); - - const shadowRoot = el.shadowRoot; - assert.isTrue(!!shadowRoot); - }); - - it('supports `mode: "open"`', () => { - function ShadowDomOpen() { - return
Shadow DOM Open
; - } - - 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('supports `mode: "closed"`', () => { - function ShadowDomClosed() { - return
Shadow DOM Closed
; - } - - registerElement(ShadowDomClosed, 'x-shadowdom-closed', [], { - shadow: true, - mode: 'closed', - }); - - const el = document.createElement('x-shadowdom-closed'); - root.appendChild(el); - - assert.isTrue(el.shadowRoot === null); - }); - - it('supports `adoptedStyleSheets`', () => { - function AdoptedStyleSheets() { - return
Adopted Style Sheets
; - } - - const sheet = new CSSStyleSheet(); - sheet.replaceSync('.styled-child { color: red; }'); - - registerElement(AdoptedStyleSheets, 'x-adopted-style-sheets', [], { - shadow: true, - adoptedStyleSheets: [sheet], - }); - - root.innerHTML = ``; - - const child = document - .querySelector('x-adopted-style-sheets') - .shadowRoot.querySelector('.styled-child'); - - const style = getComputedStyle(child); - assert.equal(style.color, 'rgb(255, 0, 0)'); - }); - - it('supports `serializable`', async () => { - function SerializableComponent() { - return
Serializable Shadow DOM
; - } - - function NonSerializableComponent() { - return
Non-serializable Shadow DOM
; - } - - registerElement(SerializableComponent, 'x-serializable', [], { - shadow: true, - serializable: true, - }); - - registerElement(NonSerializableComponent, 'x-non-serializable', [], { - shadow: true, - }); - - root.innerHTML = ` - - - `; - - const serializableEl = document.querySelector('x-serializable'); - const nonSerializableEl = document.querySelector('x-non-serializable'); - - assert.isTrue(serializableEl.shadowRoot.serializable); - assert.isFalse(nonSerializableEl.shadowRoot.serializable); - - const serializableHtml = serializableEl.getHTML({ - serializableShadowRoots: true, - }); - const nonSerializableHtml = nonSerializableEl.getHTML({ - serializableShadowRoots: true, - }); - - assert.equal( - serializableHtml, - '' - ); - assert.isEmpty(nonSerializableHtml); - }); - }); - - describe('static properties', () => { - it('supports `tagName`', () => { - class TagNameClass extends Component { - static tagName = 'x-tag-name-class'; - - render() { - return ; - } - } - registerElement(TagNameClass); - - function TagNameFunction() { - return ; - } - TagNameFunction.tagName = 'x-tag-name-function'; - registerElement(TagNameFunction); - - root.innerHTML = ` -
- - -
- `; - - assert.isTrue(!!document.querySelector('x-tag-name-class')); - assert.isTrue(!!document.querySelector('x-tag-name-function')); - }); - - it('supports `observedAttributes`', () => { - class ObservedAttributesClass extends Component { - static observedAttributes = ['name']; - - render({ name }) { - return ; - } - } - registerElement(ObservedAttributesClass, 'x-observed-attributes-class'); - - function ObservedAttributesFunction({ name }) { - return ; - } - ObservedAttributesFunction.observedAttributes = ['name']; - registerElement( - ObservedAttributesFunction, - 'x-observed-attributes-function' - ); - - const observedAttributesClassEl = document.createElement( - 'x-observed-attributes-class' - ); - const observedAttributesFunctionEl = document.createElement( - 'x-observed-attributes-function' - ); - - observedAttributesClassEl.setAttribute('name', 'class-name'); - observedAttributesFunctionEl.setAttribute('name', 'function-name'); - - root.appendChild(observedAttributesClassEl); - root.appendChild(observedAttributesFunctionEl); - - assert.equal( - root.innerHTML, - `` - ); - - observedAttributesClassEl.setAttribute('name', 'new-class-name'); - observedAttributesFunctionEl.setAttribute('name', 'new-function-name'); - - assert.equal( - root.innerHTML, - `` - ); - }); - - it('supports `formAssociated`', () => { - class FormAssociatedClass extends Component { - static formAssociated = true; - - render() { - return ; - } - } - registerElement(FormAssociatedClass, 'x-form-associated-class', []); - - function FormAssociatedFunction() { - return ; - } - FormAssociatedFunction.formAssociated = true; - registerElement(FormAssociatedFunction, 'x-form-associated-function', []); - - root.innerHTML = ` -
- - -
- `; - - const myForm = document.getElementById('myForm'); - - // The `.elements` property of a form includes all form-associated elements - assert.equal(myForm.elements[0].tagName, 'X-FORM-ASSOCIATED-CLASS'); - assert.equal(myForm.elements[2].tagName, 'X-FORM-ASSOCIATED-FUNCTION'); - }); - }); - - function Clock({ time }) { - return {time}; - } - - 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, - '10:28:57 PM' - ); - - el.setAttribute('time', '11:01:10 AM'); - assert.equal( - root.innerHTML, - '11:01:10 AM' - ); - }); - - function NullProps({ size = 'md' }) { - return
{size.toUpperCase()}
; - } - - 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, - '10:28:57 PM' - ); - - el.time = '11:01:10 AM'; - assert.equal(el.time, '11:01:10 AM'); - - assert.equal( - root.innerHTML, - '11:01:10 AM' - ); - }); - - function DummyButton({ onClick, text = 'click' }) { - return ; - } - - 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, - '' - ); - - // Update - el.text = 'bar'; - assert.equal( - root.innerHTML, - '' - ); - }); - - 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, - '' - ); - - 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); - }); - - it('sets complex property after other property', () => { - const el = document.createElement('x-dummy-button'); - - // set simple property first - el.text = 'click me'; - - let clicks = 0; - const onClick = () => clicks++; - el.onClick = onClick; - - assert.equal(el.text, 'click me'); - assert.equal(el.onClick, onClick); - - root.appendChild(el); - assert.equal( - root.innerHTML, - '' - ); - - act(() => { - el.querySelector('button').click(); - }); - - assert.equal(el.onClick, onClick); - assert.equal(clicks, 1); - }); - }); - - function Foo({ text, children }) { - return ( - -
{children}
-
{text}
-
- ); - } - - registerElement(Foo, 'x-foo', [], { shadow: true }); - - it('renders slots as props with shadow DOM', () => { - const el = document.createElement('x-foo'); - - // here is a slot - const slot = document.createElement('span'); - slot.textContent = 'here is a slot'; - slot.slot = 'text'; - el.appendChild(slot); - - //
no slot
- const noSlot = document.createElement('div'); - noSlot.textContent = 'no slot'; - el.appendChild(noSlot); - el.appendChild(slot); - - root.appendChild(el); - assert.equal( - root.innerHTML, - '
no slot
here is a slot
' - ); - - const shadowHTML = document.querySelector('x-foo').shadowRoot.innerHTML; - assert.equal( - shadowHTML, - '
no slot
here is a slot
' - ); - }); - - const kebabName = 'custom-date-long-name'; - const camelName = 'customDateLongName'; - const lowerName = camelName.toLowerCase(); - function PropNameTransform(props) { - return ( - - {props[kebabName]} {props[lowerName]} {props[camelName]} - - ); - } - 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, - `11/11/2011 pretended to be camel 11/11/2011` - ); - - el.setAttribute(kebabName, '01/01/2001'); - assert.equal( - root.innerHTML, - `01/01/2001 pretended to be camel 01/01/2001` - ); - }); - - const Theme = createContext('light'); - - function DisplayTheme() { - const theme = useContext(Theme); - return

Active theme: {theme}

; - } - - registerElement(DisplayTheme, 'x-display-theme', [], { shadow: true }); - - function Parent({ children, theme = 'dark' }) { - return ( - -
{children}
-
- ); - } - - registerElement(Parent, 'x-parent', ['theme'], { shadow: true }); - - it('passes context over custom element boundaries', () => { - const el = document.createElement('x-parent'); - - const noSlot = document.createElement('x-display-theme'); - el.appendChild(noSlot); - - root.appendChild(el); - assert.equal( - root.innerHTML, - '' - ); - - const getShadowHTML = () => - document.querySelector('x-display-theme').shadowRoot.innerHTML; - assert.equal(getShadowHTML(), '

Active theme: dark

'); - - // Trigger context update - act(() => { - el.setAttribute('theme', 'sunny'); - }); - assert.equal(getShadowHTML(), '

Active theme: sunny

'); - }); - - it('supports controlling light DOM children', () => { - function LightDomChildren({ children }) { - return ( - -

Light DOM Children

-
{children}
-
- ); - } - - registerElement(LightDomChildren, 'light-dom-children', []); - registerElement(LightDomChildren, 'light-dom-children-shadow-false', [], { - shadow: false, - }); - - root.appendChild(createTestElement('light-dom-children')); - root.appendChild(createTestElement('light-dom-children-shadow-false')); - - assert.equal( - document.querySelector('light-dom-children').innerHTML, - '

Light DOM Children

Child 1

Child 2

' - ); - assert.equal( - document.querySelector('light-dom-children-shadow-false').innerHTML, - '

Light DOM Children

Child 1

Child 2

' - ); - }); - - it('supports controlling shadow DOM children', () => { - function ShadowDomChildren({ children }) { - return ( - -

Light DOM Children

-
{children}
-
- ); - } - - registerElement(ShadowDomChildren, 'shadow-dom-children', [], { - shadow: true, - }); - - root.appendChild(createTestElement('shadow-dom-children')); - - assert.equal( - document.querySelector('shadow-dom-children').shadowRoot.innerHTML, - '

Light DOM Children

Child 1

Child 2

' - ); - }); -}); diff --git a/test/types.test.tsx b/test/types/index.test.tsx similarity index 91% rename from test/types.test.tsx rename to test/types/index.test.tsx index 775e47e..383a841 100644 --- a/test/types.test.tsx +++ b/test/types/index.test.tsx @@ -1,5 +1,5 @@ import { h } from 'preact'; -import registerElement from '../src/index'; +import registerElement from '../../src/index'; interface AppProps { name: string; diff --git a/test/tsconfig.json b/test/types/tsconfig.json similarity index 87% rename from test/tsconfig.json rename to test/types/tsconfig.json index 840cf0e..6f3c645 100644 --- a/test/tsconfig.json +++ b/test/types/tsconfig.json @@ -9,5 +9,4 @@ "jsxFactory": "h", "jsxFragmentFactory": "Fragment", }, - "include": ["./types.test.tsx"], } From 0fbc21636b200ddfe575f3b1f27bf8167b9306fd Mon Sep 17 00:00:00 2001 From: Ryan Christian Date: Wed, 29 Oct 2025 22:06:14 -0500 Subject: [PATCH 4/4] test: Finish up refactor --- test/browser/index.test.jsx | 116 +++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 54 deletions(-) diff --git a/test/browser/index.test.jsx b/test/browser/index.test.jsx index 1a8c002..494900b 100644 --- a/test/browser/index.test.jsx +++ b/test/browser/index.test.jsx @@ -4,18 +4,6 @@ import { useContext } from 'preact/hooks'; import { act } from 'preact/test-utils'; import registerElement from '../../src/index'; -/** @param {string} name */ -function createTestElement(name) { - const el = document.createElement(name); - const child1 = document.createElement('p'); - child1.textContent = 'Child 1'; - const child2 = document.createElement('p'); - child2.textContent = 'Child 2'; - el.appendChild(child1); - el.appendChild(child2); - return el; -} - describe('web components', () => { /** @type {HTMLDivElement} */ let root; @@ -29,13 +17,13 @@ describe('web components', () => { document.body.removeChild(root); }); - function Clock({ time }) { - return {time}; - } + it('renders ok, updates on attr change', () => { + function Clock({ time }) { + return {time}; + } - registerElement(Clock, 'x-clock', ['time']); + registerElement(Clock, 'x-clock', ['time']); - it('renders ok, updates on attr change', () => { const el = document.createElement('x-clock'); el.setAttribute('time', '10:28:57 PM'); @@ -52,14 +40,14 @@ describe('web components', () => { ); }); - function NullProps({ size = 'md' }) { - return
{size.toUpperCase()}
; - } - - registerElement(NullProps, 'x-null-props', ['size'], { shadow: true }); - // #50 it('remove attributes without crashing', () => { + function NullProps({ size = 'md' }) { + return
{size.toUpperCase()}
; + } + + registerElement(NullProps, 'x-null-props', ['size'], { shadow: true }); + const el = document.createElement('x-null-props'); assert.doesNotThrow(() => (el.size = 'foo')); root.appendChild(el); @@ -69,7 +57,13 @@ describe('web components', () => { describe('DOM properties', () => { it('passes property changes to props', () => { - const el = document.createElement('x-clock'); + function Clock({ time }) { + return {time}; + } + + registerElement(Clock, 'x-clock-props', ['time']); + + const el = document.createElement('x-clock-props'); el.time = '10:28:57 PM'; assert.equal(el.time, '10:28:57 PM'); @@ -77,7 +71,7 @@ describe('web components', () => { root.appendChild(el); assert.equal( root.innerHTML, - '10:28:57 PM' + '10:28:57 PM' ); el.time = '11:01:10 AM'; @@ -85,7 +79,7 @@ describe('web components', () => { assert.equal( root.innerHTML, - '11:01:10 AM' + '11:01:10 AM' ); }); @@ -210,40 +204,54 @@ describe('web components', () => { ); }); - const kebabName = 'custom-date-long-name'; - const camelName = 'customDateLongName'; - const lowerName = camelName.toLowerCase(); - function PropNameTransform(props) { - return ( - - {props[kebabName]} {props[lowerName]} {props[camelName]} - - ); - } - registerElement(PropNameTransform, 'x-prop-name-transform', [ - kebabName, - camelName, - ]); + describe('attribute/property name transformation', () => { + const kebabName = 'custom-date-long-name'; + const camelName = 'customDateLongName'; + const lowerName = camelName.toLowerCase(); + function PropNameTransform(props) { + return ( + + {props[kebabName]} {props[lowerName]} {props[camelName]} + + ); + } + 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'); + 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, - `11/11/2011 pretended to be camel 11/11/2011` - ); + root.appendChild(el); + assert.equal( + root.innerHTML, + `11/11/2011 pretended to be camel 11/11/2011` + ); - el.setAttribute(kebabName, '01/01/2001'); - assert.equal( - root.innerHTML, - `01/01/2001 pretended to be camel 01/01/2001` - ); + el.setAttribute(kebabName, '01/01/2001'); + assert.equal( + root.innerHTML, + `01/01/2001 pretended to be camel 01/01/2001` + ); + }); }); describe('children', () => { + /** @param {string} name */ + function createTestElement(name) { + const el = document.createElement(name); + const child1 = document.createElement('p'); + child1.textContent = 'Child 1'; + const child2 = document.createElement('p'); + child2.textContent = 'Child 2'; + el.appendChild(child1); + el.appendChild(child2); + return el; + } + it('supports controlling light DOM children', () => { function LightDomChildren({ children }) { return (