@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/CHANGELOG.md +27 -0
- package/assets/uno.generated.css +4 -1
- package/dist/index.js +219 -216
- package/package.json +6 -6
- package/src/__tests__/debug.test.ts +262 -0
- package/src/__tests__/event-bus.test.ts +76 -0
- package/src/__tests__/format-time.test.ts +52 -0
- package/src/__tests__/format-value.test.ts +46 -0
- package/src/__tests__/registry.test.ts +331 -0
- package/src/__tests__/trace.test.ts +149 -0
- package/src/plugins/timeline/components/timeline-row.tsx +5 -3
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { beforeEach, describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Registry } from "../core/registry";
|
|
4
|
+
|
|
5
|
+
// Reset singleton before each test to keep tests isolated
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
(Registry as unknown as { _instance: Registry | null })._instance = null;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe("Registry singleton", () => {
|
|
11
|
+
it("returns the same instance on subsequent accesses", () => {
|
|
12
|
+
const a = Registry.instance;
|
|
13
|
+
const b = Registry.instance;
|
|
14
|
+
expect(a).toBe(b);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("creates a fresh instance after reset", () => {
|
|
18
|
+
const first = Registry.instance;
|
|
19
|
+
(Registry as unknown as { _instance: Registry | null })._instance = null;
|
|
20
|
+
const second = Registry.instance;
|
|
21
|
+
expect(first).not.toBe(second);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("Registry.registerSignal", () => {
|
|
26
|
+
it("stores the signal and emits 'signal:registered'", () => {
|
|
27
|
+
const registry = Registry.instance;
|
|
28
|
+
const handler = vi.fn();
|
|
29
|
+
registry.bus.on("signal:registered", handler);
|
|
30
|
+
|
|
31
|
+
const instance = {};
|
|
32
|
+
registry.registerSignal(instance, "count", 0, "Counter");
|
|
33
|
+
|
|
34
|
+
const signals = registry.getSignals();
|
|
35
|
+
expect(signals).toHaveLength(1);
|
|
36
|
+
expect(signals[0].label).toBe("count");
|
|
37
|
+
expect(signals[0].value).toBe(0);
|
|
38
|
+
expect(signals[0].componentName).toBe("Counter");
|
|
39
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("builds the signal id as componentId:key", () => {
|
|
43
|
+
const registry = Registry.instance;
|
|
44
|
+
const instance = {};
|
|
45
|
+
registry.registerSignal(instance, "name", "Alice", "MyComp");
|
|
46
|
+
const signal = registry.getSignals()[0];
|
|
47
|
+
expect(signal.id).toMatch(/:.+/);
|
|
48
|
+
expect(signal.id.endsWith(":name")).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("initialises history with the initial value", () => {
|
|
52
|
+
const registry = Registry.instance;
|
|
53
|
+
registry.registerSignal({}, "x", 42, "Comp");
|
|
54
|
+
const signal = registry.getSignals()[0];
|
|
55
|
+
expect(signal.history).toHaveLength(1);
|
|
56
|
+
expect(signal.history[0].value).toBe(42);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("Registry.updateSignal", () => {
|
|
61
|
+
it("updates the value and appends to history", () => {
|
|
62
|
+
const registry = Registry.instance;
|
|
63
|
+
const instance = {};
|
|
64
|
+
registry.registerSignal(instance, "count", 0, "Counter");
|
|
65
|
+
registry.updateSignal(instance, "count", 1, 0);
|
|
66
|
+
|
|
67
|
+
const signal = registry.getSignals()[0];
|
|
68
|
+
expect(signal.value).toBe(1);
|
|
69
|
+
expect(signal.history).toHaveLength(2);
|
|
70
|
+
expect(signal.history[1].value).toBe(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("emits 'signal:changed' with old and new value", () => {
|
|
74
|
+
const registry = Registry.instance;
|
|
75
|
+
const handler = vi.fn();
|
|
76
|
+
registry.bus.on("signal:changed", handler);
|
|
77
|
+
|
|
78
|
+
const instance = {};
|
|
79
|
+
registry.registerSignal(instance, "val", "old", "Comp");
|
|
80
|
+
registry.updateSignal(instance, "val", "new", "old");
|
|
81
|
+
|
|
82
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
83
|
+
const payload = handler.mock.calls[0][0] as { entry: { value: unknown }; oldValue: unknown };
|
|
84
|
+
expect(payload.entry.value).toBe("new");
|
|
85
|
+
expect(payload.oldValue).toBe("old");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("caps history at 20 entries", () => {
|
|
89
|
+
const registry = Registry.instance;
|
|
90
|
+
const instance = {};
|
|
91
|
+
registry.registerSignal(instance, "n", 0, "Comp");
|
|
92
|
+
for (let i = 1; i <= 25; i++) {
|
|
93
|
+
registry.updateSignal(instance, "n", i, i - 1);
|
|
94
|
+
}
|
|
95
|
+
const signal = registry.getSignals()[0];
|
|
96
|
+
expect(signal.history.length).toBe(20);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("does nothing when the signal does not exist", () => {
|
|
100
|
+
const registry = Registry.instance;
|
|
101
|
+
const handler = vi.fn();
|
|
102
|
+
registry.bus.on("signal:changed", handler);
|
|
103
|
+
registry.updateSignal({}, "missing", 1, 0);
|
|
104
|
+
expect(handler).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("pushes a 'signal:change' timeline entry", () => {
|
|
108
|
+
const registry = Registry.instance;
|
|
109
|
+
const instance = {};
|
|
110
|
+
registry.registerSignal(instance, "x", 0, "Comp");
|
|
111
|
+
registry.updateSignal(instance, "x", 5, 0);
|
|
112
|
+
const timeline = registry.getTimeline();
|
|
113
|
+
const entry = timeline.find((t) => t.type === "signal:change");
|
|
114
|
+
expect(entry).toBeDefined();
|
|
115
|
+
expect(entry?.data.new).toBe(5);
|
|
116
|
+
expect(entry?.data.old).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("Registry.registerComponent", () => {
|
|
121
|
+
it("stores the component and emits 'component:registered'", () => {
|
|
122
|
+
const registry = Registry.instance;
|
|
123
|
+
const handler = vi.fn();
|
|
124
|
+
registry.bus.on("component:registered", handler);
|
|
125
|
+
|
|
126
|
+
const instance = {};
|
|
127
|
+
registry.registerComponent(instance, "MyComp");
|
|
128
|
+
|
|
129
|
+
const components = registry.getComponents();
|
|
130
|
+
expect(components).toHaveLength(1);
|
|
131
|
+
expect(components[0].name).toBe("MyComp");
|
|
132
|
+
expect(components[0].renderCount).toBe(0);
|
|
133
|
+
expect(components[0].status).toBe("mounted");
|
|
134
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("Registry.recordRender", () => {
|
|
139
|
+
it("increments renderCount and emits 'component:render'", () => {
|
|
140
|
+
const registry = Registry.instance;
|
|
141
|
+
const handler = vi.fn();
|
|
142
|
+
registry.bus.on("component:render", handler);
|
|
143
|
+
|
|
144
|
+
const instance = {};
|
|
145
|
+
registry.registerComponent(instance, "App");
|
|
146
|
+
registry.recordRender(instance, 5.2);
|
|
147
|
+
|
|
148
|
+
const comp = registry.getComponents()[0];
|
|
149
|
+
expect(comp.renderCount).toBe(1);
|
|
150
|
+
expect(comp.lastRenderDuration).toBe(5.2);
|
|
151
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("accumulates renderCount across multiple renders", () => {
|
|
155
|
+
const registry = Registry.instance;
|
|
156
|
+
const instance = {};
|
|
157
|
+
registry.registerComponent(instance, "App");
|
|
158
|
+
registry.recordRender(instance, 1);
|
|
159
|
+
registry.recordRender(instance, 2);
|
|
160
|
+
registry.recordRender(instance, 3);
|
|
161
|
+
expect(registry.getComponents()[0].renderCount).toBe(3);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("does nothing when the component does not exist", () => {
|
|
165
|
+
const registry = Registry.instance;
|
|
166
|
+
const handler = vi.fn();
|
|
167
|
+
registry.bus.on("component:render", handler);
|
|
168
|
+
registry.recordRender({}, 1);
|
|
169
|
+
expect(handler).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("pushes a 'component:render' timeline entry", () => {
|
|
173
|
+
const registry = Registry.instance;
|
|
174
|
+
const instance = {};
|
|
175
|
+
registry.registerComponent(instance, "App");
|
|
176
|
+
registry.recordRender(instance, 3.5);
|
|
177
|
+
const entry = registry.getTimeline().find((t) => t.type === "component:render");
|
|
178
|
+
expect(entry).toBeDefined();
|
|
179
|
+
expect(entry?.data.duration).toBe(3.5);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("Registry.recordLifecycle", () => {
|
|
184
|
+
it("records a lifecycle event and emits 'lifecycle'", () => {
|
|
185
|
+
const registry = Registry.instance;
|
|
186
|
+
const handler = vi.fn();
|
|
187
|
+
registry.bus.on("lifecycle", handler);
|
|
188
|
+
|
|
189
|
+
const instance = {};
|
|
190
|
+
registry.registerComponent(instance, "Comp");
|
|
191
|
+
registry.recordLifecycle(instance, "onMount");
|
|
192
|
+
|
|
193
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
194
|
+
const payload = handler.mock.calls[0][0] as { hook: string; name: string };
|
|
195
|
+
expect(payload.hook).toBe("onMount");
|
|
196
|
+
expect(payload.name).toBe("Comp");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("removes component and its signals on 'onUnmount'", () => {
|
|
200
|
+
const registry = Registry.instance;
|
|
201
|
+
const instance = {};
|
|
202
|
+
registry.registerComponent(instance, "Comp");
|
|
203
|
+
registry.registerSignal(instance, "val", 1, "Comp");
|
|
204
|
+
|
|
205
|
+
expect(registry.getComponents()).toHaveLength(1);
|
|
206
|
+
expect(registry.getSignals()).toHaveLength(1);
|
|
207
|
+
|
|
208
|
+
registry.recordLifecycle(instance, "onUnmount");
|
|
209
|
+
|
|
210
|
+
expect(registry.getComponents()).toHaveLength(0);
|
|
211
|
+
expect(registry.getSignals()).toHaveLength(0);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("emits 'component:unmount' on onUnmount", () => {
|
|
215
|
+
const registry = Registry.instance;
|
|
216
|
+
const handler = vi.fn();
|
|
217
|
+
registry.bus.on("component:unmount", handler);
|
|
218
|
+
|
|
219
|
+
const instance = {};
|
|
220
|
+
registry.registerComponent(instance, "Comp");
|
|
221
|
+
registry.recordLifecycle(instance, "onUnmount");
|
|
222
|
+
|
|
223
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("maps onBeforeMount hook to 'component:mount' timeline type", () => {
|
|
227
|
+
const registry = Registry.instance;
|
|
228
|
+
const instance = {};
|
|
229
|
+
registry.registerComponent(instance, "Comp");
|
|
230
|
+
registry.recordLifecycle(instance, "onBeforeMount");
|
|
231
|
+
const entry = registry.getTimeline().find((t) => t.type === "component:mount");
|
|
232
|
+
expect(entry).toBeDefined();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("maps onUnmount hook to 'component:unmount' timeline type", () => {
|
|
236
|
+
const registry = Registry.instance;
|
|
237
|
+
const instance = {};
|
|
238
|
+
registry.registerComponent(instance, "Comp");
|
|
239
|
+
registry.recordLifecycle(instance, "onUnmount");
|
|
240
|
+
const entry = registry.getTimeline().find((t) => t.type === "component:unmount");
|
|
241
|
+
expect(entry).toBeDefined();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("maps other hooks to 'lifecycle' timeline type", () => {
|
|
245
|
+
const registry = Registry.instance;
|
|
246
|
+
const instance = {};
|
|
247
|
+
registry.registerComponent(instance, "Comp");
|
|
248
|
+
registry.recordLifecycle(instance, "onUpdate");
|
|
249
|
+
const entry = registry.getTimeline().find((t) => t.type === "lifecycle");
|
|
250
|
+
expect(entry).toBeDefined();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("does nothing when the component does not exist", () => {
|
|
254
|
+
const registry = Registry.instance;
|
|
255
|
+
const handler = vi.fn();
|
|
256
|
+
registry.bus.on("lifecycle", handler);
|
|
257
|
+
registry.recordLifecycle({}, "onMount");
|
|
258
|
+
expect(handler).not.toHaveBeenCalled();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("Registry.recordMethodCall", () => {
|
|
263
|
+
it("pushes a 'method:call' timeline entry", () => {
|
|
264
|
+
const registry = Registry.instance;
|
|
265
|
+
const timelineHandler = vi.fn();
|
|
266
|
+
registry.bus.on("timeline:push", timelineHandler);
|
|
267
|
+
|
|
268
|
+
const instance = {};
|
|
269
|
+
registry.recordMethodCall(instance, "doSomething", [1, 2], "result", 3.14, "MyComp");
|
|
270
|
+
|
|
271
|
+
const entry = registry.getTimeline().find((t) => t.type === "method:call");
|
|
272
|
+
expect(entry).toBeDefined();
|
|
273
|
+
expect(entry?.label).toBe("MyComp.doSomething()");
|
|
274
|
+
expect(entry?.data.args).toEqual([1, 2]);
|
|
275
|
+
expect(entry?.data.result).toBe("result");
|
|
276
|
+
expect(entry?.data.duration).toBe(3.14);
|
|
277
|
+
expect(timelineHandler).toHaveBeenCalledOnce();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("Registry.getSignalsByComponent", () => {
|
|
282
|
+
it("returns only signals belonging to the given component", () => {
|
|
283
|
+
const registry = Registry.instance;
|
|
284
|
+
const inst1 = {};
|
|
285
|
+
const inst2 = {};
|
|
286
|
+
registry.registerComponent(inst1, "Comp1");
|
|
287
|
+
registry.registerComponent(inst2, "Comp2");
|
|
288
|
+
registry.registerSignal(inst1, "a", 1, "Comp1");
|
|
289
|
+
registry.registerSignal(inst2, "b", 2, "Comp2");
|
|
290
|
+
|
|
291
|
+
const comp1Id = registry.getComponents().find((c) => c.name === "Comp1")!.id;
|
|
292
|
+
const signals = registry.getSignalsByComponent(comp1Id);
|
|
293
|
+
expect(signals).toHaveLength(1);
|
|
294
|
+
expect(signals[0].label).toBe("a");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("Registry.recordLifecycle — onUnmount cross-component isolation", () => {
|
|
299
|
+
it("only removes signals belonging to the unmounted component", () => {
|
|
300
|
+
const registry = Registry.instance;
|
|
301
|
+
const inst1 = {};
|
|
302
|
+
const inst2 = {};
|
|
303
|
+
registry.registerComponent(inst1, "Comp1");
|
|
304
|
+
registry.registerComponent(inst2, "Comp2");
|
|
305
|
+
registry.registerSignal(inst1, "a", 1, "Comp1");
|
|
306
|
+
registry.registerSignal(inst2, "b", 2, "Comp2");
|
|
307
|
+
|
|
308
|
+
registry.recordLifecycle(inst1, "onUnmount");
|
|
309
|
+
|
|
310
|
+
// inst1's signal is gone, inst2's signal remains
|
|
311
|
+
const remaining = registry.getSignals();
|
|
312
|
+
expect(remaining).toHaveLength(1);
|
|
313
|
+
expect(remaining[0].label).toBe("b");
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("Registry timeline cap", () => {
|
|
318
|
+
it("caps timeline at 200 entries", () => {
|
|
319
|
+
const registry = Registry.instance;
|
|
320
|
+
const instance = {};
|
|
321
|
+
registry.registerComponent(instance, "Comp");
|
|
322
|
+
|
|
323
|
+
// Each updateSignal pushes one timeline entry (plus the registerSignal = 0 entries)
|
|
324
|
+
registry.registerSignal(instance, "val", 0, "Comp");
|
|
325
|
+
for (let i = 1; i <= 210; i++) {
|
|
326
|
+
registry.updateSignal(instance, "val", i, i - 1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
expect(registry.getTimeline().length).toBe(200);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { beforeEach, describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Registry } from "../core/registry";
|
|
4
|
+
import { Trace } from "../decorators/trace";
|
|
5
|
+
|
|
6
|
+
// Reset the Registry singleton before each test
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
(Registry as unknown as { _instance: Registry | null })._instance = null;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
type AnyConstructor = new (...args: any[]) => any;
|
|
15
|
+
|
|
16
|
+
function applyTrace<T extends AnyConstructor>(cls: T): T {
|
|
17
|
+
return Trace()(cls, {} as ClassDecoratorContext);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── Trace decorator ───────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
describe("Trace — render()", () => {
|
|
23
|
+
it("records a render event after calling render()", () => {
|
|
24
|
+
class MyComp {
|
|
25
|
+
render() { return "html"; }
|
|
26
|
+
}
|
|
27
|
+
const Traced = applyTrace(MyComp);
|
|
28
|
+
const instance = new Traced() as { render: () => string };
|
|
29
|
+
|
|
30
|
+
const registry = Registry.instance;
|
|
31
|
+
registry.registerComponent(instance, "MyComp");
|
|
32
|
+
|
|
33
|
+
instance.render();
|
|
34
|
+
|
|
35
|
+
const renders = registry.getTimeline().filter((e) => e.type === "component:render");
|
|
36
|
+
expect(renders).toHaveLength(1);
|
|
37
|
+
expect(renders[0].label).toBe("<MyComp>");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns the original render result", () => {
|
|
41
|
+
class Box {
|
|
42
|
+
render() { return "<div>box</div>"; }
|
|
43
|
+
}
|
|
44
|
+
const Traced = applyTrace(Box);
|
|
45
|
+
const instance = new Traced() as { render: () => string };
|
|
46
|
+
|
|
47
|
+
Registry.instance.registerComponent(instance, "Box");
|
|
48
|
+
expect(instance.render()).toBe("<div>box</div>");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("wraps render even when it takes arguments", () => {
|
|
52
|
+
class Comp {
|
|
53
|
+
render(ctx: string) { return ctx; }
|
|
54
|
+
}
|
|
55
|
+
const Traced = applyTrace(Comp);
|
|
56
|
+
const instance = new Traced() as { render: (ctx: string) => string };
|
|
57
|
+
|
|
58
|
+
Registry.instance.registerComponent(instance, "Comp");
|
|
59
|
+
const result = instance.render("ctx-arg");
|
|
60
|
+
expect(result).toBe("ctx-arg");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not wrap render when the class has no render method", () => {
|
|
64
|
+
class NoRender {}
|
|
65
|
+
const Traced = applyTrace(NoRender);
|
|
66
|
+
const instance = new Traced() as Record<string, unknown>;
|
|
67
|
+
expect(instance.render).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("Trace — onBeforeMount()", () => {
|
|
72
|
+
it("registers the component in the registry on onBeforeMount", () => {
|
|
73
|
+
class Widget {
|
|
74
|
+
onBeforeMount() {}
|
|
75
|
+
}
|
|
76
|
+
const Traced = applyTrace(Widget);
|
|
77
|
+
const instance = new Traced() as { onBeforeMount: () => void };
|
|
78
|
+
|
|
79
|
+
const registry = Registry.instance;
|
|
80
|
+
instance.onBeforeMount();
|
|
81
|
+
|
|
82
|
+
const components = registry.getComponents();
|
|
83
|
+
expect(components).toHaveLength(1);
|
|
84
|
+
expect(components[0].name).toBe("Widget");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("calls the original onBeforeMount when it exists", () => {
|
|
88
|
+
const originalSpy = vi.fn();
|
|
89
|
+
class Panel {
|
|
90
|
+
onBeforeMount() { originalSpy(); }
|
|
91
|
+
}
|
|
92
|
+
const Traced = applyTrace(Panel);
|
|
93
|
+
const instance = new Traced() as { onBeforeMount: () => void };
|
|
94
|
+
|
|
95
|
+
instance.onBeforeMount();
|
|
96
|
+
expect(originalSpy).toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("records a component:mount timeline event on onBeforeMount", () => {
|
|
100
|
+
class App {
|
|
101
|
+
onBeforeMount() {}
|
|
102
|
+
}
|
|
103
|
+
const Traced = applyTrace(App);
|
|
104
|
+
const instance = new Traced() as { onBeforeMount: () => void };
|
|
105
|
+
|
|
106
|
+
instance.onBeforeMount();
|
|
107
|
+
|
|
108
|
+
const mounts = Registry.instance.getTimeline().filter((e) => e.type === "component:mount");
|
|
109
|
+
expect(mounts).toHaveLength(1);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("Trace — lifecycle hooks", () => {
|
|
114
|
+
const hooks = ["onMount", "onUnmount", "onBeforeUpdate", "onUpdate", "onAfterUpdate"] as const;
|
|
115
|
+
|
|
116
|
+
for (const hook of hooks) {
|
|
117
|
+
it(`wraps ${hook} and records the lifecycle event`, () => {
|
|
118
|
+
class Comp {
|
|
119
|
+
onBeforeMount() {}
|
|
120
|
+
}
|
|
121
|
+
const Traced = applyTrace(Comp);
|
|
122
|
+
const instance = new Traced() as unknown as Record<string, () => void>;
|
|
123
|
+
|
|
124
|
+
// Register the component first via onBeforeMount
|
|
125
|
+
instance.onBeforeMount();
|
|
126
|
+
|
|
127
|
+
instance[hook]?.();
|
|
128
|
+
|
|
129
|
+
const events = Registry.instance.getTimeline().filter(
|
|
130
|
+
(e) => e.data.hook === hook,
|
|
131
|
+
);
|
|
132
|
+
expect(events.length).toBeGreaterThanOrEqual(1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it(`${hook}: calls original implementation if it exists`, () => {
|
|
136
|
+
const spy = vi.fn();
|
|
137
|
+
class Comp {
|
|
138
|
+
onBeforeMount() {}
|
|
139
|
+
}
|
|
140
|
+
(Comp.prototype as unknown as Record<string, unknown>)[hook] = spy;
|
|
141
|
+
const Traced = applyTrace(Comp);
|
|
142
|
+
const instance = new Traced() as unknown as Record<string, () => void>;
|
|
143
|
+
|
|
144
|
+
instance.onBeforeMount();
|
|
145
|
+
instance[hook]?.();
|
|
146
|
+
expect(spy).toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { time } from "@utils/format-time";
|
|
2
2
|
|
|
3
3
|
import { StatefulComponent } from "@praxisjs/core";
|
|
4
|
-
import { Component, State } from "@praxisjs/decorators";
|
|
5
|
-
|
|
4
|
+
import { Component, Prop, State } from "@praxisjs/decorators";
|
|
6
5
|
|
|
7
6
|
import { Badge } from "./badge";
|
|
8
7
|
|
|
@@ -11,9 +10,12 @@ import type { TimelineEntry } from "@core/types";
|
|
|
11
10
|
@Component()
|
|
12
11
|
export class TimelineRow extends StatefulComponent {
|
|
13
12
|
@State() open = false;
|
|
13
|
+
@Prop() entry: TimelineEntry | undefined;
|
|
14
14
|
|
|
15
15
|
render() {
|
|
16
|
-
const
|
|
16
|
+
const entry = this.entry;
|
|
17
|
+
if (!entry) return null;
|
|
18
|
+
|
|
17
19
|
const hasData = Object.keys(entry.data).length > 0;
|
|
18
20
|
|
|
19
21
|
return (
|