Skip to content

Commit 2a5524c

Browse files
committed
test(core): add JSX renderer tests — 38 tests (SOP v0.24.3 Step 3)
1 parent a4b3bc1 commit 2a5524c

2 files changed

Lines changed: 323 additions & 0 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* @lessjs/core - JSX renderToDOM tests (v0.24.3).
3+
*
4+
* Uses happy-dom for DOM simulation. Dynamic imports after globalThis setup
5+
* so that renderToDOM's document global is available.
6+
*/
7+
8+
import { assert, assertEquals } from 'jsr:@std/assert@1';
9+
import { Window } from 'happy-dom';
10+
11+
// ── DOM setup (must run before dynamic imports) ──────────────────────────
12+
const window = new Window();
13+
Object.assign(globalThis, {
14+
window,
15+
document: window.document,
16+
HTMLElement: window.HTMLElement,
17+
AbortController: window.AbortController,
18+
});
19+
20+
const { jsx, jsxs, Fragment } = await import('../src/jsx-runtime.ts');
21+
const { renderToDOM } = await import('../src/jsx-render-dom.ts');
22+
const { isVNode } = await import('../src/vnode.ts');
23+
const { signal } = await import('../../signals/src/framework.ts');
24+
25+
Deno.test('renderToDOM renders text node', () => {
26+
const node = renderToDOM('hello');
27+
assertEquals(node.nodeType, 3);
28+
assertEquals(node.textContent, 'hello');
29+
});
30+
31+
Deno.test('renderToDOM renders number', () => {
32+
assertEquals(renderToDOM(42).textContent, '42');
33+
});
34+
35+
Deno.test('renderToDOM unwraps Signal children', () => {
36+
const s = signal('hello');
37+
assertEquals(renderToDOM(s).textContent, 'hello');
38+
});
39+
40+
Deno.test('renderToDOM null returns empty text', () => {
41+
assertEquals(renderToDOM(null).textContent, '');
42+
});
43+
44+
Deno.test('renderToDOM creates HTML element', () => {
45+
const vnode = jsx('div', { id: 'test' });
46+
const el = renderToDOM(vnode) as Element;
47+
assertEquals(el.tagName, 'DIV');
48+
assertEquals(el.getAttribute('id'), 'test');
49+
});
50+
51+
Deno.test('renderToDOM element with text child', () => {
52+
const vnode = jsx('span', { children: ['hello'] });
53+
assertEquals((renderToDOM(vnode) as Element).textContent, 'hello');
54+
});
55+
56+
Deno.test('renderToDOM element with Signal child', () => {
57+
const s = signal('world');
58+
const vnode = jsx('span', { children: [s as unknown as string] });
59+
assertEquals((renderToDOM(vnode) as Element).textContent, 'world');
60+
});
61+
62+
Deno.test('renderToDOM SVG namespace', () => {
63+
const vnode = jsx('svg', {
64+
viewBox: '0 0 16 16',
65+
children: [
66+
jsx('circle', { cx: '8', cy: '8', r: '3' }),
67+
],
68+
});
69+
const svg = renderToDOM(vnode) as Element;
70+
assertEquals(svg.namespaceURI, 'http://www.w3.org/2000/svg');
71+
const circle = svg.firstElementChild!;
72+
assertEquals(circle.namespaceURI, 'http://www.w3.org/2000/svg');
73+
});
74+
75+
Deno.test('renderToDOM SVG path namespace', () => {
76+
const vnode = jsx('svg', { children: [jsx('path', { d: 'M0 0 L10 10' })] });
77+
const path = (renderToDOM(vnode) as Element).firstElementChild!;
78+
assertEquals(path.namespaceURI, 'http://www.w3.org/2000/svg');
79+
});
80+
81+
Deno.test('renderToDOM onClick via addEventListener', () => {
82+
let clicked = false;
83+
const vnode = jsx('button', {
84+
onClick: () => {
85+
clicked = true;
86+
},
87+
children: ['Click'],
88+
});
89+
const btn = renderToDOM(vnode) as HTMLButtonElement;
90+
btn.click();
91+
assertEquals(clicked, true);
92+
});
93+
94+
Deno.test('renderToDOM onClick stops after AbortController abort', () => {
95+
const controller = new AbortController();
96+
let count = 0;
97+
const vnode = jsx('button', {
98+
onClick: () => {
99+
count++;
100+
},
101+
children: ['Click'],
102+
});
103+
const btn = renderToDOM(vnode, controller.signal) as HTMLButtonElement;
104+
btn.click();
105+
assertEquals(count, 1);
106+
controller.abort();
107+
btn.click();
108+
assertEquals(count, 1);
109+
});
110+
111+
Deno.test('renderToDOM Fragment returns DocumentFragment', () => {
112+
const vnode = jsxs(Fragment, {
113+
children: [
114+
jsx('span', { children: ['a'] }),
115+
jsx('span', { children: ['b'] }),
116+
],
117+
});
118+
const frag = renderToDOM(vnode);
119+
assertEquals(frag.nodeType, 11);
120+
assertEquals(frag.childNodes.length, 2);
121+
});
122+
123+
Deno.test('renderToDOM Signal in attribute (current behavior)', () => {
124+
// Step 4 (v0.24.3 hardening) will add auto-unwrap for attr values.
125+
// For now, users must explicitly use .value in attributes.
126+
const s = signal('tooltip text');
127+
const vnode = jsx('button', { title: s.value, children: ['Hover'] });
128+
const el = renderToDOM(vnode) as Element;
129+
assertEquals(el.getAttribute('title'), 'tooltip text');
130+
});
131+
132+
Deno.test('renderToDOM className → class', () => {
133+
const vnode = jsx('div', { className: 'foo bar' });
134+
assertEquals((renderToDOM(vnode) as Element).getAttribute('class'), 'foo bar');
135+
});
136+
137+
Deno.test('renderToDOM multiple children', () => {
138+
const vnode = jsx('ul', {
139+
children: [
140+
jsx('li', { children: ['a'] }),
141+
jsx('li', { children: ['b'] }),
142+
jsx('li', { children: ['c'] }),
143+
],
144+
});
145+
assertEquals((renderToDOM(vnode) as Element).children.length, 3);
146+
});
147+
148+
Deno.test('renderToDOM nested SVG children namespace', () => {
149+
const vnode = jsx('svg', {
150+
children: [
151+
jsx('g', { children: [jsx('rect', { width: '10', height: '10' })] }),
152+
],
153+
});
154+
const svg = renderToDOM(vnode) as Element;
155+
const g = svg.firstElementChild!;
156+
assertEquals(g.namespaceURI, 'http://www.w3.org/2000/svg');
157+
assertEquals(g.firstElementChild!.namespaceURI, 'http://www.w3.org/2000/svg');
158+
});
159+
160+
Deno.test('renderToDOM isVNode type guard', () => {
161+
assert(isVNode(jsx('div', {})));
162+
assert(!isVNode('string'));
163+
assert(!isVNode(null));
164+
});
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/**
2+
* @lessjs/core - JSX renderToString tests.
3+
*
4+
* v0.24.3: Direct tests for SSR renderer — Signal unwrap, SVG attrs,
5+
* boolean attrs, style serialisation, event exclusion.
6+
*/
7+
8+
import { assertEquals, assertStringIncludes } from 'jsr:@std/assert@1';
9+
import { Fragment, jsx, jsxs } from '../src/jsx-runtime.ts';
10+
import { renderToString } from '../src/jsx-render-string.ts';
11+
import { signal } from '@lessjs/signals';
12+
13+
Deno.test('renderToString renders text', () => {
14+
assertEquals(renderToString('hello'), 'hello');
15+
});
16+
17+
Deno.test('renderToString renders number', () => {
18+
assertEquals(renderToString(42), '42');
19+
});
20+
21+
Deno.test('renderToString renders null as empty', () => {
22+
assertEquals(renderToString(null), '');
23+
});
24+
25+
Deno.test('renderToString unwraps Signal children', () => {
26+
const s = signal('world');
27+
assertEquals(renderToString(s), 'world');
28+
});
29+
30+
Deno.test('renderToString basic element', () => {
31+
const vnode = jsx('div', { id: 'x' });
32+
assertEquals(renderToString(vnode), '<div id="x"></div>');
33+
});
34+
35+
Deno.test('renderToString element with text child', () => {
36+
const vnode = jsx('p', { children: ['hello'] });
37+
assertEquals(renderToString(vnode), '<p>hello</p>');
38+
});
39+
40+
Deno.test('renderToString element with Signal child', () => {
41+
const s = signal('dynamic');
42+
const vnode = jsx('span', { children: [s] });
43+
assertEquals(renderToString(vnode), '<span>dynamic</span>');
44+
});
45+
46+
Deno.test('renderToString excludes onClick handler', () => {
47+
const vnode = jsx('button', { onClick: () => {}, children: ['Click'] });
48+
const html = renderToString(vnode);
49+
assertStringIncludes(html, '<button>');
50+
// onClick must not appear in HTML
51+
assertEquals(html.includes('onClick'), false);
52+
assertEquals(html.includes('onclick'), false);
53+
});
54+
55+
Deno.test('renderToString className maps to class', () => {
56+
const vnode = jsx('div', { className: 'foo bar' });
57+
assertEquals(renderToString(vnode), '<div class="foo bar"></div>');
58+
});
59+
60+
Deno.test('renderToString htmlFor maps to for', () => {
61+
const vnode = jsx('label', { htmlFor: 'input-id', children: ['Label'] });
62+
assertEquals(renderToString(vnode), '<label for="input-id">Label</label>');
63+
});
64+
65+
Deno.test('renderToString boolean true emits attribute', () => {
66+
const vnode = jsx('input', { disabled: true });
67+
assertEquals(renderToString(vnode), '<input disabled>');
68+
});
69+
70+
Deno.test('renderToString boolean false omits attribute', () => {
71+
const vnode = jsx('input', { disabled: false });
72+
assertEquals(renderToString(vnode), '<input>');
73+
});
74+
75+
Deno.test('renderToString style object serialises', () => {
76+
const vnode = jsx('div', { style: { color: 'red', fontSize: '16px' } });
77+
const html = renderToString(vnode);
78+
assertStringIncludes(html, 'style=');
79+
assertStringIncludes(html, 'color: red');
80+
assertStringIncludes(html, 'font-size: 16px');
81+
});
82+
83+
Deno.test('renderToString SVG attributes preserved', () => {
84+
const vnode = jsx('svg', {
85+
viewBox: '0 0 16 16',
86+
fill: 'none',
87+
children: [
88+
jsx('circle', { cx: '8', cy: '8', r: '3' }),
89+
],
90+
});
91+
const html = renderToString(vnode);
92+
assertStringIncludes(html, 'viewBox="0 0 16 16"');
93+
assertStringIncludes(html, '<circle');
94+
});
95+
96+
Deno.test('renderToString SVG stroke-width attribute', () => {
97+
const vnode = jsx('svg', {
98+
children: [
99+
jsx('line', { 'stroke-width': '1.5', x1: '0', y1: '0', x2: '10', y2: '10' }),
100+
],
101+
});
102+
const html = renderToString(vnode);
103+
assertStringIncludes(html, 'stroke-width="1.5"');
104+
});
105+
106+
Deno.test('renderToString Fragment concatenates children', () => {
107+
const vnode = jsxs(Fragment, {
108+
children: [
109+
jsx('span', { children: ['a'] }),
110+
jsx('span', { children: ['b'] }),
111+
],
112+
});
113+
assertEquals(renderToString(vnode), '<span>a</span><span>b</span>');
114+
});
115+
116+
Deno.test('renderToString void elements self-close', () => {
117+
assertEquals(renderToString(jsx('br', {})), '<br>');
118+
assertEquals(renderToString(jsx('img', { src: 'x.png' })), '<img src="x.png">');
119+
assertEquals(renderToString(jsx('input', { type: 'text' })), '<input type="text">');
120+
});
121+
122+
Deno.test('renderToString Signal attribute (TODO: unwrap hardening Step 4)', () => {
123+
// Step 4 will add Signal auto-unwrap for attributes.
124+
// Currently produces [object Object]; users must use .value explicitly.
125+
const s = signal('tooltip');
126+
const vnode = jsx('button', { title: s.value as unknown as string, children: ['Hover'] });
127+
const html = renderToString(vnode);
128+
assertStringIncludes(html, 'title="tooltip"');
129+
});
130+
131+
Deno.test('renderToString multiple children with signals', () => {
132+
const a = signal('hello');
133+
const b = signal('world');
134+
const vnode = jsx('div', { children: [a, ' ', b] });
135+
assertEquals(renderToString(vnode), '<div>hello world</div>');
136+
});
137+
138+
Deno.test('renderToString nested elements', () => {
139+
const vnode = jsx('ul', {
140+
children: [
141+
jsx('li', { children: ['a'] }),
142+
jsx('li', { children: ['b'] }),
143+
],
144+
});
145+
assertEquals(renderToString(vnode), '<ul><li>a</li><li>b</li></ul>');
146+
});
147+
148+
Deno.test('renderToString ref callback excluded', () => {
149+
let called = false;
150+
const vnode = jsx('div', {
151+
ref: () => {
152+
called = true;
153+
},
154+
});
155+
const html = renderToString(vnode);
156+
assertEquals(html, '<div></div>');
157+
// ref should NOT be called in SSR
158+
assertEquals(called, false);
159+
});

0 commit comments

Comments
 (0)