@praxisjs/devtools 0.2.3 → 0.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisjs/devtools",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -12,11 +12,11 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@unocss/reset": "^66.6.2",
15
- "@praxisjs/core": "0.4.0",
16
- "@praxisjs/decorators": "0.4.1",
17
- "@praxisjs/jsx": "0.3.1",
18
- "@praxisjs/runtime": "0.2.2",
19
- "@praxisjs/shared": "0.2.0"
15
+ "@praxisjs/core": "0.4.2",
16
+ "@praxisjs/decorators": "0.4.3",
17
+ "@praxisjs/jsx": "0.3.3",
18
+ "@praxisjs/shared": "0.2.0",
19
+ "@praxisjs/runtime": "0.2.4"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^25.3.0",
@@ -0,0 +1,262 @@
1
+ import { beforeEach, describe, it, expect, vi } from "vitest";
2
+
3
+ import { computed, signal } from "@praxisjs/core/internal";
4
+
5
+ import { Registry } from "../core/registry";
6
+ import { Debug } from "../decorators/debug";
7
+
8
+ beforeEach(() => {
9
+ (Registry as unknown as { _instance: Registry | null })._instance = null;
10
+ });
11
+
12
+ // ── Method decorator ──────────────────────────────────────────────────────────
13
+
14
+ describe("Debug — method decorator", () => {
15
+ it("records a method:call timeline entry", () => {
16
+ const ctx = { kind: "method" as const, name: "doWork" } as ClassMethodDecoratorContext;
17
+ const original = vi.fn((_x: unknown) => "result");
18
+
19
+ const wrapped = Debug()(original as never, ctx) as (...args: unknown[]) => unknown;
20
+
21
+ class Comp { constructor() {} }
22
+ const instance = new Comp();
23
+ const registry = Registry.instance;
24
+ registry.registerComponent(instance, "Comp");
25
+
26
+ wrapped.call(instance, 42);
27
+
28
+ const entry = registry.getTimeline().find((t) => t.type === "method:call");
29
+ expect(entry).toBeDefined();
30
+ expect(entry?.label).toContain("doWork");
31
+ expect(entry?.data.args).toEqual([42]);
32
+ expect(entry?.data.result).toBe("result");
33
+ });
34
+
35
+ it("records throw result when method throws", () => {
36
+ const ctx = { kind: "method" as const, name: "boom" } as ClassMethodDecoratorContext;
37
+ const original = () => { throw new Error("fail"); };
38
+
39
+ const wrapped = Debug()(original as never, ctx) as () => void;
40
+
41
+ class Comp {}
42
+ const instance = new Comp();
43
+ Registry.instance.registerComponent(instance, "Comp");
44
+
45
+ expect(() => wrapped.call(instance)).toThrow("fail");
46
+
47
+ const entry = Registry.instance.getTimeline().find((t) => t.type === "method:call");
48
+ expect(entry?.data.result).toContain("throw");
49
+ });
50
+
51
+ it("uses custom label from options", () => {
52
+ const ctx = { kind: "method" as const, name: "internal" } as ClassMethodDecoratorContext;
53
+ const original = vi.fn(() => undefined);
54
+
55
+ const wrapped = Debug({ label: "myLabel" })(original as never, ctx) as () => void;
56
+
57
+ class Comp {}
58
+ const instance = new Comp();
59
+ Registry.instance.registerComponent(instance, "Comp");
60
+ wrapped.call(instance);
61
+
62
+ const entry = Registry.instance.getTimeline().find((t) => t.type === "method:call");
63
+ expect(entry?.label).toContain("myLabel");
64
+ });
65
+ });
66
+
67
+ // ── Field decorator (wrapping @State getter/setter) ───────────────────────────
68
+
69
+ describe("Debug — field decorator wrapping @State", () => {
70
+ it("registers signal and tracks updates", () => {
71
+ const registry = Registry.instance;
72
+ const instance = { count: 0 } as Record<string, unknown>;
73
+
74
+ // Simulate what @State() does: install a getter/setter on the instance
75
+ const s = signal(0);
76
+ Object.defineProperty(instance, "count", {
77
+ get() { return s(); },
78
+ set(v: number) { s.set(v); },
79
+ enumerable: true,
80
+ configurable: true,
81
+ });
82
+
83
+ const initializers: Array<(this: unknown) => void> = [];
84
+ const ctx = {
85
+ kind: "field" as const,
86
+ name: "count",
87
+ addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
88
+ } as ClassFieldDecoratorContext;
89
+
90
+ Debug()(undefined, ctx);
91
+ initializers.forEach((fn) => fn.call(instance));
92
+
93
+ const signals = registry.getSignals();
94
+ expect(signals.length).toBeGreaterThan(0);
95
+ expect(signals[0].label).toBe("count");
96
+ expect(signals[0].value).toBe(0);
97
+
98
+ // Read the property through the getter (covers the get() function body)
99
+ const readValue = (instance as Record<string, unknown>).count;
100
+ expect(readValue).toBe(0);
101
+
102
+ // Trigger update through the wrapped setter
103
+ (instance as Record<string, unknown>).count = 10;
104
+
105
+ // The set through the defined property calls updateSignal
106
+ expect(registry.getSignals()[0].value).toBe(10);
107
+ });
108
+ });
109
+
110
+ // ── Field decorator (computed field) ─────────────────────────────────────────
111
+
112
+ describe("Debug — field decorator with computed value", () => {
113
+ it("registers computed signal and tracks updates", () => {
114
+ const registry = Registry.instance;
115
+ const src = signal(1);
116
+ const c = computed(() => src() * 2);
117
+
118
+ const instance = { doubled: c } as Record<string, unknown>;
119
+
120
+ const initializers: Array<(this: unknown) => void> = [];
121
+ const ctx = {
122
+ kind: "field" as const,
123
+ name: "doubled",
124
+ addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
125
+ } as ClassFieldDecoratorContext;
126
+
127
+ Debug()(undefined, ctx);
128
+ initializers.forEach((fn) => fn.call(instance));
129
+
130
+ const signals = registry.getSignals();
131
+ expect(signals.length).toBeGreaterThan(0);
132
+ expect(signals[0].label).toBe("doubled");
133
+ expect(signals[0].value).toBe(2);
134
+
135
+ src.set(5);
136
+ expect(registry.getSignals()[0].value).toBe(10);
137
+ });
138
+
139
+ it("warns when field value is not a computed and not undefined", () => {
140
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
141
+ const instance = { myField: "not-computed" } as Record<string, unknown>;
142
+
143
+ const initializers: Array<(this: unknown) => void> = [];
144
+ const ctx = {
145
+ kind: "field" as const,
146
+ name: "myField",
147
+ addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
148
+ } as ClassFieldDecoratorContext;
149
+
150
+ Debug()(undefined, ctx);
151
+ initializers.forEach((fn) => fn.call(instance));
152
+
153
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("[PraxisJS DevTools]"));
154
+ warn.mockRestore();
155
+ });
156
+
157
+ it("warns when computed field is replaced with non-computed value via setter", () => {
158
+ const registry = Registry.instance;
159
+ const src = signal(1);
160
+ const c = computed(() => src() * 2);
161
+
162
+ const instance = { doubled: c } as Record<string, unknown>;
163
+
164
+ const initializers: Array<(this: unknown) => void> = [];
165
+ const ctx = {
166
+ kind: "field" as const,
167
+ name: "doubled",
168
+ addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
169
+ } as ClassFieldDecoratorContext;
170
+
171
+ Debug()(undefined, ctx);
172
+ initializers.forEach((fn) => fn.call(instance));
173
+
174
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
175
+ // Set a non-computed value through the property setter
176
+ (instance as Record<string, unknown>).doubled = "not-a-computed";
177
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("[PraxisJS DevTools]"));
178
+ warn.mockRestore();
179
+
180
+ void registry;
181
+ });
182
+
183
+ it("updates slot when computed field is replaced with a new computed", () => {
184
+ const registry = Registry.instance;
185
+ const src = signal(1);
186
+ const c1 = computed(() => src() * 2);
187
+ const c2 = computed(() => src() * 10);
188
+
189
+ const instance = { doubled: c1 } as Record<string, unknown>;
190
+
191
+ const initializers: Array<(this: unknown) => void> = [];
192
+ const ctx = {
193
+ kind: "field" as const,
194
+ name: "doubled",
195
+ addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
196
+ } as ClassFieldDecoratorContext;
197
+
198
+ Debug()(undefined, ctx);
199
+ initializers.forEach((fn) => fn.call(instance));
200
+
201
+ // Replace with new computed
202
+ (instance as Record<string, unknown>).doubled = c2;
203
+
204
+ // The getter should now return c2
205
+ const newComputed = (instance as Record<string, unknown>).doubled;
206
+ expect(typeof newComputed).toBe("function");
207
+
208
+ void registry;
209
+ });
210
+
211
+ it("does not warn when field value is undefined", () => {
212
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
213
+ const instance = { myUndef: undefined } as Record<string, unknown>;
214
+
215
+ const initializers: Array<(this: unknown) => void> = [];
216
+ const ctx = {
217
+ kind: "field" as const,
218
+ name: "myUndef",
219
+ addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
220
+ } as ClassFieldDecoratorContext;
221
+
222
+ Debug()(undefined, ctx);
223
+ initializers.forEach((fn) => fn.call(instance));
224
+
225
+ expect(warn).not.toHaveBeenCalled();
226
+ warn.mockRestore();
227
+ });
228
+ });
229
+
230
+ // ── Getter decorator ──────────────────────────────────────────────────────────
231
+
232
+ describe("Debug — getter decorator", () => {
233
+ it("returns undefined (void) for getter kind", async () => {
234
+ const initializers: Array<(this: unknown) => void> = [];
235
+ const ctx = {
236
+ kind: "getter" as const,
237
+ name: "myGetter",
238
+ addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
239
+ } as ClassGetterDecoratorContext;
240
+
241
+ const src = signal(3);
242
+ const result = Debug()(() => src() * 2 as unknown as never, ctx);
243
+ expect(result).toBeUndefined();
244
+
245
+ // Run the initializer on a fake instance
246
+ const instance = { myGetter: computed(() => src() * 2) };
247
+ initializers.forEach((fn) => fn.call(instance));
248
+
249
+ // Wait for queueMicrotask
250
+ await new Promise<void>((r) => setTimeout(r, 0));
251
+
252
+ const signals = Registry.instance.getSignals();
253
+ expect(signals.length).toBeGreaterThan(0);
254
+
255
+ // Update src to trigger the subscribe callback (covers the updateSignal path)
256
+ const signalsBefore = Registry.instance.getSignals()[0]?.value;
257
+ src.set(10);
258
+ // The computed updates: src() * 2 = 20
259
+ await new Promise<void>((r) => setTimeout(r, 0));
260
+ expect(signalsBefore).toBe(6); // 3 * 2 = 6
261
+ });
262
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+
3
+ import { EventBus } from "../core/event-bus";
4
+
5
+ describe("EventBus", () => {
6
+ it("calls registered handler when event is emitted", () => {
7
+ const bus = new EventBus();
8
+ const handler = vi.fn();
9
+ bus.on("test", handler);
10
+ bus.emit("test", "payload");
11
+ expect(handler).toHaveBeenCalledWith("payload");
12
+ });
13
+
14
+ it("calls multiple handlers for the same event", () => {
15
+ const bus = new EventBus();
16
+ const h1 = vi.fn();
17
+ const h2 = vi.fn();
18
+ bus.on("event", h1);
19
+ bus.on("event", h2);
20
+ bus.emit("event", 42);
21
+ expect(h1).toHaveBeenCalledWith(42);
22
+ expect(h2).toHaveBeenCalledWith(42);
23
+ });
24
+
25
+ it("does not call handlers for different events", () => {
26
+ const bus = new EventBus();
27
+ const handler = vi.fn();
28
+ bus.on("a", handler);
29
+ bus.emit("b", "data");
30
+ expect(handler).not.toHaveBeenCalled();
31
+ });
32
+
33
+ it("does not throw when emitting to an event with no handlers", () => {
34
+ const bus = new EventBus();
35
+ expect(() => bus.emit("no-listeners", null)).not.toThrow();
36
+ });
37
+
38
+ it("unsubscribe function removes the handler", () => {
39
+ const bus = new EventBus();
40
+ const handler = vi.fn();
41
+ const unsub = bus.on("event", handler);
42
+ unsub();
43
+ bus.emit("event", "data");
44
+ expect(handler).not.toHaveBeenCalled();
45
+ });
46
+
47
+ it("unsubscribing one handler does not affect others", () => {
48
+ const bus = new EventBus();
49
+ const h1 = vi.fn();
50
+ const h2 = vi.fn();
51
+ const unsub = bus.on("event", h1);
52
+ bus.on("event", h2);
53
+ unsub();
54
+ bus.emit("event", "data");
55
+ expect(h1).not.toHaveBeenCalled();
56
+ expect(h2).toHaveBeenCalledWith("data");
57
+ });
58
+
59
+ it("passes payload correctly to handler", () => {
60
+ const bus = new EventBus();
61
+ const handler = vi.fn();
62
+ const payload = { key: "value", nested: { n: 1 } };
63
+ bus.on("event", handler);
64
+ bus.emit("event", payload);
65
+ expect(handler).toHaveBeenCalledWith(payload);
66
+ });
67
+
68
+ it("each EventBus instance is independent", () => {
69
+ const bus1 = new EventBus();
70
+ const bus2 = new EventBus();
71
+ const handler = vi.fn();
72
+ bus1.on("event", handler);
73
+ bus2.emit("event", "data");
74
+ expect(handler).not.toHaveBeenCalled();
75
+ });
76
+ });
@@ -0,0 +1,52 @@
1
+ import { beforeEach, afterEach, describe, it, expect, vi } from "vitest";
2
+
3
+ import { time } from "../utils/format-time";
4
+
5
+ describe("time", () => {
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ });
9
+
10
+ afterEach(() => {
11
+ vi.useRealTimers();
12
+ });
13
+
14
+ it("formats diff < 1000ms as milliseconds", () => {
15
+ // Date.now() = 1000, ts = 1000 - diff
16
+ vi.setSystemTime(1000);
17
+ expect(time(1000 - 500)).toBe("500ms"); // diff = 500
18
+ expect(time(1000 - 999)).toBe("999ms"); // diff = 999
19
+ expect(time(1000)).toBe("0ms"); // diff = 0
20
+ });
21
+
22
+ it("formats diff >= 1000ms and < 60000ms as seconds", () => {
23
+ vi.setSystemTime(10_000);
24
+ expect(time(0)).toBe("10.0s"); // diff = 10_000ms
25
+
26
+ vi.setSystemTime(5_500);
27
+ expect(time(0)).toBe("5.5s"); // diff = 5_500ms
28
+ });
29
+
30
+ it("formats diff >= 60000ms as minutes", () => {
31
+ vi.setSystemTime(120_000);
32
+ expect(time(0)).toBe("2m"); // diff = 120_000ms
33
+
34
+ vi.setSystemTime(90_000);
35
+ expect(time(0)).toBe("1m"); // diff = 90_000ms (1.5m → floor = 1)
36
+ });
37
+
38
+ it("appends prefix when provided", () => {
39
+ vi.setSystemTime(500);
40
+ expect(time(0, "ago")).toBe("500ms ago");
41
+ });
42
+
43
+ it("has no prefix when not provided", () => {
44
+ vi.setSystemTime(500);
45
+ expect(time(0)).toBe("500ms");
46
+ });
47
+
48
+ it("diff = 0ms renders as '0ms'", () => {
49
+ vi.setSystemTime(100);
50
+ expect(time(100)).toBe("0ms");
51
+ });
52
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { formatValue } from "../utils/format-value";
4
+
5
+ describe("formatValue", () => {
6
+ it("formats null", () => {
7
+ expect(formatValue(null)).toBe("null");
8
+ });
9
+
10
+ it("formats undefined", () => {
11
+ expect(formatValue(undefined)).toBe("undefined");
12
+ });
13
+
14
+ it("wraps strings in double quotes", () => {
15
+ expect(formatValue("hello")).toBe('"hello"');
16
+ expect(formatValue("")).toBe('""');
17
+ });
18
+
19
+ it("formats numbers via String()", () => {
20
+ expect(formatValue(42)).toBe("42");
21
+ expect(formatValue(3.14)).toBe("3.14");
22
+ expect(formatValue(0)).toBe("0");
23
+ expect(formatValue(-1)).toBe("-1");
24
+ });
25
+
26
+ it("formats booleans via String()", () => {
27
+ expect(formatValue(true)).toBe("true");
28
+ expect(formatValue(false)).toBe("false");
29
+ });
30
+
31
+ it("JSON-stringifies plain objects", () => {
32
+ expect(formatValue({ a: 1 })).toBe('{"a":1}');
33
+ expect(formatValue({})).toBe("{}");
34
+ });
35
+
36
+ it("JSON-stringifies arrays", () => {
37
+ expect(formatValue([1, 2, 3])).toBe("[1,2,3]");
38
+ expect(formatValue([])).toBe("[]");
39
+ });
40
+
41
+ it("returns '[object]' for circular references", () => {
42
+ const obj: Record<string, unknown> = {};
43
+ obj.self = obj;
44
+ expect(formatValue(obj)).toBe("[object]");
45
+ });
46
+ });