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('