From 5be9bc4d58af92d88674894edb228cca6664263f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:00:28 +0000 Subject: [PATCH 1/7] Initial plan From 020699bad7f805e274f59822eba722eb992793b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:03:09 +0000 Subject: [PATCH 2/7] Add comprehensive tests for defineComponent() API Co-authored-by: JosunLP <20913954+JosunLP@users.noreply.github.com> --- tests/component.test.ts | 166 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 1 deletion(-) diff --git a/tests/component.test.ts b/tests/component.test.ts index 89937e23..31c4c819 100644 --- a/tests/component.test.ts +++ b/tests/component.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'bun:test'; -import { component, html } from '../src/component/index'; +import { component, defineComponent, html } from '../src/component/index'; describe('component/html', () => { it('creates HTML from template literal', () => { @@ -598,3 +598,167 @@ describe('component/component', () => { el.remove(); }); }); + +describe('component/defineComponent', () => { + it('returns an HTMLElement subclass', () => { + const tagName = `test-define-class-${Date.now()}`; + const ElementClass = defineComponent(tagName, { + props: {}, + render: () => html`
Test
`, + }); + + expect(ElementClass).toBeDefined(); + expect(typeof ElementClass).toBe('function'); + expect(ElementClass.prototype instanceof HTMLElement).toBe(true); + }); + + it('returned class can be registered with custom tag name', () => { + const originalTagName = `test-define-original-${Date.now()}`; + const customTagName = `test-define-custom-${Date.now()}`; + + const ElementClass = defineComponent(originalTagName, { + props: {}, + render: () => html`
Test
`, + }); + + // Register with a different tag name + customElements.define(customTagName, ElementClass); + + expect(customElements.get(customTagName)).toBe(ElementClass); + + const el = document.createElement(customTagName); + document.body.appendChild(el); + + expect(el.shadowRoot).toBeDefined(); + expect(el.shadowRoot?.innerHTML).toContain('Test'); + + el.remove(); + }); + + it('returned class has correct observedAttributes', () => { + const tagName = `test-define-observed-${Date.now()}`; + const ElementClass = defineComponent(tagName, { + props: { + name: { type: String, required: true }, + count: { type: Number, default: 0 }, + active: { type: Boolean, default: false }, + }, + render: () => html`
Test
`, + }); + + const observedAttrs = (ElementClass as typeof HTMLElement & { observedAttributes: string[] }).observedAttributes; + expect(observedAttrs).toBeDefined(); + expect(observedAttrs).toContain('name'); + expect(observedAttrs).toContain('count'); + expect(observedAttrs).toContain('active'); + }); + + it('instances have shadow DOM', () => { + const tagName = `test-define-shadow-${Date.now()}`; + const ElementClass = defineComponent(tagName, { + props: {}, + render: () => html`
Shadow Content
`, + }); + + customElements.define(tagName, ElementClass); + const el = document.createElement(tagName); + document.body.appendChild(el); + + expect(el.shadowRoot).toBeDefined(); + expect(el.shadowRoot?.mode).toBe('open'); + expect(el.shadowRoot?.innerHTML).toContain('Shadow Content'); + + el.remove(); + }); + + it('attributeChangedCallback triggers re-render', () => { + const tagName = `test-define-attr-change-${Date.now()}`; + let renderCount = 0; + + const ElementClass = defineComponent<{ count: number }>(tagName, { + props: { + count: { type: Number, default: 0 }, + }, + render: ({ props }) => { + renderCount++; + return html`
Count: ${props.count}
`; + }, + }); + + customElements.define(tagName, ElementClass); + const el = document.createElement(tagName); + document.body.appendChild(el); + + expect(renderCount).toBe(1); + expect(el.shadowRoot?.innerHTML).toContain('Count: 0'); + + // Trigger attribute change + el.setAttribute('count', '42'); + + expect(renderCount).toBe(2); + expect(el.shadowRoot?.innerHTML).toContain('Count: 42'); + + el.remove(); + }); + + it('instances sanitize rendered markup for security', () => { + const tagName = `test-define-sanitize-${Date.now()}`; + const ElementClass = defineComponent(tagName, { + props: {}, + render: () => html`
Safe text
`, + }); + + customElements.define(tagName, ElementClass); + const el = document.createElement(tagName); + document.body.appendChild(el); + + const shadowHTML = el.shadowRoot?.innerHTML ?? ''; + // Script tags should be stripped by sanitizeHtml + expect(shadowHTML).not.toContain('