@dom-expressions/tagged-jsx 0.50.0-next.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/index.d.mts +12544 -0
- package/dist/index.mjs +2 -0
- package/package.json +32 -0
- package/src/index.ts +2 -0
- package/src/parse.ts +257 -0
- package/src/tagged-jsx.ts +247 -0
- package/src/tokenize.ts +260 -0
- package/src/types.ts +69 -0
- package/tests/core.ts +27 -0
- package/tests/parse.test.ts +971 -0
- package/tests/tagged-jsx.test.ts +1119 -0
- package/tests/tokenize.test.ts +1019 -0
- package/tsconfig.json +15 -0
- package/tsdown.config.mjs +5 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,1119 @@
|
|
|
1
|
+
import { createRoot, createSignal, flush, createMemo } from "@solidjs/signals";
|
|
2
|
+
import { createTaggedJSXRuntime } from "../src";
|
|
3
|
+
import { expect, it, describe, beforeEach } from "vitest";
|
|
4
|
+
import * as r from "../../runtime/src/client";
|
|
5
|
+
import { MathMLElements } from "../../runtime/src/constants";
|
|
6
|
+
import { RawTextElements, VoidElements } from "./core";
|
|
7
|
+
|
|
8
|
+
const For = (props: any) => {
|
|
9
|
+
return createMemo(() => props.each.map((v: any) => props.children(v))) as any;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const Show = (props: any) => {
|
|
13
|
+
return createMemo(() => (props.when ? props.children : null)) as any;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const html = createTaggedJSXRuntime({
|
|
17
|
+
...r,
|
|
18
|
+
VoidElements,
|
|
19
|
+
RawTextElements,
|
|
20
|
+
MathMLElements
|
|
21
|
+
}).define({ For, Show });
|
|
22
|
+
|
|
23
|
+
// tagged JSX returns a scalar when a template resolves to a single node/value, and
|
|
24
|
+
// an array when it resolves to multiple. Tests that need to iterate or spread
|
|
25
|
+
// normalize via this helper.
|
|
26
|
+
const arrify = (v: any): Node[] => (Array.isArray(v) ? v : [v]);
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
document.body.innerHTML = "";
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("Tagged JSX Integration Tests", () => {
|
|
33
|
+
describe("Basic Element Rendering", () => {
|
|
34
|
+
it("renders simple text content", () =>
|
|
35
|
+
createRoot(dispose => {
|
|
36
|
+
const result = html`Hello World!`;
|
|
37
|
+
expect(result).toBe("Hello World!");
|
|
38
|
+
dispose();
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
it("renders simple elements", () =>
|
|
42
|
+
createRoot(dispose => {
|
|
43
|
+
const el = html`<div>Test</div>` as HTMLElement;
|
|
44
|
+
expect(el.tagName).toBe("DIV");
|
|
45
|
+
expect(el.textContent).toBe("Test");
|
|
46
|
+
dispose();
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
it("renders self-closing elements", () =>
|
|
50
|
+
createRoot(dispose => {
|
|
51
|
+
const input = html`<input type="text" />` as HTMLInputElement;
|
|
52
|
+
expect(input.tagName).toBe("INPUT");
|
|
53
|
+
expect(input.type).toBe("text");
|
|
54
|
+
dispose();
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
it("renders nested elements", () =>
|
|
58
|
+
createRoot(dispose => {
|
|
59
|
+
const div = html`<div><span>Nested</span></div>` as HTMLElement;
|
|
60
|
+
const span = div.querySelector("span");
|
|
61
|
+
expect(span?.textContent).toBe("Nested");
|
|
62
|
+
dispose();
|
|
63
|
+
}));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("Attributes and Props", () => {
|
|
67
|
+
it("handles complex attribute names and mixed values", () => {
|
|
68
|
+
const [cls, setCls] = createSignal("active");
|
|
69
|
+
let el!: HTMLElement;
|
|
70
|
+
const dispose = createRoot(d => {
|
|
71
|
+
el =
|
|
72
|
+
html`<div class=${() => `base ${cls()}`} data-test-id="main-div" aria-hidden="false"></div>` as HTMLElement;
|
|
73
|
+
return d;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(el.className).toBe("base active");
|
|
77
|
+
expect(el.getAttribute("data-test-id")).toBe("main-div");
|
|
78
|
+
|
|
79
|
+
setCls("inactive");
|
|
80
|
+
flush();
|
|
81
|
+
|
|
82
|
+
expect(el.className).toBe("base inactive");
|
|
83
|
+
dispose();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("handles boolean attributes correctly", () => {
|
|
87
|
+
const [disabled, setDisabled] = createSignal(true);
|
|
88
|
+
let btn!: HTMLButtonElement;
|
|
89
|
+
const dispose = createRoot(d => {
|
|
90
|
+
btn = html`<button disabled=${disabled} autofocus>Click</button>` as HTMLButtonElement;
|
|
91
|
+
return d;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(btn.disabled).toBe(true);
|
|
95
|
+
expect(btn.hasAttribute("autofocus")).toBe(true);
|
|
96
|
+
|
|
97
|
+
setDisabled(false);
|
|
98
|
+
flush();
|
|
99
|
+
|
|
100
|
+
expect(btn.disabled).toBe(false);
|
|
101
|
+
dispose();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("handles spread props with static before spread", () =>
|
|
105
|
+
createRoot(dispose => {
|
|
106
|
+
const props = { id: "spread", class: "blue", "data-attr": "val" };
|
|
107
|
+
const el = html`<div id="static" class="red" ...${props}></div>` as HTMLElement;
|
|
108
|
+
|
|
109
|
+
expect(el.id).toBe("spread");
|
|
110
|
+
expect(el.className).toBe("blue");
|
|
111
|
+
expect(el.getAttribute("data-attr")).toBe("val");
|
|
112
|
+
dispose();
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
it("handles spread props with static after spread", () =>
|
|
116
|
+
createRoot(dispose => {
|
|
117
|
+
const props = { id: "spread", class: "blue", "data-attr": "val" };
|
|
118
|
+
const el = html`<div ...${props} id="static" class="red"></div>` as HTMLElement;
|
|
119
|
+
|
|
120
|
+
expect(el.id).toBe("static");
|
|
121
|
+
expect(el.className).toBe("red");
|
|
122
|
+
expect(el.getAttribute("data-attr")).toBe("val");
|
|
123
|
+
dispose();
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
it("respects override order with spreads and static attributes", () =>
|
|
127
|
+
createRoot(dispose => {
|
|
128
|
+
const props = { id: "from-spread", "data-info": "hidden" };
|
|
129
|
+
const el = html`<div ...${props} id="final-id"></div>` as HTMLElement;
|
|
130
|
+
|
|
131
|
+
expect(el.id).toBe("final-id");
|
|
132
|
+
expect(el.getAttribute("data-info")).toBe("hidden");
|
|
133
|
+
dispose();
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
it("handles explicit properties and attributes via namespaces", () => {
|
|
137
|
+
const [val, setVal] = createSignal("initial");
|
|
138
|
+
let input!: HTMLInputElement;
|
|
139
|
+
const dispose = createRoot(d => {
|
|
140
|
+
input = html`<input prop:value=${val} title=${"hello"} />` as HTMLInputElement;
|
|
141
|
+
return d;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(input.value).toBe("initial");
|
|
145
|
+
expect(input.getAttribute("title")).toBe("hello");
|
|
146
|
+
|
|
147
|
+
setVal("updated");
|
|
148
|
+
flush();
|
|
149
|
+
|
|
150
|
+
expect(input.value).toBe("updated");
|
|
151
|
+
dispose();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("handles mixed static and dynamic attribute parts", () =>
|
|
155
|
+
createRoot(dispose => {
|
|
156
|
+
const [welcoming] = createSignal("hello");
|
|
157
|
+
const h1 = html`
|
|
158
|
+
<h1 title=${() => `${welcoming()} John ${"Smith"}`}></h1>
|
|
159
|
+
` as HTMLHeadingElement;
|
|
160
|
+
|
|
161
|
+
expect(h1.title).toBe("hello John Smith");
|
|
162
|
+
dispose();
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
it("handles multi-line, complex, and unquoted-style attributes", () =>
|
|
166
|
+
createRoot(dispose => {
|
|
167
|
+
const div = html`
|
|
168
|
+
<div
|
|
169
|
+
multiline="
|
|
170
|
+
foo
|
|
171
|
+
bar
|
|
172
|
+
"
|
|
173
|
+
lorem ipsum
|
|
174
|
+
></div>` as HTMLElement;
|
|
175
|
+
|
|
176
|
+
expect(div.getAttribute("multiline")).toContain("foo");
|
|
177
|
+
expect(div.getAttribute("multiline")).toContain("bar");
|
|
178
|
+
expect(div.hasAttribute("lorem")).toBe(true);
|
|
179
|
+
expect(div.hasAttribute("ipsum")).toBe(true);
|
|
180
|
+
dispose();
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
it("correctly handles JSON-like strings in attributes", () =>
|
|
184
|
+
createRoot(dispose => {
|
|
185
|
+
const el = html`
|
|
186
|
+
<lume-box uniforms='{ "iTime": { "value": 0 } }'></lume-box>
|
|
187
|
+
` as HTMLElement;
|
|
188
|
+
|
|
189
|
+
expect(el.getAttribute("uniforms")).toBe('{ "iTime": { "value": 0 } }');
|
|
190
|
+
dispose();
|
|
191
|
+
}));
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("Expressions and Reactivity", () => {
|
|
195
|
+
it("handles expressions inside text", () =>
|
|
196
|
+
createRoot(dispose => {
|
|
197
|
+
const name = "World";
|
|
198
|
+
const el = html`<div>Hello ${name}!</div>` as HTMLElement;
|
|
199
|
+
expect(el.textContent).toBe("Hello World!");
|
|
200
|
+
dispose();
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
it("handles multiple expressions", () => {
|
|
204
|
+
const [count, setCount] = createSignal(0);
|
|
205
|
+
let el!: HTMLElement;
|
|
206
|
+
const dispose = createRoot(d => {
|
|
207
|
+
const name = "Counter";
|
|
208
|
+
el = html`<div>${name}: ${count}</div>` as HTMLElement;
|
|
209
|
+
return d;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(el.textContent).toBe("Counter: 0");
|
|
213
|
+
setCount(5);
|
|
214
|
+
flush();
|
|
215
|
+
|
|
216
|
+
expect(el.textContent).toBe("Counter: 5");
|
|
217
|
+
dispose();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("handles top level expressions", () => {
|
|
221
|
+
const [count, setCount] = createSignal(0);
|
|
222
|
+
const dispose = createRoot(d => {
|
|
223
|
+
const nodes = html`<div></div>${() => count()}` as HTMLElement;
|
|
224
|
+
r.insert(document.body, nodes);
|
|
225
|
+
return d;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(document.body.textContent).toContain("0");
|
|
229
|
+
setCount(5);
|
|
230
|
+
flush();
|
|
231
|
+
|
|
232
|
+
expect(document.body.textContent).toContain("5");
|
|
233
|
+
dispose();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("handles sibling expressions and static text correctly", () =>
|
|
237
|
+
createRoot(dispose => {
|
|
238
|
+
const [a] = createSignal("A");
|
|
239
|
+
const [b] = createSignal("B");
|
|
240
|
+
|
|
241
|
+
const el = html`<div>${a} - ${b} !</div>` as HTMLDivElement;
|
|
242
|
+
|
|
243
|
+
expect(el.textContent).toBe("A - B !");
|
|
244
|
+
dispose();
|
|
245
|
+
}));
|
|
246
|
+
|
|
247
|
+
it("trims whitespace correctly while preserving nested spaces", () =>
|
|
248
|
+
createRoot(dispose => {
|
|
249
|
+
const name = "John";
|
|
250
|
+
const div = html`
|
|
251
|
+
<div>
|
|
252
|
+
<b>Hello, my name is: <i> ${name}</i></b>
|
|
253
|
+
</div>
|
|
254
|
+
` as HTMLElement;
|
|
255
|
+
|
|
256
|
+
const b = div.querySelector("b")!;
|
|
257
|
+
expect(b.textContent).toBe("Hello, my name is: John");
|
|
258
|
+
dispose();
|
|
259
|
+
}));
|
|
260
|
+
|
|
261
|
+
it("handles dynamic attributes", () => {
|
|
262
|
+
const [visible, setVisible] = createSignal(true);
|
|
263
|
+
let el!: HTMLElement;
|
|
264
|
+
const dispose = createRoot(d => {
|
|
265
|
+
el = html`<div hidden=${!visible}>Content</div>` as HTMLElement;
|
|
266
|
+
return d;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(el.hasAttribute("hidden")).toBe(false);
|
|
270
|
+
setVisible(false);
|
|
271
|
+
flush();
|
|
272
|
+
|
|
273
|
+
expect(el.hasAttribute("hidden")).toBe(false);
|
|
274
|
+
dispose();
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe("Logic and Control Flow", () => {
|
|
279
|
+
it("works with Solid's <Show> component", () => {
|
|
280
|
+
const [visible, setVisible] = createSignal(false);
|
|
281
|
+
const container = document.createElement("div");
|
|
282
|
+
const dispose = createRoot(d => {
|
|
283
|
+
const result = html`<div>
|
|
284
|
+
<Show when=${visible}>
|
|
285
|
+
<span id="target">I am visible</span>
|
|
286
|
+
</Show>
|
|
287
|
+
</div>`;
|
|
288
|
+
container.append(...arrify(result));
|
|
289
|
+
document.body.appendChild(container);
|
|
290
|
+
return d;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(container.querySelector("#target")).toBeNull();
|
|
294
|
+
|
|
295
|
+
setVisible(true);
|
|
296
|
+
flush();
|
|
297
|
+
|
|
298
|
+
expect(container.querySelector("#target")).not.toBeNull();
|
|
299
|
+
expect(container.querySelector("#target")?.textContent).toBe("I am visible");
|
|
300
|
+
dispose();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("works with Solid's <For> component", () => {
|
|
304
|
+
const [items, setItems] = createSignal(["A", "B"]);
|
|
305
|
+
const container = document.createElement("div");
|
|
306
|
+
const dispose = createRoot(d => {
|
|
307
|
+
const result = html`
|
|
308
|
+
<ul>
|
|
309
|
+
<For each=${items}>
|
|
310
|
+
${(item: string) => html`<li>${item}</li>`}
|
|
311
|
+
</For>
|
|
312
|
+
</ul>`;
|
|
313
|
+
container.append(...arrify(result));
|
|
314
|
+
return d;
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(container.querySelectorAll("li").length).toBe(2);
|
|
318
|
+
|
|
319
|
+
setItems(["A", "B", "C"]);
|
|
320
|
+
flush();
|
|
321
|
+
|
|
322
|
+
expect(container.querySelectorAll("li").length).toBe(3);
|
|
323
|
+
dispose();
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe("Events and Refs", () => {
|
|
328
|
+
it("handles refs and event listeners correctly", () =>
|
|
329
|
+
createRoot(dispose => {
|
|
330
|
+
let elementRef: HTMLDivElement | undefined;
|
|
331
|
+
let clickCount = 0;
|
|
332
|
+
const ref = (el: HTMLDivElement) => {
|
|
333
|
+
elementRef = el;
|
|
334
|
+
el.addEventListener("click", () => clickCount++);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const el = html`
|
|
338
|
+
<div
|
|
339
|
+
ref=${ref}
|
|
340
|
+
>Click me</div>` as HTMLDivElement;
|
|
341
|
+
|
|
342
|
+
expect(elementRef).toBe(el);
|
|
343
|
+
el.click();
|
|
344
|
+
expect(clickCount).toBe(1);
|
|
345
|
+
dispose();
|
|
346
|
+
}));
|
|
347
|
+
|
|
348
|
+
it("integrates ref listeners and delegated events", () => {
|
|
349
|
+
const exec = { first: false, delegated: false, second: false };
|
|
350
|
+
const container = document.createElement("div");
|
|
351
|
+
document.body.append(container);
|
|
352
|
+
const dispose = r.render(
|
|
353
|
+
() =>
|
|
354
|
+
html`
|
|
355
|
+
<div id="main">
|
|
356
|
+
<button ref=${(node: HTMLButtonElement) =>
|
|
357
|
+
node.addEventListener("click", () => (exec.first = true))}>Bound</button>
|
|
358
|
+
<button onClick=${[(v: any) => (exec.delegated = v), true]}>Delegated</button>
|
|
359
|
+
<button ref=${(node: HTMLButtonElement) =>
|
|
360
|
+
node.addEventListener("click", () => (exec.second = true), {
|
|
361
|
+
capture: true
|
|
362
|
+
})}>Ref Listener</button>
|
|
363
|
+
</div>
|
|
364
|
+
`,
|
|
365
|
+
container
|
|
366
|
+
);
|
|
367
|
+
const el = container.firstElementChild as HTMLElement;
|
|
368
|
+
|
|
369
|
+
const [btn1, btn2, btn3] = el.querySelectorAll("button");
|
|
370
|
+
|
|
371
|
+
expect(btn1.textContent).toBe("Bound");
|
|
372
|
+
expect(btn2.textContent).toBe("Delegated");
|
|
373
|
+
expect(btn3.textContent).toBe("Ref Listener");
|
|
374
|
+
|
|
375
|
+
btn1.click();
|
|
376
|
+
btn2.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
377
|
+
btn3.click();
|
|
378
|
+
|
|
379
|
+
expect(exec.first).toBe(true);
|
|
380
|
+
expect(exec.delegated).toBe(true);
|
|
381
|
+
expect(exec.second).toBe(true);
|
|
382
|
+
dispose();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("captures refs across components and elements", () =>
|
|
386
|
+
createRoot(dispose => {
|
|
387
|
+
let linkRef: HTMLAnchorElement | undefined;
|
|
388
|
+
const div = html`
|
|
389
|
+
<div>
|
|
390
|
+
<a href="/" ref=${(el: HTMLAnchorElement) => (linkRef = el)}>Link</a>
|
|
391
|
+
</div>
|
|
392
|
+
` as HTMLElement;
|
|
393
|
+
|
|
394
|
+
expect(linkRef).toBe(div.querySelector("a"));
|
|
395
|
+
dispose();
|
|
396
|
+
}));
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe("Custom Components", () => {
|
|
400
|
+
it("passes children correctly to registered components", () =>
|
|
401
|
+
createRoot(dispose => {
|
|
402
|
+
const Wrapper = (props: { children: any }) => html`<section>${props.children}</section>`;
|
|
403
|
+
const localHtml = html.define({ Wrapper });
|
|
404
|
+
|
|
405
|
+
const section = localHtml`
|
|
406
|
+
<Wrapper>
|
|
407
|
+
<span>Inside</span>
|
|
408
|
+
</Wrapper>` as HTMLElement;
|
|
409
|
+
|
|
410
|
+
expect(section.tagName).toBe("SECTION");
|
|
411
|
+
expect(section.querySelector("span")?.textContent).toBe("Inside");
|
|
412
|
+
dispose();
|
|
413
|
+
}));
|
|
414
|
+
|
|
415
|
+
it("handles deep nesting with custom components", () =>
|
|
416
|
+
createRoot(dispose => {
|
|
417
|
+
const Box = (props: any) => html`<div class="box">${props.children}</div>`;
|
|
418
|
+
const localHtml = html.define({ Box });
|
|
419
|
+
|
|
420
|
+
const result = localHtml`
|
|
421
|
+
<Box>
|
|
422
|
+
<ul>
|
|
423
|
+
<li><Box>Item 1</Box></li>
|
|
424
|
+
<li><Box>Item 2</Box></li>
|
|
425
|
+
</ul>
|
|
426
|
+
</Box>`;
|
|
427
|
+
|
|
428
|
+
const container = document.createElement("div");
|
|
429
|
+
container.append(...arrify(result));
|
|
430
|
+
expect(container.querySelectorAll(".box").length).toBe(3);
|
|
431
|
+
expect(container.querySelector("li")?.textContent).toBe("Item 1");
|
|
432
|
+
dispose();
|
|
433
|
+
}));
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe("Special Elements and Namespaces", () => {
|
|
437
|
+
it("treats <script> and <style> as raw text", () =>
|
|
438
|
+
createRoot(dispose => {
|
|
439
|
+
const style = html`
|
|
440
|
+
<style>
|
|
441
|
+
body > div { color: red; }
|
|
442
|
+
</style>` as HTMLStyleElement;
|
|
443
|
+
|
|
444
|
+
expect(style.textContent).toContain("body > div");
|
|
445
|
+
expect(style.tagName).toBe("STYLE");
|
|
446
|
+
dispose();
|
|
447
|
+
}));
|
|
448
|
+
|
|
449
|
+
it("maintains SVG namespace across nested dynamic paths", () => {
|
|
450
|
+
const [radius, setRadius] = createSignal(10);
|
|
451
|
+
let circle!: SVGCircleElement;
|
|
452
|
+
const dispose = createRoot(d => {
|
|
453
|
+
const svg = html`
|
|
454
|
+
<svg>
|
|
455
|
+
<g>
|
|
456
|
+
<circle r=${radius} />
|
|
457
|
+
</g>
|
|
458
|
+
</svg>` as SVGSVGElement;
|
|
459
|
+
|
|
460
|
+
circle = svg.querySelector("circle")!;
|
|
461
|
+
return d;
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
expect(circle.namespaceURI).toBe("http://www.w3.org/2000/svg");
|
|
465
|
+
expect(circle.getAttribute("r")).toBe("10");
|
|
466
|
+
|
|
467
|
+
setRadius(20);
|
|
468
|
+
flush();
|
|
469
|
+
expect(circle.getAttribute("r")).toBe("20");
|
|
470
|
+
dispose();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("handles template elements correctly", () =>
|
|
474
|
+
createRoot(dispose => {
|
|
475
|
+
const nodes = html`${"hole"}<template>Count: ${() => 1}</template>` as Node[];
|
|
476
|
+
document.body.append(...nodes);
|
|
477
|
+
expect((nodes[1] as HTMLTemplateElement).content.textContent).toEqual("Count: 1");
|
|
478
|
+
dispose();
|
|
479
|
+
}));
|
|
480
|
+
|
|
481
|
+
it("handles mathml elements", () =>
|
|
482
|
+
createRoot(dispose => {
|
|
483
|
+
const Frac = () => html`<mfrac>
|
|
484
|
+
<mn>1</mn>
|
|
485
|
+
<mn>3</mn>
|
|
486
|
+
</mfrac>`;
|
|
487
|
+
|
|
488
|
+
const p = html.define({ Frac }).jsx`<p>
|
|
489
|
+
The fraction
|
|
490
|
+
<math>
|
|
491
|
+
<Frac />
|
|
492
|
+
</math>
|
|
493
|
+
is not a decimal number.
|
|
494
|
+
</p>` as HTMLElement;
|
|
495
|
+
|
|
496
|
+
document.body.append(p);
|
|
497
|
+
expect(document.querySelector("math")).toBeTruthy();
|
|
498
|
+
dispose();
|
|
499
|
+
}));
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
describe("HTML Entities and Encoding", () => {
|
|
503
|
+
it("handles html encodings", () =>
|
|
504
|
+
createRoot(dispose => {
|
|
505
|
+
const elem = html`©<span>></span>` as Node[];
|
|
506
|
+
|
|
507
|
+
expect(elem[0]).toEqual("\u00A9");
|
|
508
|
+
expect(elem[1].textContent).toEqual(">");
|
|
509
|
+
dispose();
|
|
510
|
+
}));
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
describe("Edge Cases and Error Handling", () => {
|
|
514
|
+
it("handles empty templates", () =>
|
|
515
|
+
createRoot(dispose => {
|
|
516
|
+
const result = html``;
|
|
517
|
+
expect(result).toEqual([]);
|
|
518
|
+
dispose();
|
|
519
|
+
}));
|
|
520
|
+
|
|
521
|
+
it("handles whitespace-only templates", () =>
|
|
522
|
+
createRoot(dispose => {
|
|
523
|
+
const result = html` `;
|
|
524
|
+
expect(result).toBe(" ");
|
|
525
|
+
dispose();
|
|
526
|
+
}));
|
|
527
|
+
|
|
528
|
+
it("handles weird whitespace and line breaks in tags", () =>
|
|
529
|
+
createRoot(dispose => {
|
|
530
|
+
const el = html`
|
|
531
|
+
<div
|
|
532
|
+
id="test
|
|
533
|
+
"
|
|
534
|
+
class =
|
|
535
|
+
"spaced"
|
|
536
|
+
> Text </div>` as HTMLElement;
|
|
537
|
+
expect(el.id).toBe(`test
|
|
538
|
+
`);
|
|
539
|
+
expect(el.className).toBe("spaced");
|
|
540
|
+
expect(el.textContent?.trim()).toBe("Text");
|
|
541
|
+
dispose();
|
|
542
|
+
}));
|
|
543
|
+
|
|
544
|
+
it("throws for an unregistered capitalized component", () =>
|
|
545
|
+
createRoot(dispose => {
|
|
546
|
+
expect(() => html`<UnregisteredComponent />`).toThrow(/not found in registry/);
|
|
547
|
+
dispose();
|
|
548
|
+
}));
|
|
549
|
+
|
|
550
|
+
it("throws for spread of a non-object value", () =>
|
|
551
|
+
createRoot(dispose => {
|
|
552
|
+
const invalid = 42;
|
|
553
|
+
expect(() => html`<div ...${invalid}></div>`).toThrow(/Can only spread objects/);
|
|
554
|
+
dispose();
|
|
555
|
+
}));
|
|
556
|
+
|
|
557
|
+
it("throws for dynamic component that is not a function", () =>
|
|
558
|
+
createRoot(dispose => {
|
|
559
|
+
const notAComponent = "not-a-function";
|
|
560
|
+
expect(() => html`<${notAComponent} />`).toThrowError(/not found in registry/);
|
|
561
|
+
dispose();
|
|
562
|
+
}));
|
|
563
|
+
|
|
564
|
+
it("ignores HTML comments and their contents", () =>
|
|
565
|
+
createRoot(dispose => {
|
|
566
|
+
const signal = () => "HIDDEN";
|
|
567
|
+
const div = html`
|
|
568
|
+
<div>
|
|
569
|
+
<!-- This is a comment with an expression: ${signal()} -->
|
|
570
|
+
<p>Visible</p>
|
|
571
|
+
</div>` as HTMLElement;
|
|
572
|
+
|
|
573
|
+
const container = document.createElement("div");
|
|
574
|
+
container.append(div);
|
|
575
|
+
expect(container.innerHTML).not.toContain("HIDDEN");
|
|
576
|
+
expect(container.querySelector("p")?.textContent).toBe("Visible");
|
|
577
|
+
dispose();
|
|
578
|
+
}));
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
describe("Complex DOM Integration Tests", () => {
|
|
582
|
+
describe("Advanced Attribute Scenarios", () => {
|
|
583
|
+
it("handles multiple spread attributes with complex override behavior", () =>
|
|
584
|
+
createRoot(dispose => {
|
|
585
|
+
const baseProps = { class: "base", id: "base-id" };
|
|
586
|
+
const overrideProps = { class: "override", "data-override": true };
|
|
587
|
+
const finalId = "final-id";
|
|
588
|
+
|
|
589
|
+
const el =
|
|
590
|
+
html`<div ...${baseProps} ...${overrideProps} id=${finalId} class="final">Content</div>` as HTMLElement;
|
|
591
|
+
|
|
592
|
+
expect(el.className).toBe("final");
|
|
593
|
+
expect(el.id).toBe("final-id");
|
|
594
|
+
expect(el.getAttribute("data-override")).toBe("");
|
|
595
|
+
expect(el.textContent).toBe("Content");
|
|
596
|
+
dispose();
|
|
597
|
+
}));
|
|
598
|
+
|
|
599
|
+
it("handles mixed quoted and unquoted attributes with expressions", () => {
|
|
600
|
+
const [dynamicClass, setClass] = createSignal("active");
|
|
601
|
+
const [dynamicId] = createSignal("dynamic-123");
|
|
602
|
+
const staticTitle = "Static Title";
|
|
603
|
+
let button!: HTMLButtonElement;
|
|
604
|
+
const dispose = createRoot(d => {
|
|
605
|
+
button =
|
|
606
|
+
html`<button class=${() => `btn-${dynamicClass()}`} id=${dynamicId} title=${staticTitle} disabled=${dynamicClass() === "active"}>Click</button>` as HTMLButtonElement;
|
|
607
|
+
return d;
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
expect(button.className).toBe("btn-active");
|
|
611
|
+
expect(button.id).toBe("dynamic-123");
|
|
612
|
+
expect(button.title).toBe("Static Title");
|
|
613
|
+
expect(button.disabled).toBe(true);
|
|
614
|
+
expect(button.textContent).toBe("Click");
|
|
615
|
+
|
|
616
|
+
setClass("inactive");
|
|
617
|
+
flush();
|
|
618
|
+
|
|
619
|
+
expect(button.className).toBe("btn-inactive");
|
|
620
|
+
dispose();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it("handles boolean attributes with dynamic expressions", () => {
|
|
624
|
+
const [enabled, setEnabled] = createSignal(true);
|
|
625
|
+
const [checked, setChecked] = createSignal(false);
|
|
626
|
+
let input!: HTMLInputElement;
|
|
627
|
+
const dispose = createRoot(d => {
|
|
628
|
+
input =
|
|
629
|
+
html`<input type="checkbox" disabled=${() => !enabled()} checked=${checked} readonly />` as HTMLInputElement;
|
|
630
|
+
return d;
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
expect(input.disabled).toBe(false);
|
|
634
|
+
expect(input.checked).toBe(false);
|
|
635
|
+
expect(input.hasAttribute("readonly")).toBe(true);
|
|
636
|
+
|
|
637
|
+
setEnabled(false);
|
|
638
|
+
setChecked(true);
|
|
639
|
+
flush();
|
|
640
|
+
|
|
641
|
+
expect(input.disabled).toBe(true);
|
|
642
|
+
expect(input.checked).toBe(true);
|
|
643
|
+
dispose();
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it("handles style attribute with mixed static and dynamic values", () => {
|
|
647
|
+
const [color, setColor] = createSignal("red");
|
|
648
|
+
let el!: HTMLElement;
|
|
649
|
+
const dispose = createRoot(d => {
|
|
650
|
+
el =
|
|
651
|
+
html`<div style=${() => `color: ${color()}; background: blue; padding: ${10}px`}>Styled content</div>` as HTMLElement;
|
|
652
|
+
return d;
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
expect(el.style.color).toBe("red");
|
|
656
|
+
expect(el.style.backgroundColor).toBe("blue");
|
|
657
|
+
expect(el.style.padding).toBe("10px");
|
|
658
|
+
|
|
659
|
+
setColor("green");
|
|
660
|
+
flush();
|
|
661
|
+
|
|
662
|
+
expect(el.style.color).toBe("green");
|
|
663
|
+
dispose();
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("handles class attribute with complex expressions", () => {
|
|
667
|
+
const [isActive, setActive] = createSignal(true);
|
|
668
|
+
const [theme, setTheme] = createSignal("dark");
|
|
669
|
+
let el!: HTMLElement;
|
|
670
|
+
const dispose = createRoot(d => {
|
|
671
|
+
el =
|
|
672
|
+
html`<div class=${() => `${isActive() ? "active" : "inactive"} theme-${theme()} static-class`}>Mixed classes</div>` as HTMLElement;
|
|
673
|
+
return d;
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
expect(el.className).toBe("active theme-dark static-class");
|
|
677
|
+
|
|
678
|
+
setActive(false);
|
|
679
|
+
setTheme("light");
|
|
680
|
+
flush();
|
|
681
|
+
|
|
682
|
+
expect(el.className).toBe("inactive theme-light static-class");
|
|
683
|
+
dispose();
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
describe("Reactive DOM Updates", () => {
|
|
688
|
+
it("updates DOM when multiple signals change simultaneously", () => {
|
|
689
|
+
const [count, setCount] = createSignal(0);
|
|
690
|
+
const [text, setText] = createSignal("Initial");
|
|
691
|
+
const container = document.createElement("div");
|
|
692
|
+
const dispose = createRoot(d => {
|
|
693
|
+
const div = html`<div>
|
|
694
|
+
<span class=${() => `count-${count()}`}>Count: ${count}</span>
|
|
695
|
+
<span class=${() => `text-${text()}`}>Text: ${text}</span>
|
|
696
|
+
</div>` as HTMLElement;
|
|
697
|
+
container.append(div);
|
|
698
|
+
return d;
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
const countSpan = container.querySelector(".count-0")!;
|
|
702
|
+
const textSpan = container.querySelector(".text-Initial")!;
|
|
703
|
+
|
|
704
|
+
expect(countSpan.className).toBe("count-0");
|
|
705
|
+
expect(countSpan.textContent).toBe("Count: 0");
|
|
706
|
+
expect(textSpan.className).toBe("text-Initial");
|
|
707
|
+
expect(textSpan.textContent).toBe("Text: Initial");
|
|
708
|
+
|
|
709
|
+
setCount(5);
|
|
710
|
+
setText("Updated");
|
|
711
|
+
flush();
|
|
712
|
+
|
|
713
|
+
expect(countSpan.className).toBe("count-5");
|
|
714
|
+
expect(countSpan.textContent).toBe("Count: 5");
|
|
715
|
+
expect(textSpan.className).toBe("text-Updated");
|
|
716
|
+
expect(textSpan.textContent).toBe("Text: Updated");
|
|
717
|
+
dispose();
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it("handles conditional attributes with boolean logic", () => {
|
|
721
|
+
const [visible, setVisible] = createSignal(true);
|
|
722
|
+
const [disabled, setDisabled] = createSignal(false);
|
|
723
|
+
let button!: HTMLButtonElement;
|
|
724
|
+
const dispose = createRoot(d => {
|
|
725
|
+
button = html`<button
|
|
726
|
+
hidden=${() => !visible()}
|
|
727
|
+
disabled=${disabled}
|
|
728
|
+
aria-hidden=${() => !visible()}
|
|
729
|
+
class=${() => (visible() ? "visible" : "hidden")}
|
|
730
|
+
>Button</button>` as HTMLButtonElement;
|
|
731
|
+
return d;
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
expect(button.hidden).toBe(false);
|
|
735
|
+
expect(button.disabled).toBe(false);
|
|
736
|
+
expect(button.getAttribute("aria-hidden")).toBe(null);
|
|
737
|
+
expect(button.className).toBe("visible");
|
|
738
|
+
|
|
739
|
+
setVisible(false);
|
|
740
|
+
setDisabled(true);
|
|
741
|
+
flush();
|
|
742
|
+
|
|
743
|
+
expect(button.hidden).toBe(true);
|
|
744
|
+
expect(button.disabled).toBe(true);
|
|
745
|
+
expect(button.getAttribute("aria-hidden")).toBe("");
|
|
746
|
+
expect(button.className).toBe("hidden");
|
|
747
|
+
dispose();
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
describe("Event Handling Integration", () => {
|
|
752
|
+
it("handles multiple event types with proper cleanup", () =>
|
|
753
|
+
createRoot(dispose => {
|
|
754
|
+
let clickCount = 0;
|
|
755
|
+
let mouseOverCount = 0;
|
|
756
|
+
const div = html`<div>
|
|
757
|
+
<button ref=${(node: HTMLButtonElement) =>
|
|
758
|
+
node.addEventListener("click", () => clickCount++)}>Click me</button>
|
|
759
|
+
<span ref=${(node: HTMLSpanElement) =>
|
|
760
|
+
node.addEventListener("mouseover", () => mouseOverCount++)}>Hover me</span>
|
|
761
|
+
</div>` as HTMLElement;
|
|
762
|
+
const container = document.createElement("div");
|
|
763
|
+
container.append(div);
|
|
764
|
+
document.body.append(container);
|
|
765
|
+
|
|
766
|
+
const button = container.querySelector("button")!;
|
|
767
|
+
const hoverDiv = container.querySelector("span")!;
|
|
768
|
+
|
|
769
|
+
button.click();
|
|
770
|
+
hoverDiv.dispatchEvent(new MouseEvent("mouseover"));
|
|
771
|
+
|
|
772
|
+
expect(clickCount).toBe(1);
|
|
773
|
+
expect(mouseOverCount).toBe(1);
|
|
774
|
+
dispose();
|
|
775
|
+
}));
|
|
776
|
+
|
|
777
|
+
it("handles event delegation with stopPropagation", () => {
|
|
778
|
+
const events: string[] = [];
|
|
779
|
+
const container = document.createElement("div");
|
|
780
|
+
document.body.append(container);
|
|
781
|
+
const dispose = r.render(
|
|
782
|
+
() =>
|
|
783
|
+
html`<div onClick=${() => events.push("parent")}>
|
|
784
|
+
<button onClick=${(e: Event) => {
|
|
785
|
+
e.stopPropagation();
|
|
786
|
+
events.push("child");
|
|
787
|
+
}}>Child</button>
|
|
788
|
+
</div>`,
|
|
789
|
+
container
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
const button = container.querySelector("button")!;
|
|
793
|
+
button.click();
|
|
794
|
+
|
|
795
|
+
expect(events.length).toBeGreaterThan(0);
|
|
796
|
+
dispose();
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
describe("Form Element Integration", () => {
|
|
801
|
+
it("handles form controls with reactive values", () => {
|
|
802
|
+
const [value, setValue] = createSignal("initial");
|
|
803
|
+
const [checked, setChecked] = createSignal(false);
|
|
804
|
+
const container = document.createElement("div");
|
|
805
|
+
const dispose = createRoot(d => {
|
|
806
|
+
const form = html`<form>
|
|
807
|
+
<input type="text" value=${value} />
|
|
808
|
+
<input type="checkbox" checked=${checked} />
|
|
809
|
+
<select value=${value}>
|
|
810
|
+
<option value="initial">Option 1</option>
|
|
811
|
+
<option value="updated">Option 2</option>
|
|
812
|
+
</select>
|
|
813
|
+
</form>` as HTMLFormElement;
|
|
814
|
+
container.append(form);
|
|
815
|
+
document.body.append(container);
|
|
816
|
+
return d;
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
const textInput = container.querySelector("input[type='text']") as HTMLInputElement;
|
|
820
|
+
const checkboxInput = container.querySelector("input[type='checkbox']") as HTMLInputElement;
|
|
821
|
+
const select = container.querySelector("select") as HTMLSelectElement;
|
|
822
|
+
|
|
823
|
+
expect(textInput.value).toBe("initial");
|
|
824
|
+
expect(checkboxInput.checked).toBe(false);
|
|
825
|
+
expect(select.value).toBe("initial");
|
|
826
|
+
|
|
827
|
+
setValue("updated");
|
|
828
|
+
setChecked(true);
|
|
829
|
+
flush();
|
|
830
|
+
|
|
831
|
+
expect(textInput.value).toBe("updated");
|
|
832
|
+
expect(checkboxInput.checked).toBe(true);
|
|
833
|
+
expect(select.value).toBe("updated");
|
|
834
|
+
dispose();
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it("handles textarea with raw text content", () => {
|
|
838
|
+
const [content, setContent] = createSignal("Initial content");
|
|
839
|
+
const container = document.createElement("div");
|
|
840
|
+
const dispose = createRoot(d => {
|
|
841
|
+
const textarea = html`<textarea>${content}</textarea>` as HTMLTextAreaElement;
|
|
842
|
+
container.append(textarea);
|
|
843
|
+
return d;
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
const textarea = container.querySelector("textarea") as HTMLTextAreaElement;
|
|
847
|
+
expect(textarea.value).toBe("Initial content");
|
|
848
|
+
|
|
849
|
+
setContent("Updated content");
|
|
850
|
+
flush();
|
|
851
|
+
|
|
852
|
+
expect(textarea.value).toBe("Updated content");
|
|
853
|
+
dispose();
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
describe("List and Table Rendering", () => {
|
|
858
|
+
it("renders table with dynamic rows and cells", () =>
|
|
859
|
+
createRoot(dispose => {
|
|
860
|
+
const [rows] = createSignal([
|
|
861
|
+
["Cell 1", "Cell 2", "Cell 3"],
|
|
862
|
+
["Cell 4", "Cell 5", "Cell 6"]
|
|
863
|
+
]);
|
|
864
|
+
|
|
865
|
+
const table = html`<table>
|
|
866
|
+
<thead>
|
|
867
|
+
<tr><th>Col 1</th><th>Col 2</th><th>Col 3</th></tr>
|
|
868
|
+
</thead>
|
|
869
|
+
<tbody>
|
|
870
|
+
<For each=${rows}>
|
|
871
|
+
${(row: string[]) => html`
|
|
872
|
+
<tr>
|
|
873
|
+
<For each=${row}>
|
|
874
|
+
${(cell: string) => html`<td>${cell}</td>`}
|
|
875
|
+
</For>
|
|
876
|
+
</tr>
|
|
877
|
+
`}
|
|
878
|
+
</For>
|
|
879
|
+
</tbody>
|
|
880
|
+
</table>` as HTMLTableElement;
|
|
881
|
+
const container = document.createElement("div");
|
|
882
|
+
container.append(table);
|
|
883
|
+
|
|
884
|
+
const tbody = table.querySelector("tbody")!;
|
|
885
|
+
const trElements = tbody.querySelectorAll("tr");
|
|
886
|
+
|
|
887
|
+
expect(trElements.length).toBe(2);
|
|
888
|
+
|
|
889
|
+
const firstRowCells = trElements[0].querySelectorAll("td");
|
|
890
|
+
expect(firstRowCells.length).toBe(3);
|
|
891
|
+
expect(firstRowCells[0].textContent).toBe("Cell 1");
|
|
892
|
+
expect(firstRowCells[1].textContent).toBe("Cell 2");
|
|
893
|
+
expect(firstRowCells[2].textContent).toBe("Cell 3");
|
|
894
|
+
dispose();
|
|
895
|
+
}));
|
|
896
|
+
|
|
897
|
+
it("renders ordered and unordered lists with nested items", () =>
|
|
898
|
+
createRoot(dispose => {
|
|
899
|
+
const [items] = createSignal([
|
|
900
|
+
{ text: "Item 1", children: ["Subitem 1.1", "Subitem 1.2"] },
|
|
901
|
+
{ text: "Item 2", children: [] }
|
|
902
|
+
]);
|
|
903
|
+
|
|
904
|
+
const div = html`<div>
|
|
905
|
+
<ul>
|
|
906
|
+
<For each=${items}>
|
|
907
|
+
${(item: any) => html`
|
|
908
|
+
<li class="parent-item">
|
|
909
|
+
${item.text}
|
|
910
|
+
<ol>
|
|
911
|
+
<For each=${item.children}>
|
|
912
|
+
${(child: string) => html`<li class="child-item">${child}</li>`}
|
|
913
|
+
</For>
|
|
914
|
+
</ol>
|
|
915
|
+
</li>
|
|
916
|
+
`}
|
|
917
|
+
</For>
|
|
918
|
+
</ul>
|
|
919
|
+
</div>` as HTMLElement;
|
|
920
|
+
const container = document.createElement("div");
|
|
921
|
+
container.append(div);
|
|
922
|
+
|
|
923
|
+
const parentItems = container.querySelectorAll(".parent-item");
|
|
924
|
+
expect(parentItems.length).toBe(2);
|
|
925
|
+
|
|
926
|
+
const childItems = container.querySelectorAll(".child-item");
|
|
927
|
+
expect(childItems.length).toBe(2);
|
|
928
|
+
expect(childItems[0].textContent).toBe("Subitem 1.1");
|
|
929
|
+
expect(childItems[1].textContent).toBe("Subitem 1.2");
|
|
930
|
+
dispose();
|
|
931
|
+
}));
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
describe("Media and Resource Elements", () => {
|
|
935
|
+
it("handles img element with reactive src and alt", () => {
|
|
936
|
+
const [src, setSrc] = createSignal("image1.jpg");
|
|
937
|
+
const [alt, setAlt] = createSignal("Image 1");
|
|
938
|
+
const container = document.createElement("div");
|
|
939
|
+
const dispose = createRoot(d => {
|
|
940
|
+
const img =
|
|
941
|
+
html`<img src=${src} alt=${alt} width="100" height="100" />` as HTMLImageElement;
|
|
942
|
+
container.append(img);
|
|
943
|
+
return d;
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
const img = container.querySelector("img") as HTMLImageElement;
|
|
947
|
+
expect(img.src).toContain("image1.jpg");
|
|
948
|
+
expect(img.alt).toBe("Image 1");
|
|
949
|
+
expect(img.width).toBe(100);
|
|
950
|
+
expect(img.height).toBe(100);
|
|
951
|
+
|
|
952
|
+
setSrc("image2.jpg");
|
|
953
|
+
setAlt("Image 2");
|
|
954
|
+
flush();
|
|
955
|
+
|
|
956
|
+
expect(img.src).toContain("image2.jpg");
|
|
957
|
+
expect(img.alt).toBe("Image 2");
|
|
958
|
+
dispose();
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it("handles link and meta elements correctly", () =>
|
|
962
|
+
createRoot(dispose => {
|
|
963
|
+
const head = html`<head>
|
|
964
|
+
<link rel="stylesheet" href="style.css" />
|
|
965
|
+
<meta charset="utf-8" />
|
|
966
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
967
|
+
</head>` as HTMLElement;
|
|
968
|
+
const container = document.createElement("div");
|
|
969
|
+
container.append(head);
|
|
970
|
+
|
|
971
|
+
const link = container.querySelector("link") as HTMLLinkElement;
|
|
972
|
+
expect(link.rel).toBe("stylesheet");
|
|
973
|
+
expect(link.href).toContain("style.css");
|
|
974
|
+
|
|
975
|
+
const charsetMeta = container.querySelector("meta[charset]") as HTMLMetaElement;
|
|
976
|
+
expect(charsetMeta.getAttribute("charset")).toBe("utf-8");
|
|
977
|
+
|
|
978
|
+
const viewportMeta = container.querySelector('meta[name="viewport"]') as HTMLMetaElement;
|
|
979
|
+
expect(viewportMeta.content).toBe("width=device-width, initial-scale=1");
|
|
980
|
+
dispose();
|
|
981
|
+
}));
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
describe("Dynamic Components", () => {
|
|
986
|
+
it("renders dynamic component with expression as tag name", () =>
|
|
987
|
+
createRoot(dispose => {
|
|
988
|
+
const Comp = (props: any) => html`<div>Dynamic</div>`;
|
|
989
|
+
const result = html.define({ Comp }).jsx`<${Comp} />`;
|
|
990
|
+
const container = document.createElement("div");
|
|
991
|
+
container.append(...arrify(result));
|
|
992
|
+
expect(container.innerHTML).toBe("<div>Dynamic</div>");
|
|
993
|
+
dispose();
|
|
994
|
+
}));
|
|
995
|
+
|
|
996
|
+
it("renders dynamic component with children", () =>
|
|
997
|
+
createRoot(dispose => {
|
|
998
|
+
const Comp = (props: any) => html`<section>${props.children}</section>`;
|
|
999
|
+
const result = html.define({ Comp }).jsx`<${Comp}><span>content</span></${Comp}>`;
|
|
1000
|
+
const container = document.createElement("div");
|
|
1001
|
+
container.append(...arrify(result));
|
|
1002
|
+
expect(container.querySelector("section")?.innerHTML).toContain("<span>content</span>");
|
|
1003
|
+
dispose();
|
|
1004
|
+
}));
|
|
1005
|
+
|
|
1006
|
+
// it("renders dynamic component with shorthand close tag", () => {
|
|
1007
|
+
// const Comp = (props: any) => html`<section>${props.children}</section>`;
|
|
1008
|
+
// const result = html.define({ Comp }).jsx`<Comp>text<//>`;
|
|
1009
|
+
// const container = document.createElement("div");
|
|
1010
|
+
// container.append(...arrify(result));
|
|
1011
|
+
// expect(container.querySelector("section")?.textContent).toBe("text");
|
|
1012
|
+
// });
|
|
1013
|
+
|
|
1014
|
+
it("renders dynamic component with shorthand close tag", () =>
|
|
1015
|
+
createRoot(dispose => {
|
|
1016
|
+
const Comp = (props: any) => html`<section>${props.children}</section>`;
|
|
1017
|
+
const result = html.define({ Comp }).jsx`<${Comp}>text<//>`;
|
|
1018
|
+
const container = document.createElement("div");
|
|
1019
|
+
container.append(...arrify(result));
|
|
1020
|
+
expect(container.querySelector("section")?.textContent).toBe("text");
|
|
1021
|
+
dispose();
|
|
1022
|
+
}));
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
describe("GitHub Issues Compatibility", () => {
|
|
1026
|
+
it("https://github.com/ryansolid/dom-expressions/issues/156", () =>
|
|
1027
|
+
createRoot(dispose => {
|
|
1028
|
+
const div =
|
|
1029
|
+
html`<div><For each=${() => [1, 2, 3]}>${(n: number) => html`<h1>${n}</h1>`}</For></div>` as HTMLElement;
|
|
1030
|
+
const container = document.createElement("div");
|
|
1031
|
+
container.append(div);
|
|
1032
|
+
expect(container.innerHTML).toBe(
|
|
1033
|
+
"<div><h1>1<!--+--></h1><h1>2<!--+--></h1><h1>3<!--+--></h1><!--For--></div>"
|
|
1034
|
+
);
|
|
1035
|
+
dispose();
|
|
1036
|
+
}));
|
|
1037
|
+
|
|
1038
|
+
it("https://github.com/ryansolid/dom-expressions/issues/248", () =>
|
|
1039
|
+
createRoot(dispose => {
|
|
1040
|
+
const Comp = (props: any) => html`<div>${props.children}</div>`;
|
|
1041
|
+
const result = html.define({ Comp }).jsx`<Comp>test "ups"</Comp>`;
|
|
1042
|
+
const container = document.createElement("div");
|
|
1043
|
+
container.append(...arrify(result));
|
|
1044
|
+
expect(container.innerHTML).toBe('<div>test "ups"<!--+--></div>');
|
|
1045
|
+
dispose();
|
|
1046
|
+
}));
|
|
1047
|
+
|
|
1048
|
+
it("https://github.com/ryansolid/dom-expressions/issues/268", () =>
|
|
1049
|
+
createRoot(dispose => {
|
|
1050
|
+
const elements = html`
|
|
1051
|
+
<div id="div">Test</div>
|
|
1052
|
+
<style>
|
|
1053
|
+
#div {
|
|
1054
|
+
color:${() => "red"};
|
|
1055
|
+
background-color:blue;
|
|
1056
|
+
}
|
|
1057
|
+
</style>
|
|
1058
|
+
` as Node[];
|
|
1059
|
+
const container = document.createElement("div");
|
|
1060
|
+
container.append(...elements);
|
|
1061
|
+
document.body.append(container);
|
|
1062
|
+
expect(container.innerHTML).toEqual(
|
|
1063
|
+
`<div id="div">Test</div><style>
|
|
1064
|
+
#div {
|
|
1065
|
+
color:red<!--+-->;
|
|
1066
|
+
background-color:blue;
|
|
1067
|
+
}
|
|
1068
|
+
</style>`
|
|
1069
|
+
);
|
|
1070
|
+
dispose();
|
|
1071
|
+
}));
|
|
1072
|
+
|
|
1073
|
+
it("https://github.com/ryansolid/dom-expressions/issues/269", () =>
|
|
1074
|
+
createRoot(dispose => {
|
|
1075
|
+
const elements = html``;
|
|
1076
|
+
expect(elements).toEqual([]);
|
|
1077
|
+
dispose();
|
|
1078
|
+
}));
|
|
1079
|
+
|
|
1080
|
+
it("https://github.com/ryansolid/dom-expressions/issues/399", () =>
|
|
1081
|
+
createRoot(dispose => {
|
|
1082
|
+
const Foo = (props: any) => html`${props.bar}`;
|
|
1083
|
+
const result = html.define({ Foo }).jsx`<Foo bar></Foo>`;
|
|
1084
|
+
expect(result).toEqual(true);
|
|
1085
|
+
dispose();
|
|
1086
|
+
}));
|
|
1087
|
+
|
|
1088
|
+
// it("https://github.com/solidjs/solid/issues/1996", () => {
|
|
1089
|
+
// const elem = html`<some-el attr:foo="123">inspect element</some-el>` as HTMLElement;
|
|
1090
|
+
// expect(elem.hasAttribute("foo")).toEqual(true);
|
|
1091
|
+
// });
|
|
1092
|
+
|
|
1093
|
+
it("https://github.com/solidjs/solid/issues/2299", () =>
|
|
1094
|
+
createRoot(dispose => {
|
|
1095
|
+
const nodes = html`
|
|
1096
|
+
foo: ${123}
|
|
1097
|
+
bar: ${456}
|
|
1098
|
+
` as Node[];
|
|
1099
|
+
|
|
1100
|
+
expect(nodes[0]).toEqual("\n foo: ");
|
|
1101
|
+
expect(nodes[1]).toEqual(123);
|
|
1102
|
+
dispose();
|
|
1103
|
+
}));
|
|
1104
|
+
|
|
1105
|
+
it("template element edge case", () =>
|
|
1106
|
+
createRoot(dispose => {
|
|
1107
|
+
const elem = html`
|
|
1108
|
+
<div>
|
|
1109
|
+
<template>
|
|
1110
|
+
<h1>${123}</h1>
|
|
1111
|
+
</template>
|
|
1112
|
+
</div>
|
|
1113
|
+
`;
|
|
1114
|
+
const template = (elem as HTMLElement).querySelector("template")!;
|
|
1115
|
+
expect(template.content.querySelector("h1")?.textContent).toBe("123");
|
|
1116
|
+
dispose();
|
|
1117
|
+
}));
|
|
1118
|
+
});
|
|
1119
|
+
});
|