@praxisjs/runtime 0.2.2 → 0.2.3
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 +9 -0
- package/dist/__tests__/children.test.d.ts +2 -0
- package/dist/__tests__/children.test.d.ts.map +1 -0
- package/dist/__tests__/children.test.js +64 -0
- package/dist/__tests__/children.test.js.map +1 -0
- package/dist/__tests__/context.test.d.ts +2 -0
- package/dist/__tests__/context.test.d.ts.map +1 -0
- package/dist/__tests__/context.test.js +43 -0
- package/dist/__tests__/context.test.js.map +1 -0
- package/dist/__tests__/dom.test.d.ts +2 -0
- package/dist/__tests__/dom.test.d.ts.map +1 -0
- package/dist/__tests__/dom.test.js +146 -0
- package/dist/__tests__/dom.test.js.map +1 -0
- package/dist/__tests__/render.test.d.ts +2 -0
- package/dist/__tests__/render.test.d.ts.map +1 -0
- package/dist/__tests__/render.test.js +73 -0
- package/dist/__tests__/render.test.js.map +1 -0
- package/dist/__tests__/scope.test.d.ts +2 -0
- package/dist/__tests__/scope.test.d.ts.map +1 -0
- package/dist/__tests__/scope.test.js +56 -0
- package/dist/__tests__/scope.test.js.map +1 -0
- package/package.json +3 -3
- package/src/__tests__/children.test.ts +73 -0
- package/src/__tests__/context.test.ts +53 -0
- package/src/__tests__/dom.test.ts +170 -0
- package/src/__tests__/render.test.ts +89 -0
- package/src/__tests__/scope.test.ts +62 -0
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"children.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/children.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { signal } from "@praxisjs/core/internal";
|
|
4
|
+
import { mountChildren } from "../children";
|
|
5
|
+
import { Scope } from "../scope";
|
|
6
|
+
function container() {
|
|
7
|
+
return document.createElement("div");
|
|
8
|
+
}
|
|
9
|
+
describe("mountChildren", () => {
|
|
10
|
+
it("does nothing for null, undefined, or false", () => {
|
|
11
|
+
const el = container();
|
|
12
|
+
const scope = new Scope();
|
|
13
|
+
mountChildren(el, null, scope);
|
|
14
|
+
mountChildren(el, undefined, scope);
|
|
15
|
+
mountChildren(el, false, scope);
|
|
16
|
+
expect(el.childNodes.length).toBe(0);
|
|
17
|
+
});
|
|
18
|
+
it("appends a text node for a string", () => {
|
|
19
|
+
const el = container();
|
|
20
|
+
const scope = new Scope();
|
|
21
|
+
mountChildren(el, "hello", scope);
|
|
22
|
+
expect(el.textContent).toBe("hello");
|
|
23
|
+
});
|
|
24
|
+
it("appends a text node for a number", () => {
|
|
25
|
+
const el = container();
|
|
26
|
+
const scope = new Scope();
|
|
27
|
+
mountChildren(el, 42, scope);
|
|
28
|
+
expect(el.textContent).toBe("42");
|
|
29
|
+
});
|
|
30
|
+
it("appends a DOM node directly", () => {
|
|
31
|
+
const el = container();
|
|
32
|
+
const scope = new Scope();
|
|
33
|
+
const span = document.createElement("span");
|
|
34
|
+
mountChildren(el, span, scope);
|
|
35
|
+
expect(el.firstChild).toBe(span);
|
|
36
|
+
});
|
|
37
|
+
it("recursively mounts arrays", () => {
|
|
38
|
+
const el = container();
|
|
39
|
+
const scope = new Scope();
|
|
40
|
+
mountChildren(el, ["a", "b", "c"], scope);
|
|
41
|
+
expect(el.textContent).toBe("abc");
|
|
42
|
+
});
|
|
43
|
+
it("mounts a reactive function — updates when signal changes", () => {
|
|
44
|
+
const el = container();
|
|
45
|
+
const scope = new Scope();
|
|
46
|
+
const s = signal("first");
|
|
47
|
+
mountChildren(el, () => s(), scope);
|
|
48
|
+
expect(el.textContent).toBe("first");
|
|
49
|
+
s.set("second");
|
|
50
|
+
expect(el.textContent).toBe("second");
|
|
51
|
+
scope.dispose();
|
|
52
|
+
});
|
|
53
|
+
it("reactive children clean up old nodes on update", () => {
|
|
54
|
+
const el = container();
|
|
55
|
+
const scope = new Scope();
|
|
56
|
+
const s = signal("visible");
|
|
57
|
+
mountChildren(el, () => s(), scope);
|
|
58
|
+
expect(el.textContent).toBe("visible");
|
|
59
|
+
s.set(null);
|
|
60
|
+
expect(el.textContent).toBe("");
|
|
61
|
+
scope.dispose();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
//# sourceMappingURL=children.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"children.test.js","sourceRoot":"","sources":["../../src/__tests__/children.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/context.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { runInScope, getCurrentScope } from "../context";
|
|
3
|
+
import { Scope } from "../scope";
|
|
4
|
+
describe("runInScope / getCurrentScope", () => {
|
|
5
|
+
it("getCurrentScope throws when called outside of a render context", () => {
|
|
6
|
+
expect(() => getCurrentScope()).toThrow("[PraxisJS]");
|
|
7
|
+
});
|
|
8
|
+
it("runInScope makes the scope available via getCurrentScope", () => {
|
|
9
|
+
const scope = new Scope();
|
|
10
|
+
let captured = null;
|
|
11
|
+
runInScope(scope, () => {
|
|
12
|
+
captured = getCurrentScope();
|
|
13
|
+
});
|
|
14
|
+
expect(captured).toBe(scope);
|
|
15
|
+
});
|
|
16
|
+
it("restores previous scope after runInScope exits", () => {
|
|
17
|
+
const outer = new Scope();
|
|
18
|
+
const inner = new Scope();
|
|
19
|
+
let innerCaptured = null;
|
|
20
|
+
let outerAfter = null;
|
|
21
|
+
runInScope(outer, () => {
|
|
22
|
+
runInScope(inner, () => {
|
|
23
|
+
innerCaptured = getCurrentScope();
|
|
24
|
+
});
|
|
25
|
+
outerAfter = getCurrentScope();
|
|
26
|
+
});
|
|
27
|
+
expect(innerCaptured).toBe(inner);
|
|
28
|
+
expect(outerAfter).toBe(outer);
|
|
29
|
+
});
|
|
30
|
+
it("restores null scope even when the callback throws", () => {
|
|
31
|
+
const scope = new Scope();
|
|
32
|
+
expect(() => runInScope(scope, () => {
|
|
33
|
+
throw new Error("boom");
|
|
34
|
+
})).toThrow("boom");
|
|
35
|
+
expect(() => getCurrentScope()).toThrow("[PraxisJS]");
|
|
36
|
+
});
|
|
37
|
+
it("returns the callback's return value", () => {
|
|
38
|
+
const scope = new Scope();
|
|
39
|
+
const result = runInScope(scope, () => 42);
|
|
40
|
+
expect(result).toBe(42);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=context.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.test.js","sourceRoot":"","sources":["../../src/__tests__/context.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC,QAAQ,CAAC,8BAA8B,EAAE,GAAG,EAAE;IAC5C,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,CAAC,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,QAAQ,GAAiB,IAAI,CAAC;QAClC,UAAU,CAAC,KAAK,EAAE,GAAG,EAAE;YACrB,QAAQ,GAAG,eAAe,EAAE,CAAC;QAC/B,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,aAAa,GAAiB,IAAI,CAAC;QACvC,IAAI,UAAU,GAAiB,IAAI,CAAC;QAEpC,UAAU,CAAC,KAAK,EAAE,GAAG,EAAE;YACrB,UAAU,CAAC,KAAK,EAAE,GAAG,EAAE;gBACrB,aAAa,GAAG,eAAe,EAAE,CAAC;YACpC,CAAC,CAAC,CAAC;YACH,UAAU,GAAG,eAAe,EAAE,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,EAAE,CACV,UAAU,CAAC,KAAK,EAAE,GAAG,EAAE;YACrB,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC;QAC1B,CAAC,CAAC,CACH,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAElB,MAAM,CAAC,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAC3C,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/dom.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
import { signal } from "@praxisjs/core/internal";
|
|
4
|
+
import { createElement } from "../dom/create";
|
|
5
|
+
import { addEvent } from "../dom/events";
|
|
6
|
+
import { applyProp } from "../dom/props";
|
|
7
|
+
import { Scope } from "../scope";
|
|
8
|
+
// ── createElement ────────────────────────────────────────────────────────────
|
|
9
|
+
describe("createElement", () => {
|
|
10
|
+
it("creates an HTMLElement for regular tags", () => {
|
|
11
|
+
const el = createElement("div");
|
|
12
|
+
expect(el).toBeInstanceOf(HTMLDivElement);
|
|
13
|
+
expect(el.tagName.toLowerCase()).toBe("div");
|
|
14
|
+
});
|
|
15
|
+
it("creates an SVGElement for svg tags", () => {
|
|
16
|
+
const el = createElement("svg");
|
|
17
|
+
expect(el).toBeInstanceOf(SVGElement);
|
|
18
|
+
expect(el.tagName.toLowerCase()).toBe("svg");
|
|
19
|
+
});
|
|
20
|
+
it("creates nested svg elements with the SVG namespace", () => {
|
|
21
|
+
const path = createElement("path");
|
|
22
|
+
expect(path.namespaceURI).toBe("http://www.w3.org/2000/svg");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
// ── applyProp ────────────────────────────────────────────────────────────────
|
|
26
|
+
describe("applyProp", () => {
|
|
27
|
+
it("sets class attribute", () => {
|
|
28
|
+
const el = document.createElement("div");
|
|
29
|
+
const scope = new Scope();
|
|
30
|
+
applyProp(el, "class", "foo bar", scope);
|
|
31
|
+
expect(el.getAttribute("class")).toBe("foo bar");
|
|
32
|
+
});
|
|
33
|
+
it("sets className as class attribute", () => {
|
|
34
|
+
const el = document.createElement("div");
|
|
35
|
+
const scope = new Scope();
|
|
36
|
+
applyProp(el, "className", "my-class", scope);
|
|
37
|
+
expect(el.getAttribute("class")).toBe("my-class");
|
|
38
|
+
});
|
|
39
|
+
it("removes class when value is null", () => {
|
|
40
|
+
const el = document.createElement("div");
|
|
41
|
+
el.setAttribute("class", "old");
|
|
42
|
+
const scope = new Scope();
|
|
43
|
+
applyProp(el, "class", null, scope);
|
|
44
|
+
expect(el.hasAttribute("class")).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
it("sets style as string", () => {
|
|
47
|
+
const el = document.createElement("div");
|
|
48
|
+
const scope = new Scope();
|
|
49
|
+
applyProp(el, "style", "color: red;", scope);
|
|
50
|
+
expect(el.getAttribute("style")).toBe("color: red;");
|
|
51
|
+
});
|
|
52
|
+
it("sets style as object", () => {
|
|
53
|
+
const el = document.createElement("div");
|
|
54
|
+
const scope = new Scope();
|
|
55
|
+
applyProp(el, "style", { color: "blue" }, scope);
|
|
56
|
+
expect(el.style.color).toBe("blue");
|
|
57
|
+
});
|
|
58
|
+
it("sets boolean true as empty attribute", () => {
|
|
59
|
+
const el = document.createElement("input");
|
|
60
|
+
const scope = new Scope();
|
|
61
|
+
applyProp(el, "disabled", true, scope);
|
|
62
|
+
expect(el.hasAttribute("disabled")).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it("removes attribute when value is false", () => {
|
|
65
|
+
const el = document.createElement("input");
|
|
66
|
+
el.setAttribute("disabled", "");
|
|
67
|
+
const scope = new Scope();
|
|
68
|
+
applyProp(el, "disabled", false, scope);
|
|
69
|
+
expect(el.hasAttribute("disabled")).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
it("sets value prop directly on element", () => {
|
|
72
|
+
const el = document.createElement("input");
|
|
73
|
+
const scope = new Scope();
|
|
74
|
+
applyProp(el, "value", "hello", scope);
|
|
75
|
+
expect(el.value).toBe("hello");
|
|
76
|
+
});
|
|
77
|
+
it("calls ref callback with the element", () => {
|
|
78
|
+
const el = document.createElement("span");
|
|
79
|
+
const scope = new Scope();
|
|
80
|
+
const ref = vi.fn();
|
|
81
|
+
applyProp(el, "ref", ref, scope);
|
|
82
|
+
expect(ref).toHaveBeenCalledWith(el);
|
|
83
|
+
});
|
|
84
|
+
it("skips 'children' and 'key' props", () => {
|
|
85
|
+
const el = document.createElement("div");
|
|
86
|
+
const scope = new Scope();
|
|
87
|
+
applyProp(el, "children", "should be ignored", scope);
|
|
88
|
+
applyProp(el, "key", "k1", scope);
|
|
89
|
+
expect(el.childNodes.length).toBe(0);
|
|
90
|
+
expect(el.hasAttribute("key")).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
it("attaches event listener for onClick", () => {
|
|
93
|
+
const el = document.createElement("button");
|
|
94
|
+
const scope = new Scope();
|
|
95
|
+
const handler = vi.fn();
|
|
96
|
+
applyProp(el, "onClick", handler, scope);
|
|
97
|
+
el.dispatchEvent(new MouseEvent("click"));
|
|
98
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
99
|
+
});
|
|
100
|
+
it("removes event listener on scope dispose", () => {
|
|
101
|
+
const el = document.createElement("button");
|
|
102
|
+
const scope = new Scope();
|
|
103
|
+
const handler = vi.fn();
|
|
104
|
+
applyProp(el, "onClick", handler, scope);
|
|
105
|
+
scope.dispose();
|
|
106
|
+
el.dispatchEvent(new MouseEvent("click"));
|
|
107
|
+
expect(handler).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
it("tracks reactive prop — updates when signal changes", () => {
|
|
110
|
+
const el = document.createElement("div");
|
|
111
|
+
const scope = new Scope();
|
|
112
|
+
const cls = signal("initial");
|
|
113
|
+
applyProp(el, "class", () => cls(), scope);
|
|
114
|
+
expect(el.getAttribute("class")).toBe("initial");
|
|
115
|
+
cls.set("updated");
|
|
116
|
+
expect(el.getAttribute("class")).toBe("updated");
|
|
117
|
+
scope.dispose();
|
|
118
|
+
});
|
|
119
|
+
it("normalizes htmlFor to for attribute", () => {
|
|
120
|
+
const label = document.createElement("label");
|
|
121
|
+
const scope = new Scope();
|
|
122
|
+
applyProp(label, "htmlFor", "my-input", scope);
|
|
123
|
+
expect(label.getAttribute("for")).toBe("my-input");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
// ── addEvent ─────────────────────────────────────────────────────────────────
|
|
127
|
+
describe("addEvent", () => {
|
|
128
|
+
it("adds event listener that fires on dispatch", () => {
|
|
129
|
+
const el = document.createElement("button");
|
|
130
|
+
const scope = new Scope();
|
|
131
|
+
const fn = vi.fn();
|
|
132
|
+
addEvent(el, "click", fn, scope);
|
|
133
|
+
el.dispatchEvent(new MouseEvent("click"));
|
|
134
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
135
|
+
});
|
|
136
|
+
it("removes listener when scope is disposed", () => {
|
|
137
|
+
const el = document.createElement("button");
|
|
138
|
+
const scope = new Scope();
|
|
139
|
+
const fn = vi.fn();
|
|
140
|
+
addEvent(el, "click", fn, scope);
|
|
141
|
+
scope.dispose();
|
|
142
|
+
el.dispatchEvent(new MouseEvent("click"));
|
|
143
|
+
expect(fn).not.toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
//# sourceMappingURL=dom.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom.test.js","sourceRoot":"","sources":["../../src/__tests__/dom.test.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAC5B,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAElD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAGjC,gFAAgF;AAEhF,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,EAAE,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,CAAC,EAAE,CAAC,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC;QAC1C,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,EAAE,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,CAAC,EAAE,CAAC,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;QACtC,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,gFAAgF;AAEhF,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,SAAS,CAAC,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QACzC,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,SAAS,CAAC,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC;QAC9C,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACzC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAChC,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,SAAS,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACpC,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,SAAS,CAAC,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,CAAC,CAAC;QAC7C,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAgB,CAAC;QACxD,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,SAAS,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,CAAC,CAAC;QACjD,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACvC,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC3C,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QAChC,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QACxC,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,SAAS,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QACvC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAC1C,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACpB,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QACjC,MAAM,CAAC,GAAG,CAAC,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,mBAAmB,EAAE,KAAK,CAAC,CAAC;QACtD,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QAClC,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACrC,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACxB,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QACzC,EAAE,CAAC,aAAa,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,OAAO,CAAC,CAAC,oBAAoB,EAAE,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACxB,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QACzC,KAAK,CAAC,OAAO,EAAE,CAAC;QAChB,EAAE,CAAC,aAAa,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;QAC9B,SAAS,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,CAAC,CAAC;QAC3C,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACjD,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACnB,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACjD,KAAK,CAAC,OAAO,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC9C,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC;QAC/C,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,gFAAgF;AAEhF,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACnB,QAAQ,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;QACjC,EAAE,CAAC,aAAa,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,EAAE,CAAC,CAAC,oBAAoB,EAAE,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACnB,QAAQ,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;QACjC,KAAK,CAAC,OAAO,EAAE,CAAC;QAChB,EAAE,CAAC,aAAa,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACpC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/render.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { signal } from "@praxisjs/core/internal";
|
|
4
|
+
import { render, mountElement } from "../index";
|
|
5
|
+
import { Scope } from "../scope";
|
|
6
|
+
// ── mountElement ─────────────────────────────────────────────────────────────
|
|
7
|
+
describe("mountElement", () => {
|
|
8
|
+
it("creates an element with the given tag", () => {
|
|
9
|
+
const scope = new Scope();
|
|
10
|
+
const el = mountElement("section", {}, scope);
|
|
11
|
+
expect(el.tagName.toLowerCase()).toBe("section");
|
|
12
|
+
});
|
|
13
|
+
it("applies static props", () => {
|
|
14
|
+
const scope = new Scope();
|
|
15
|
+
const el = mountElement("p", { id: "para", class: "text" }, scope);
|
|
16
|
+
expect(el.id).toBe("para");
|
|
17
|
+
expect(el.getAttribute("class")).toBe("text");
|
|
18
|
+
});
|
|
19
|
+
it("mounts string children", () => {
|
|
20
|
+
const scope = new Scope();
|
|
21
|
+
const el = mountElement("p", { children: "hello" }, scope);
|
|
22
|
+
expect(el.textContent).toBe("hello");
|
|
23
|
+
});
|
|
24
|
+
it("mounts reactive prop — updates when signal changes", () => {
|
|
25
|
+
const scope = new Scope();
|
|
26
|
+
const cls = signal("a");
|
|
27
|
+
const el = mountElement("div", { class: () => cls() }, scope);
|
|
28
|
+
expect(el.getAttribute("class")).toBe("a");
|
|
29
|
+
cls.set("b");
|
|
30
|
+
expect(el.getAttribute("class")).toBe("b");
|
|
31
|
+
scope.dispose();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
// ── render ───────────────────────────────────────────────────────────────────
|
|
35
|
+
describe("render", () => {
|
|
36
|
+
it("mounts content into container", () => {
|
|
37
|
+
const container = document.createElement("div");
|
|
38
|
+
document.body.appendChild(container);
|
|
39
|
+
const scope = new Scope();
|
|
40
|
+
render(() => {
|
|
41
|
+
const el = mountElement("h1", { children: "Hello" }, scope);
|
|
42
|
+
return el;
|
|
43
|
+
}, container);
|
|
44
|
+
expect(container.textContent).toBe("Hello");
|
|
45
|
+
document.body.removeChild(container);
|
|
46
|
+
});
|
|
47
|
+
it("clears container on unmount", () => {
|
|
48
|
+
const container = document.createElement("div");
|
|
49
|
+
document.body.appendChild(container);
|
|
50
|
+
const unmount = render(() => {
|
|
51
|
+
const scope = new Scope();
|
|
52
|
+
return mountElement("p", { children: "bye" }, scope);
|
|
53
|
+
}, container);
|
|
54
|
+
expect(container.textContent).toBe("bye");
|
|
55
|
+
unmount();
|
|
56
|
+
expect(container.innerHTML).toBe("");
|
|
57
|
+
document.body.removeChild(container);
|
|
58
|
+
});
|
|
59
|
+
it("reactive content re-renders on signal change", () => {
|
|
60
|
+
const container = document.createElement("div");
|
|
61
|
+
document.body.appendChild(container);
|
|
62
|
+
const text = signal("initial");
|
|
63
|
+
render(() => {
|
|
64
|
+
const scope = new Scope();
|
|
65
|
+
return mountElement("span", { children: () => text() }, scope);
|
|
66
|
+
}, container);
|
|
67
|
+
expect(container.textContent).toBe("initial");
|
|
68
|
+
text.set("updated");
|
|
69
|
+
expect(container.textContent).toBe("updated");
|
|
70
|
+
document.body.removeChild(container);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
//# sourceMappingURL=render.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.test.js","sourceRoot":"","sources":["../../src/__tests__/render.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/scope.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { Scope } from "../scope";
|
|
3
|
+
describe("Scope", () => {
|
|
4
|
+
it("add() registers a cleanup fn that runs on dispose()", () => {
|
|
5
|
+
const scope = new Scope();
|
|
6
|
+
const cleanup = vi.fn();
|
|
7
|
+
scope.add(cleanup);
|
|
8
|
+
scope.dispose();
|
|
9
|
+
expect(cleanup).toHaveBeenCalledOnce();
|
|
10
|
+
});
|
|
11
|
+
it("dispose() runs all cleanups in order", () => {
|
|
12
|
+
const scope = new Scope();
|
|
13
|
+
const order = [];
|
|
14
|
+
scope.add(() => order.push(1));
|
|
15
|
+
scope.add(() => order.push(2));
|
|
16
|
+
scope.add(() => order.push(3));
|
|
17
|
+
scope.dispose();
|
|
18
|
+
expect(order).toEqual([1, 2, 3]);
|
|
19
|
+
});
|
|
20
|
+
it("dispose() clears cleanups — second dispose() is a no-op", () => {
|
|
21
|
+
const scope = new Scope();
|
|
22
|
+
const fn = vi.fn();
|
|
23
|
+
scope.add(fn);
|
|
24
|
+
scope.dispose();
|
|
25
|
+
scope.dispose();
|
|
26
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
27
|
+
});
|
|
28
|
+
it("effect() registers a reactive effect and its cleanup", () => {
|
|
29
|
+
const scope = new Scope();
|
|
30
|
+
const ran = [];
|
|
31
|
+
scope.effect(() => {
|
|
32
|
+
ran.push(1);
|
|
33
|
+
});
|
|
34
|
+
expect(ran).toHaveLength(1); // runs immediately
|
|
35
|
+
scope.dispose();
|
|
36
|
+
});
|
|
37
|
+
it("fork() creates a child scope that disposes when parent disposes", () => {
|
|
38
|
+
const parent = new Scope();
|
|
39
|
+
const child = parent.fork();
|
|
40
|
+
const childCleanup = vi.fn();
|
|
41
|
+
child.add(childCleanup);
|
|
42
|
+
parent.dispose();
|
|
43
|
+
expect(childCleanup).toHaveBeenCalledOnce();
|
|
44
|
+
});
|
|
45
|
+
it("fork() child can be disposed independently before parent", () => {
|
|
46
|
+
const parent = new Scope();
|
|
47
|
+
const child = parent.fork();
|
|
48
|
+
const childFn = vi.fn();
|
|
49
|
+
child.add(childFn);
|
|
50
|
+
child.dispose();
|
|
51
|
+
expect(childFn).toHaveBeenCalledOnce();
|
|
52
|
+
// parent dispose should not throw even though child is already disposed
|
|
53
|
+
expect(() => { parent.dispose(); }).not.toThrow();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
//# sourceMappingURL=scope.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope.test.js","sourceRoot":"","sources":["../../src/__tests__/scope.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisjs/runtime",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"typescript": "^5.9.3"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@praxisjs/core": "0.4.
|
|
18
|
-
"@praxisjs/decorators": "0.4.
|
|
17
|
+
"@praxisjs/core": "0.4.1",
|
|
18
|
+
"@praxisjs/decorators": "0.4.2",
|
|
19
19
|
"@praxisjs/shared": "0.2.0"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { signal } from "@praxisjs/core/internal";
|
|
5
|
+
|
|
6
|
+
import { mountChildren } from "../children";
|
|
7
|
+
import { Scope } from "../scope";
|
|
8
|
+
|
|
9
|
+
function container() {
|
|
10
|
+
return document.createElement("div");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("mountChildren", () => {
|
|
14
|
+
it("does nothing for null, undefined, or false", () => {
|
|
15
|
+
const el = container();
|
|
16
|
+
const scope = new Scope();
|
|
17
|
+
mountChildren(el, null, scope);
|
|
18
|
+
mountChildren(el, undefined, scope);
|
|
19
|
+
mountChildren(el, false, scope);
|
|
20
|
+
expect(el.childNodes.length).toBe(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("appends a text node for a string", () => {
|
|
24
|
+
const el = container();
|
|
25
|
+
const scope = new Scope();
|
|
26
|
+
mountChildren(el, "hello", scope);
|
|
27
|
+
expect(el.textContent).toBe("hello");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("appends a text node for a number", () => {
|
|
31
|
+
const el = container();
|
|
32
|
+
const scope = new Scope();
|
|
33
|
+
mountChildren(el, 42, scope);
|
|
34
|
+
expect(el.textContent).toBe("42");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("appends a DOM node directly", () => {
|
|
38
|
+
const el = container();
|
|
39
|
+
const scope = new Scope();
|
|
40
|
+
const span = document.createElement("span");
|
|
41
|
+
mountChildren(el, span, scope);
|
|
42
|
+
expect(el.firstChild).toBe(span);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("recursively mounts arrays", () => {
|
|
46
|
+
const el = container();
|
|
47
|
+
const scope = new Scope();
|
|
48
|
+
mountChildren(el, ["a", "b", "c"], scope);
|
|
49
|
+
expect(el.textContent).toBe("abc");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("mounts a reactive function — updates when signal changes", () => {
|
|
53
|
+
const el = container();
|
|
54
|
+
const scope = new Scope();
|
|
55
|
+
const s = signal("first");
|
|
56
|
+
mountChildren(el, () => s(), scope);
|
|
57
|
+
expect(el.textContent).toBe("first");
|
|
58
|
+
s.set("second");
|
|
59
|
+
expect(el.textContent).toBe("second");
|
|
60
|
+
scope.dispose();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("reactive children clean up old nodes on update", () => {
|
|
64
|
+
const el = container();
|
|
65
|
+
const scope = new Scope();
|
|
66
|
+
const s = signal<string | null>("visible");
|
|
67
|
+
mountChildren(el, () => s(), scope);
|
|
68
|
+
expect(el.textContent).toBe("visible");
|
|
69
|
+
s.set(null);
|
|
70
|
+
expect(el.textContent).toBe("");
|
|
71
|
+
scope.dispose();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { runInScope, getCurrentScope } from "../context";
|
|
4
|
+
import { Scope } from "../scope";
|
|
5
|
+
|
|
6
|
+
describe("runInScope / getCurrentScope", () => {
|
|
7
|
+
it("getCurrentScope throws when called outside of a render context", () => {
|
|
8
|
+
expect(() => getCurrentScope()).toThrow("[PraxisJS]");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("runInScope makes the scope available via getCurrentScope", () => {
|
|
12
|
+
const scope = new Scope();
|
|
13
|
+
let captured: Scope | null = null;
|
|
14
|
+
runInScope(scope, () => {
|
|
15
|
+
captured = getCurrentScope();
|
|
16
|
+
});
|
|
17
|
+
expect(captured).toBe(scope);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("restores previous scope after runInScope exits", () => {
|
|
21
|
+
const outer = new Scope();
|
|
22
|
+
const inner = new Scope();
|
|
23
|
+
let innerCaptured: Scope | null = null;
|
|
24
|
+
let outerAfter: Scope | null = null;
|
|
25
|
+
|
|
26
|
+
runInScope(outer, () => {
|
|
27
|
+
runInScope(inner, () => {
|
|
28
|
+
innerCaptured = getCurrentScope();
|
|
29
|
+
});
|
|
30
|
+
outerAfter = getCurrentScope();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(innerCaptured).toBe(inner);
|
|
34
|
+
expect(outerAfter).toBe(outer);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("restores null scope even when the callback throws", () => {
|
|
38
|
+
const scope = new Scope();
|
|
39
|
+
expect(() =>
|
|
40
|
+
runInScope(scope, () => {
|
|
41
|
+
throw new Error("boom");
|
|
42
|
+
}),
|
|
43
|
+
).toThrow("boom");
|
|
44
|
+
|
|
45
|
+
expect(() => getCurrentScope()).toThrow("[PraxisJS]");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns the callback's return value", () => {
|
|
49
|
+
const scope = new Scope();
|
|
50
|
+
const result = runInScope(scope, () => 42);
|
|
51
|
+
expect(result).toBe(42);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { signal } from "@praxisjs/core/internal";
|
|
5
|
+
|
|
6
|
+
import { createElement } from "../dom/create";
|
|
7
|
+
import { addEvent } from "../dom/events";
|
|
8
|
+
import { applyProp } from "../dom/props";
|
|
9
|
+
import { Scope } from "../scope";
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// ── createElement ────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
describe("createElement", () => {
|
|
15
|
+
it("creates an HTMLElement for regular tags", () => {
|
|
16
|
+
const el = createElement("div");
|
|
17
|
+
expect(el).toBeInstanceOf(HTMLDivElement);
|
|
18
|
+
expect(el.tagName.toLowerCase()).toBe("div");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("creates an SVGElement for svg tags", () => {
|
|
22
|
+
const el = createElement("svg");
|
|
23
|
+
expect(el).toBeInstanceOf(SVGElement);
|
|
24
|
+
expect(el.tagName.toLowerCase()).toBe("svg");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("creates nested svg elements with the SVG namespace", () => {
|
|
28
|
+
const path = createElement("path");
|
|
29
|
+
expect(path.namespaceURI).toBe("http://www.w3.org/2000/svg");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ── applyProp ────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
describe("applyProp", () => {
|
|
36
|
+
it("sets class attribute", () => {
|
|
37
|
+
const el = document.createElement("div");
|
|
38
|
+
const scope = new Scope();
|
|
39
|
+
applyProp(el, "class", "foo bar", scope);
|
|
40
|
+
expect(el.getAttribute("class")).toBe("foo bar");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("sets className as class attribute", () => {
|
|
44
|
+
const el = document.createElement("div");
|
|
45
|
+
const scope = new Scope();
|
|
46
|
+
applyProp(el, "className", "my-class", scope);
|
|
47
|
+
expect(el.getAttribute("class")).toBe("my-class");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("removes class when value is null", () => {
|
|
51
|
+
const el = document.createElement("div");
|
|
52
|
+
el.setAttribute("class", "old");
|
|
53
|
+
const scope = new Scope();
|
|
54
|
+
applyProp(el, "class", null, scope);
|
|
55
|
+
expect(el.hasAttribute("class")).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("sets style as string", () => {
|
|
59
|
+
const el = document.createElement("div");
|
|
60
|
+
const scope = new Scope();
|
|
61
|
+
applyProp(el, "style", "color: red;", scope);
|
|
62
|
+
expect(el.getAttribute("style")).toBe("color: red;");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("sets style as object", () => {
|
|
66
|
+
const el = document.createElement("div") as HTMLElement;
|
|
67
|
+
const scope = new Scope();
|
|
68
|
+
applyProp(el, "style", { color: "blue" }, scope);
|
|
69
|
+
expect(el.style.color).toBe("blue");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("sets boolean true as empty attribute", () => {
|
|
73
|
+
const el = document.createElement("input");
|
|
74
|
+
const scope = new Scope();
|
|
75
|
+
applyProp(el, "disabled", true, scope);
|
|
76
|
+
expect(el.hasAttribute("disabled")).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("removes attribute when value is false", () => {
|
|
80
|
+
const el = document.createElement("input");
|
|
81
|
+
el.setAttribute("disabled", "");
|
|
82
|
+
const scope = new Scope();
|
|
83
|
+
applyProp(el, "disabled", false, scope);
|
|
84
|
+
expect(el.hasAttribute("disabled")).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("sets value prop directly on element", () => {
|
|
88
|
+
const el = document.createElement("input");
|
|
89
|
+
const scope = new Scope();
|
|
90
|
+
applyProp(el, "value", "hello", scope);
|
|
91
|
+
expect(el.value).toBe("hello");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("calls ref callback with the element", () => {
|
|
95
|
+
const el = document.createElement("span");
|
|
96
|
+
const scope = new Scope();
|
|
97
|
+
const ref = vi.fn();
|
|
98
|
+
applyProp(el, "ref", ref, scope);
|
|
99
|
+
expect(ref).toHaveBeenCalledWith(el);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("skips 'children' and 'key' props", () => {
|
|
103
|
+
const el = document.createElement("div");
|
|
104
|
+
const scope = new Scope();
|
|
105
|
+
applyProp(el, "children", "should be ignored", scope);
|
|
106
|
+
applyProp(el, "key", "k1", scope);
|
|
107
|
+
expect(el.childNodes.length).toBe(0);
|
|
108
|
+
expect(el.hasAttribute("key")).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("attaches event listener for onClick", () => {
|
|
112
|
+
const el = document.createElement("button");
|
|
113
|
+
const scope = new Scope();
|
|
114
|
+
const handler = vi.fn();
|
|
115
|
+
applyProp(el, "onClick", handler, scope);
|
|
116
|
+
el.dispatchEvent(new MouseEvent("click"));
|
|
117
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("removes event listener on scope dispose", () => {
|
|
121
|
+
const el = document.createElement("button");
|
|
122
|
+
const scope = new Scope();
|
|
123
|
+
const handler = vi.fn();
|
|
124
|
+
applyProp(el, "onClick", handler, scope);
|
|
125
|
+
scope.dispose();
|
|
126
|
+
el.dispatchEvent(new MouseEvent("click"));
|
|
127
|
+
expect(handler).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("tracks reactive prop — updates when signal changes", () => {
|
|
131
|
+
const el = document.createElement("div");
|
|
132
|
+
const scope = new Scope();
|
|
133
|
+
const cls = signal("initial");
|
|
134
|
+
applyProp(el, "class", () => cls(), scope);
|
|
135
|
+
expect(el.getAttribute("class")).toBe("initial");
|
|
136
|
+
cls.set("updated");
|
|
137
|
+
expect(el.getAttribute("class")).toBe("updated");
|
|
138
|
+
scope.dispose();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("normalizes htmlFor to for attribute", () => {
|
|
142
|
+
const label = document.createElement("label");
|
|
143
|
+
const scope = new Scope();
|
|
144
|
+
applyProp(label, "htmlFor", "my-input", scope);
|
|
145
|
+
expect(label.getAttribute("for")).toBe("my-input");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ── addEvent ─────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
describe("addEvent", () => {
|
|
152
|
+
it("adds event listener that fires on dispatch", () => {
|
|
153
|
+
const el = document.createElement("button");
|
|
154
|
+
const scope = new Scope();
|
|
155
|
+
const fn = vi.fn();
|
|
156
|
+
addEvent(el, "click", fn, scope);
|
|
157
|
+
el.dispatchEvent(new MouseEvent("click"));
|
|
158
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("removes listener when scope is disposed", () => {
|
|
162
|
+
const el = document.createElement("button");
|
|
163
|
+
const scope = new Scope();
|
|
164
|
+
const fn = vi.fn();
|
|
165
|
+
addEvent(el, "click", fn, scope);
|
|
166
|
+
scope.dispose();
|
|
167
|
+
el.dispatchEvent(new MouseEvent("click"));
|
|
168
|
+
expect(fn).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { signal } from "@praxisjs/core/internal";
|
|
5
|
+
|
|
6
|
+
import { render, mountElement } from "../index";
|
|
7
|
+
import { Scope } from "../scope";
|
|
8
|
+
|
|
9
|
+
// ── mountElement ─────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
describe("mountElement", () => {
|
|
12
|
+
it("creates an element with the given tag", () => {
|
|
13
|
+
const scope = new Scope();
|
|
14
|
+
const el = mountElement("section", {}, scope);
|
|
15
|
+
expect(el.tagName.toLowerCase()).toBe("section");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("applies static props", () => {
|
|
19
|
+
const scope = new Scope();
|
|
20
|
+
const el = mountElement("p", { id: "para", class: "text" }, scope) as HTMLElement;
|
|
21
|
+
expect(el.id).toBe("para");
|
|
22
|
+
expect(el.getAttribute("class")).toBe("text");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("mounts string children", () => {
|
|
26
|
+
const scope = new Scope();
|
|
27
|
+
const el = mountElement("p", { children: "hello" }, scope);
|
|
28
|
+
expect(el.textContent).toBe("hello");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("mounts reactive prop — updates when signal changes", () => {
|
|
32
|
+
const scope = new Scope();
|
|
33
|
+
const cls = signal("a");
|
|
34
|
+
const el = mountElement("div", { class: () => cls() }, scope);
|
|
35
|
+
expect(el.getAttribute("class")).toBe("a");
|
|
36
|
+
cls.set("b");
|
|
37
|
+
expect(el.getAttribute("class")).toBe("b");
|
|
38
|
+
scope.dispose();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ── render ───────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
describe("render", () => {
|
|
45
|
+
it("mounts content into container", () => {
|
|
46
|
+
const container = document.createElement("div");
|
|
47
|
+
document.body.appendChild(container);
|
|
48
|
+
|
|
49
|
+
const scope = new Scope();
|
|
50
|
+
render(() => {
|
|
51
|
+
const el = mountElement("h1", { children: "Hello" }, scope);
|
|
52
|
+
return el;
|
|
53
|
+
}, container);
|
|
54
|
+
|
|
55
|
+
expect(container.textContent).toBe("Hello");
|
|
56
|
+
document.body.removeChild(container);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("clears container on unmount", () => {
|
|
60
|
+
const container = document.createElement("div");
|
|
61
|
+
document.body.appendChild(container);
|
|
62
|
+
|
|
63
|
+
const unmount = render(() => {
|
|
64
|
+
const scope = new Scope();
|
|
65
|
+
return mountElement("p", { children: "bye" }, scope);
|
|
66
|
+
}, container);
|
|
67
|
+
|
|
68
|
+
expect(container.textContent).toBe("bye");
|
|
69
|
+
unmount();
|
|
70
|
+
expect(container.innerHTML).toBe("");
|
|
71
|
+
document.body.removeChild(container);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("reactive content re-renders on signal change", () => {
|
|
75
|
+
const container = document.createElement("div");
|
|
76
|
+
document.body.appendChild(container);
|
|
77
|
+
const text = signal("initial");
|
|
78
|
+
|
|
79
|
+
render(() => {
|
|
80
|
+
const scope = new Scope();
|
|
81
|
+
return mountElement("span", { children: () => text() }, scope);
|
|
82
|
+
}, container);
|
|
83
|
+
|
|
84
|
+
expect(container.textContent).toBe("initial");
|
|
85
|
+
text.set("updated");
|
|
86
|
+
expect(container.textContent).toBe("updated");
|
|
87
|
+
document.body.removeChild(container);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Scope } from "../scope";
|
|
4
|
+
|
|
5
|
+
describe("Scope", () => {
|
|
6
|
+
it("add() registers a cleanup fn that runs on dispose()", () => {
|
|
7
|
+
const scope = new Scope();
|
|
8
|
+
const cleanup = vi.fn();
|
|
9
|
+
scope.add(cleanup);
|
|
10
|
+
scope.dispose();
|
|
11
|
+
expect(cleanup).toHaveBeenCalledOnce();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("dispose() runs all cleanups in order", () => {
|
|
15
|
+
const scope = new Scope();
|
|
16
|
+
const order: number[] = [];
|
|
17
|
+
scope.add(() => order.push(1));
|
|
18
|
+
scope.add(() => order.push(2));
|
|
19
|
+
scope.add(() => order.push(3));
|
|
20
|
+
scope.dispose();
|
|
21
|
+
expect(order).toEqual([1, 2, 3]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("dispose() clears cleanups — second dispose() is a no-op", () => {
|
|
25
|
+
const scope = new Scope();
|
|
26
|
+
const fn = vi.fn();
|
|
27
|
+
scope.add(fn);
|
|
28
|
+
scope.dispose();
|
|
29
|
+
scope.dispose();
|
|
30
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("effect() registers a reactive effect and its cleanup", () => {
|
|
34
|
+
const scope = new Scope();
|
|
35
|
+
const ran: number[] = [];
|
|
36
|
+
scope.effect(() => {
|
|
37
|
+
ran.push(1);
|
|
38
|
+
});
|
|
39
|
+
expect(ran).toHaveLength(1); // runs immediately
|
|
40
|
+
scope.dispose();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("fork() creates a child scope that disposes when parent disposes", () => {
|
|
44
|
+
const parent = new Scope();
|
|
45
|
+
const child = parent.fork();
|
|
46
|
+
const childCleanup = vi.fn();
|
|
47
|
+
child.add(childCleanup);
|
|
48
|
+
parent.dispose();
|
|
49
|
+
expect(childCleanup).toHaveBeenCalledOnce();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("fork() child can be disposed independently before parent", () => {
|
|
53
|
+
const parent = new Scope();
|
|
54
|
+
const child = parent.fork();
|
|
55
|
+
const childFn = vi.fn();
|
|
56
|
+
child.add(childFn);
|
|
57
|
+
child.dispose();
|
|
58
|
+
expect(childFn).toHaveBeenCalledOnce();
|
|
59
|
+
// parent dispose should not throw even though child is already disposed
|
|
60
|
+
expect(() => { parent.dispose(); }).not.toThrow();
|
|
61
|
+
});
|
|
62
|
+
});
|