@praxisjs/devtools 0.2.33 → 0.3.0
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 +40 -0
- package/assets/uno.generated.css +5 -0
- package/dist/assets/logo.svg.js +4 -0
- package/dist/assets/tailwind-v4.css.js +4 -0
- package/dist/assets/uno.generated.css.js +4 -0
- package/dist/core/event-bus.js +21 -0
- package/dist/core/profiler.js +79 -0
- package/dist/core/registry.js +147 -0
- package/dist/decorators/debug.js +126 -0
- package/dist/decorators/trace.js +45 -0
- package/dist/icons/ellipsis-vertical.js +38 -0
- package/dist/icons/panel-bottom.js +28 -0
- package/dist/icons/panel-left.js +28 -0
- package/dist/icons/panel-right.js +28 -0
- package/dist/icons/panel-top.js +28 -0
- package/dist/icons/x.js +22 -0
- package/dist/index.d.ts +96 -3
- package/dist/index.js +10 -1157
- package/dist/plugins/components/components/component-detail.js +53 -0
- package/dist/plugins/components/components/component-row.js +22 -0
- package/dist/plugins/components/components/detail-row.js +15 -0
- package/dist/plugins/components/components/detail-section.js +14 -0
- package/dist/plugins/components/components/status-dot.js +15 -0
- package/dist/plugins/components/components-tab.js +54 -0
- package/dist/plugins/components/index.js +9 -0
- package/dist/plugins/performance/index.js +9 -0
- package/dist/plugins/performance/performance-tab.js +62 -0
- package/dist/plugins/signals/components/signal-detail.js +29 -0
- package/dist/plugins/signals/components/signal-row.js +17 -0
- package/dist/plugins/signals/index.js +9 -0
- package/dist/plugins/signals/signals-tab.js +53 -0
- package/dist/plugins/timeline/components/badge.js +16 -0
- package/dist/plugins/timeline/components/timeline-row.js +33 -0
- package/dist/plugins/timeline/constants.js +54 -0
- package/dist/plugins/timeline/index.js +9 -0
- package/dist/plugins/timeline/timeline-tab.js +46 -0
- package/dist/ui/dev-tools.js +74 -0
- package/dist/ui/panel.js +106 -0
- package/dist/ui/shared/empty-state.js +14 -0
- package/dist/ui/shared/icon-button.js +17 -0
- package/dist/ui/shared/panel-section.js +14 -0
- package/dist/ui/shared/search-input.js +17 -0
- package/dist/ui/shared/side-panel.js +15 -0
- package/dist/utils/format-time.js +7 -0
- package/dist/utils/format-value.js +14 -0
- package/package.json +14 -6
- package/src/core/profiler.ts +162 -0
- package/src/core/registry.ts +23 -21
- package/src/decorators/debug.ts +96 -32
- package/src/decorators/index.ts +1 -0
- package/src/decorators/trace.ts +27 -4
- package/src/index.ts +5 -0
- package/src/plugins/performance/index.ts +9 -0
- package/src/plugins/performance/performance-tab.tsx +111 -0
- package/src/ui/dev-tools.tsx +5 -0
- package/src/__tests__/debug.test.ts +0 -333
- package/src/__tests__/event-bus.test.ts +0 -123
- package/src/__tests__/format-time.test.ts +0 -52
- package/src/__tests__/format-value.test.ts +0 -46
- package/src/__tests__/plugins.test.ts +0 -101
- package/src/__tests__/registry.test.ts +0 -386
- package/src/__tests__/trace.test.ts +0 -200
- package/tsconfig.json +0 -21
- package/uno.config.ts +0 -57
- package/vite.config.ts +0 -38
|
@@ -1,333 +0,0 @@
|
|
|
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
|
-
function mockMethodCtx(name: string) {
|
|
15
|
-
const initializers: Array<(this: object) => void> = [];
|
|
16
|
-
return {
|
|
17
|
-
kind: "method" as const,
|
|
18
|
-
name,
|
|
19
|
-
addInitializer(fn: (this: object) => void) { initializers.push(fn); },
|
|
20
|
-
runInitializers(instance: object) { initializers.forEach((fn) => { fn.call(instance); }); },
|
|
21
|
-
} as unknown as ClassMethodDecoratorContext & { runInitializers(instance: object): void };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe("Debug — method decorator", () => {
|
|
25
|
-
it("records a method:call timeline entry", () => {
|
|
26
|
-
const ctx = mockMethodCtx("doWork");
|
|
27
|
-
const original = vi.fn((_x: unknown) => "result");
|
|
28
|
-
|
|
29
|
-
Debug()(original as never, ctx);
|
|
30
|
-
|
|
31
|
-
class Comp { doWork(_x: unknown) { return "result"; } }
|
|
32
|
-
const instance = new Comp();
|
|
33
|
-
ctx.runInitializers(instance);
|
|
34
|
-
|
|
35
|
-
const registry = Registry.instance;
|
|
36
|
-
registry.registerComponent(instance, "Comp");
|
|
37
|
-
|
|
38
|
-
instance.doWork(42);
|
|
39
|
-
|
|
40
|
-
const entry = registry.getTimeline().find((t) => t.type === "method:call");
|
|
41
|
-
expect(entry).toBeDefined();
|
|
42
|
-
expect(entry?.label).toContain("doWork");
|
|
43
|
-
expect(entry?.data.args).toEqual([42]);
|
|
44
|
-
expect(entry?.data.result).toBe("result");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("records throw result when method throws", () => {
|
|
48
|
-
const ctx = mockMethodCtx("boom");
|
|
49
|
-
const original = () => { throw new Error("fail"); };
|
|
50
|
-
|
|
51
|
-
Debug()(original as never, ctx);
|
|
52
|
-
|
|
53
|
-
class Comp { boom() { throw new Error("fail"); } }
|
|
54
|
-
const instance = new Comp();
|
|
55
|
-
ctx.runInitializers(instance);
|
|
56
|
-
Registry.instance.registerComponent(instance, "Comp");
|
|
57
|
-
|
|
58
|
-
expect(() => (instance as unknown as Record<string, () => void>).boom()).toThrow("fail");
|
|
59
|
-
|
|
60
|
-
const entry = Registry.instance.getTimeline().find((t) => t.type === "method:call");
|
|
61
|
-
expect(entry?.data.result).toContain("throw");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("uses custom label from options", () => {
|
|
65
|
-
const ctx = mockMethodCtx("internal");
|
|
66
|
-
const original = vi.fn(() => undefined);
|
|
67
|
-
|
|
68
|
-
Debug({ label: "myLabel" })(original as never, ctx);
|
|
69
|
-
|
|
70
|
-
class Comp { internal() { return undefined; } }
|
|
71
|
-
const instance = new Comp();
|
|
72
|
-
ctx.runInitializers(instance);
|
|
73
|
-
Registry.instance.registerComponent(instance, "Comp");
|
|
74
|
-
(instance as unknown as Record<string, () => void>).internal();
|
|
75
|
-
|
|
76
|
-
const entry = Registry.instance.getTimeline().find((t) => t.type === "method:call");
|
|
77
|
-
expect(entry?.label).toContain("myLabel");
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// ── Field decorator (wrapping @State getter/setter) ───────────────────────────
|
|
82
|
-
|
|
83
|
-
describe("Debug — field decorator wrapping @State", () => {
|
|
84
|
-
it("registers signal and tracks updates", () => {
|
|
85
|
-
const registry = Registry.instance;
|
|
86
|
-
const instance = { count: 0 } as Record<string, unknown>;
|
|
87
|
-
|
|
88
|
-
// Simulate what @State() does: install a getter/setter on the instance
|
|
89
|
-
const s = signal(0);
|
|
90
|
-
Object.defineProperty(instance, "count", {
|
|
91
|
-
get() { return s(); },
|
|
92
|
-
set(v: number) { s.set(v); },
|
|
93
|
-
enumerable: true,
|
|
94
|
-
configurable: true,
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const initializers: Array<(this: unknown) => void> = [];
|
|
98
|
-
const ctx = {
|
|
99
|
-
kind: "field" as const,
|
|
100
|
-
name: "count",
|
|
101
|
-
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
102
|
-
} as ClassFieldDecoratorContext;
|
|
103
|
-
|
|
104
|
-
Debug()(undefined, ctx);
|
|
105
|
-
initializers.forEach((fn) => fn.call(instance));
|
|
106
|
-
|
|
107
|
-
const signals = registry.getSignals();
|
|
108
|
-
expect(signals.length).toBeGreaterThan(0);
|
|
109
|
-
expect(signals[0].label).toBe("count");
|
|
110
|
-
expect(signals[0].value).toBe(0);
|
|
111
|
-
|
|
112
|
-
// Read the property through the getter (covers the get() function body)
|
|
113
|
-
const readValue = (instance as Record<string, unknown>).count;
|
|
114
|
-
expect(readValue).toBe(0);
|
|
115
|
-
|
|
116
|
-
// Trigger update through the wrapped setter
|
|
117
|
-
(instance as Record<string, unknown>).count = 10;
|
|
118
|
-
|
|
119
|
-
// The set through the defined property calls updateSignal
|
|
120
|
-
expect(registry.getSignals()[0].value).toBe(10);
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// ── Field decorator (computed field) ─────────────────────────────────────────
|
|
125
|
-
|
|
126
|
-
describe("Debug — field decorator with computed value", () => {
|
|
127
|
-
it("registers computed signal and tracks updates", async () => {
|
|
128
|
-
const registry = Registry.instance;
|
|
129
|
-
const src = signal(1);
|
|
130
|
-
const c = computed(() => src() * 2);
|
|
131
|
-
|
|
132
|
-
const instance = { doubled: c } as Record<string, unknown>;
|
|
133
|
-
|
|
134
|
-
const initializers: Array<(this: unknown) => void> = [];
|
|
135
|
-
const ctx = {
|
|
136
|
-
kind: "field" as const,
|
|
137
|
-
name: "doubled",
|
|
138
|
-
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
139
|
-
} as ClassFieldDecoratorContext;
|
|
140
|
-
|
|
141
|
-
Debug()(undefined, ctx);
|
|
142
|
-
initializers.forEach((fn) => fn.call(instance));
|
|
143
|
-
|
|
144
|
-
const signals = registry.getSignals();
|
|
145
|
-
expect(signals.length).toBeGreaterThan(0);
|
|
146
|
-
expect(signals[0].label).toBe("doubled");
|
|
147
|
-
expect(signals[0].value).toBe(2);
|
|
148
|
-
|
|
149
|
-
src.set(5);
|
|
150
|
-
await Promise.resolve();
|
|
151
|
-
expect(registry.getSignals()[0].value).toBe(10);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("warns when field value is not a computed and not undefined", () => {
|
|
155
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
156
|
-
const instance = { myField: "not-computed" } as Record<string, unknown>;
|
|
157
|
-
|
|
158
|
-
const initializers: Array<(this: unknown) => void> = [];
|
|
159
|
-
const ctx = {
|
|
160
|
-
kind: "field" as const,
|
|
161
|
-
name: "myField",
|
|
162
|
-
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
163
|
-
} as ClassFieldDecoratorContext;
|
|
164
|
-
|
|
165
|
-
Debug()(undefined, ctx);
|
|
166
|
-
initializers.forEach((fn) => fn.call(instance));
|
|
167
|
-
|
|
168
|
-
expect(warn).toHaveBeenCalledWith(expect.stringContaining("[PraxisJS DevTools]"));
|
|
169
|
-
warn.mockRestore();
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("warns when computed field is replaced with non-computed value via setter", () => {
|
|
173
|
-
const registry = Registry.instance;
|
|
174
|
-
const src = signal(1);
|
|
175
|
-
const c = computed(() => src() * 2);
|
|
176
|
-
|
|
177
|
-
const instance = { doubled: c } as Record<string, unknown>;
|
|
178
|
-
|
|
179
|
-
const initializers: Array<(this: unknown) => void> = [];
|
|
180
|
-
const ctx = {
|
|
181
|
-
kind: "field" as const,
|
|
182
|
-
name: "doubled",
|
|
183
|
-
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
184
|
-
} as ClassFieldDecoratorContext;
|
|
185
|
-
|
|
186
|
-
Debug()(undefined, ctx);
|
|
187
|
-
initializers.forEach((fn) => fn.call(instance));
|
|
188
|
-
|
|
189
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
190
|
-
// Set a non-computed value through the property setter
|
|
191
|
-
(instance as Record<string, unknown>).doubled = "not-a-computed";
|
|
192
|
-
expect(warn).toHaveBeenCalledWith(expect.stringContaining("[PraxisJS DevTools]"));
|
|
193
|
-
warn.mockRestore();
|
|
194
|
-
|
|
195
|
-
void registry;
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("updates slot when computed field is replaced with a new computed", () => {
|
|
199
|
-
const registry = Registry.instance;
|
|
200
|
-
const src = signal(1);
|
|
201
|
-
const c1 = computed(() => src() * 2);
|
|
202
|
-
const c2 = computed(() => src() * 10);
|
|
203
|
-
|
|
204
|
-
const instance = { doubled: c1 } as Record<string, unknown>;
|
|
205
|
-
|
|
206
|
-
const initializers: Array<(this: unknown) => void> = [];
|
|
207
|
-
const ctx = {
|
|
208
|
-
kind: "field" as const,
|
|
209
|
-
name: "doubled",
|
|
210
|
-
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
211
|
-
} as ClassFieldDecoratorContext;
|
|
212
|
-
|
|
213
|
-
Debug()(undefined, ctx);
|
|
214
|
-
initializers.forEach((fn) => fn.call(instance));
|
|
215
|
-
|
|
216
|
-
// Replace with new computed
|
|
217
|
-
(instance as Record<string, unknown>).doubled = c2;
|
|
218
|
-
|
|
219
|
-
// The getter should now return c2
|
|
220
|
-
const newComputed = (instance as Record<string, unknown>).doubled;
|
|
221
|
-
expect(typeof newComputed).toBe("function");
|
|
222
|
-
|
|
223
|
-
void registry;
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("does not warn when field value is undefined", () => {
|
|
227
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
228
|
-
const instance = { myUndef: undefined } as Record<string, unknown>;
|
|
229
|
-
|
|
230
|
-
const initializers: Array<(this: unknown) => void> = [];
|
|
231
|
-
const ctx = {
|
|
232
|
-
kind: "field" as const,
|
|
233
|
-
name: "myUndef",
|
|
234
|
-
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
235
|
-
} as ClassFieldDecoratorContext;
|
|
236
|
-
|
|
237
|
-
Debug()(undefined, ctx);
|
|
238
|
-
initializers.forEach((fn) => fn.call(instance));
|
|
239
|
-
|
|
240
|
-
expect(warn).not.toHaveBeenCalled();
|
|
241
|
-
warn.mockRestore();
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// ── Async method decorator ────────────────────────────────────────────────────
|
|
246
|
-
|
|
247
|
-
describe("Debug — async method decorator", () => {
|
|
248
|
-
it("timing is recorded for the synchronous wrapper (documents limitation: async duration is not measured)", () => {
|
|
249
|
-
// @Debug wraps methods synchronously. For async methods, the recorded duration
|
|
250
|
-
// covers only until the Promise is returned, not until it resolves.
|
|
251
|
-
const ctx = mockMethodCtx("fetchData");
|
|
252
|
-
const original = vi.fn(async () => {
|
|
253
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
254
|
-
return "done";
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
Debug()(original as never, ctx);
|
|
258
|
-
|
|
259
|
-
class Comp { async fetchData() { return "done"; } }
|
|
260
|
-
const instance = new Comp();
|
|
261
|
-
ctx.runInitializers(instance);
|
|
262
|
-
Registry.instance.registerComponent(instance, "Comp");
|
|
263
|
-
|
|
264
|
-
// Call and await
|
|
265
|
-
const promise = (instance as unknown as Record<string, () => unknown>).fetchData() as Promise<unknown>;
|
|
266
|
-
expect(promise).toBeInstanceOf(Promise);
|
|
267
|
-
|
|
268
|
-
// Timeline entry is recorded synchronously (before the promise resolves)
|
|
269
|
-
const entry = Registry.instance.getTimeline().find((t) => t.type === "method:call");
|
|
270
|
-
expect(entry).toBeDefined();
|
|
271
|
-
expect(entry?.label).toContain("fetchData");
|
|
272
|
-
// Duration is near-zero since it only measured until Promise was returned
|
|
273
|
-
expect(typeof entry?.data.duration).toBe("number");
|
|
274
|
-
|
|
275
|
-
return promise;
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// ── Empty constructor name ─────────────────────────────────────────────────────
|
|
280
|
-
|
|
281
|
-
describe("Debug — class with empty constructor name", () => {
|
|
282
|
-
it("falls back gracefully when constructor.name is empty string", () => {
|
|
283
|
-
const ctx = mockMethodCtx("doWork");
|
|
284
|
-
const original = vi.fn(() => 42);
|
|
285
|
-
|
|
286
|
-
Debug()(original as never, ctx);
|
|
287
|
-
|
|
288
|
-
// Simulate a class whose constructor.name is empty (e.g. minified code)
|
|
289
|
-
const instance = Object.create({ constructor: { name: "" } }) as object;
|
|
290
|
-
ctx.runInitializers(instance);
|
|
291
|
-
|
|
292
|
-
expect(() => {
|
|
293
|
-
(instance as Record<string, () => void>).doWork();
|
|
294
|
-
}).not.toThrow();
|
|
295
|
-
|
|
296
|
-
const entry = Registry.instance.getTimeline().find((t) => t.type === "method:call");
|
|
297
|
-
expect(entry).toBeDefined();
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
// ── Getter decorator ──────────────────────────────────────────────────────────
|
|
302
|
-
|
|
303
|
-
describe("Debug — getter decorator", () => {
|
|
304
|
-
it("returns undefined (void) for getter kind", async () => {
|
|
305
|
-
const initializers: Array<(this: unknown) => void> = [];
|
|
306
|
-
const ctx = {
|
|
307
|
-
kind: "getter" as const,
|
|
308
|
-
name: "myGetter",
|
|
309
|
-
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
310
|
-
} as ClassGetterDecoratorContext;
|
|
311
|
-
|
|
312
|
-
const src = signal(3);
|
|
313
|
-
const result = Debug()(() => src() * 2 as unknown as never, ctx);
|
|
314
|
-
expect(result).toBeUndefined();
|
|
315
|
-
|
|
316
|
-
// Run the initializer on a fake instance
|
|
317
|
-
const instance = { myGetter: computed(() => src() * 2) };
|
|
318
|
-
initializers.forEach((fn) => fn.call(instance));
|
|
319
|
-
|
|
320
|
-
// Wait for queueMicrotask
|
|
321
|
-
await new Promise<void>((r) => setTimeout(r, 0));
|
|
322
|
-
|
|
323
|
-
const signals = Registry.instance.getSignals();
|
|
324
|
-
expect(signals.length).toBeGreaterThan(0);
|
|
325
|
-
|
|
326
|
-
// Update src to trigger the subscribe callback (covers the updateSignal path)
|
|
327
|
-
const signalsBefore = Registry.instance.getSignals()[0]?.value;
|
|
328
|
-
src.set(10);
|
|
329
|
-
// The computed updates: src() * 2 = 20
|
|
330
|
-
await new Promise<void>((r) => setTimeout(r, 0));
|
|
331
|
-
expect(signalsBefore).toBe(6); // 3 * 2 = 6
|
|
332
|
-
});
|
|
333
|
-
});
|
|
@@ -1,123 +0,0 @@
|
|
|
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
|
-
|
|
77
|
-
it("handler that throws does not prevent subsequent handlers from being called", () => {
|
|
78
|
-
const bus = new EventBus();
|
|
79
|
-
const h1 = vi.fn().mockImplementation(() => { throw new Error("h1 failed"); });
|
|
80
|
-
const h2 = vi.fn();
|
|
81
|
-
const h3 = vi.fn();
|
|
82
|
-
bus.on("event", h1);
|
|
83
|
-
bus.on("event", h2);
|
|
84
|
-
bus.on("event", h3);
|
|
85
|
-
expect(() => bus.emit("event", "payload")).toThrow();
|
|
86
|
-
expect(h2).toHaveBeenCalledWith("payload");
|
|
87
|
-
expect(h3).toHaveBeenCalledWith("payload");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("handlers are called in registration order", () => {
|
|
91
|
-
const bus = new EventBus();
|
|
92
|
-
const order: number[] = [];
|
|
93
|
-
bus.on("event", () => { order.push(1); });
|
|
94
|
-
bus.on("event", () => { order.push(2); });
|
|
95
|
-
bus.on("event", () => { order.push(3); });
|
|
96
|
-
bus.emit("event", null);
|
|
97
|
-
expect(order).toEqual([1, 2, 3]);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("error from a throwing handler is re-thrown after all handlers run", () => {
|
|
101
|
-
const bus = new EventBus();
|
|
102
|
-
const err = new Error("boom");
|
|
103
|
-
bus.on("event", () => { throw err; });
|
|
104
|
-
const h2 = vi.fn();
|
|
105
|
-
bus.on("event", h2);
|
|
106
|
-
expect(() => bus.emit("event", "x")).toThrow(err);
|
|
107
|
-
expect(h2).toHaveBeenCalledOnce();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("throws AggregateError when multiple handlers throw", () => {
|
|
111
|
-
const bus = new EventBus();
|
|
112
|
-
bus.on("event", () => { throw new Error("first"); });
|
|
113
|
-
bus.on("event", () => { throw new Error("second"); });
|
|
114
|
-
let caught: unknown;
|
|
115
|
-
try {
|
|
116
|
-
bus.emit("event", null);
|
|
117
|
-
} catch (e) {
|
|
118
|
-
caught = e;
|
|
119
|
-
}
|
|
120
|
-
expect(caught).toBeInstanceOf(AggregateError);
|
|
121
|
-
expect((caught as AggregateError).errors).toHaveLength(2);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
// Mock JSX tab components before importing the plugin index files so that
|
|
4
|
-
// their @shared/* aliases (only resolved by the Vite build config) are never
|
|
5
|
-
// evaluated at test time.
|
|
6
|
-
vi.mock("../plugins/components/components-tab", () => ({
|
|
7
|
-
ComponentsTab: class MockComponentsTab {},
|
|
8
|
-
}));
|
|
9
|
-
vi.mock("../plugins/signals/signals-tab", () => ({
|
|
10
|
-
SignalsTab: class MockSignalsTab {},
|
|
11
|
-
}));
|
|
12
|
-
vi.mock("../plugins/timeline/timeline-tab", () => ({
|
|
13
|
-
TimelineTab: class MockTimelineTab {},
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
import { ComponentsPlugin } from "../plugins/components";
|
|
17
|
-
import { SignalsPlugin } from "../plugins/signals";
|
|
18
|
-
import { TimelinePlugin } from "../plugins/timeline";
|
|
19
|
-
import { TYPE_META, FILTERS } from "../plugins/timeline/constants";
|
|
20
|
-
|
|
21
|
-
describe("ComponentsPlugin", () => {
|
|
22
|
-
it("has id 'components'", () => {
|
|
23
|
-
expect(ComponentsPlugin.id).toBe("components");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("has label 'Components'", () => {
|
|
27
|
-
expect(ComponentsPlugin.label).toBe("Components");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("exposes a component", () => {
|
|
31
|
-
expect(ComponentsPlugin.component).toBeDefined();
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe("SignalsPlugin", () => {
|
|
36
|
-
it("has id 'signals'", () => {
|
|
37
|
-
expect(SignalsPlugin.id).toBe("signals");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("has label 'Signals'", () => {
|
|
41
|
-
expect(SignalsPlugin.label).toBe("Signals");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("exposes a component", () => {
|
|
45
|
-
expect(SignalsPlugin.component).toBeDefined();
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe("TimelinePlugin", () => {
|
|
50
|
-
it("has id 'timeline'", () => {
|
|
51
|
-
expect(TimelinePlugin.id).toBe("timeline");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("has label 'Timeline'", () => {
|
|
55
|
-
expect(TimelinePlugin.label).toBe("Timeline");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("exposes a component", () => {
|
|
59
|
-
expect(TimelinePlugin.component).toBeDefined();
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("TYPE_META", () => {
|
|
64
|
-
it("contains all TimelineEventType entries", () => {
|
|
65
|
-
expect(TYPE_META["signal:change"].label).toBe("signal");
|
|
66
|
-
expect(TYPE_META["component:render"].label).toBe("render");
|
|
67
|
-
expect(TYPE_META["component:mount"].label).toBe("mount");
|
|
68
|
-
expect(TYPE_META["component:unmount"].label).toBe("unmount");
|
|
69
|
-
expect(TYPE_META["lifecycle"].label).toBe("lifecycle");
|
|
70
|
-
expect(TYPE_META["method:call"].label).toBe("method");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("each entry has a cls string", () => {
|
|
74
|
-
for (const meta of Object.values(TYPE_META)) {
|
|
75
|
-
expect(typeof meta.cls).toBe("string");
|
|
76
|
-
expect(meta.cls.length).toBeGreaterThan(0);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe("FILTERS", () => {
|
|
82
|
-
it("first filter is 'all'", () => {
|
|
83
|
-
expect(FILTERS[0].value).toBe("all");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("contains expected filter values", () => {
|
|
87
|
-
const values = FILTERS.map((f) => f.value);
|
|
88
|
-
expect(values).toContain("signal:change");
|
|
89
|
-
expect(values).toContain("component:render");
|
|
90
|
-
expect(values).toContain("component:mount");
|
|
91
|
-
expect(values).toContain("lifecycle");
|
|
92
|
-
expect(values).toContain("method:call");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("each filter has a non-empty label", () => {
|
|
96
|
-
for (const f of FILTERS) {
|
|
97
|
-
expect(typeof f.label).toBe("string");
|
|
98
|
-
expect(f.label.length).toBeGreaterThan(0);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
});
|