agent-react-devtools 0.0.0 → 0.1.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.
@@ -0,0 +1,189 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ formatTree,
4
+ formatComponent,
5
+ formatSearchResults,
6
+ formatCount,
7
+ formatStatus,
8
+ formatProfileReport,
9
+ formatSlowest,
10
+ formatRerenders,
11
+ formatTimeline,
12
+ } from '../formatters.js';
13
+ import type { TreeNode } from '../component-tree.js';
14
+ import type { InspectedElement, StatusInfo, ComponentRenderReport } from '../types.js';
15
+ import type { TimelineEntry } from '../profiler.js';
16
+
17
+ describe('formatTree', () => {
18
+ it('should format empty tree', () => {
19
+ expect(formatTree([])).toContain('No components');
20
+ });
21
+
22
+ it('should format a simple tree', () => {
23
+ const nodes: TreeNode[] = [
24
+ { id: 1, label: '@c1', displayName: 'App', type: 'function', key: null, parentId: null, children: [2, 3], depth: 0 },
25
+ { id: 2, label: '@c2', displayName: 'Header', type: 'memo', key: null, parentId: 1, children: [], depth: 1 },
26
+ { id: 3, label: '@c3', displayName: 'Footer', type: 'host', key: null, parentId: 1, children: [], depth: 1 },
27
+ ];
28
+
29
+ const result = formatTree(nodes);
30
+ expect(result).toContain('@c1 [fn] "App"');
31
+ expect(result).toContain('@c2 [memo] "Header"');
32
+ expect(result).toContain('@c3 [host] "Footer"');
33
+ expect(result).toContain('├─');
34
+ expect(result).toContain('└─');
35
+ });
36
+
37
+ it('should show keys', () => {
38
+ const nodes: TreeNode[] = [
39
+ { id: 1, label: '@c1', displayName: 'List', type: 'function', key: null, parentId: null, children: [2], depth: 0 },
40
+ { id: 2, label: '@c2', displayName: 'Item', type: 'function', key: 'item-1', parentId: 1, children: [], depth: 1 },
41
+ ];
42
+
43
+ const result = formatTree(nodes);
44
+ expect(result).toContain('key="item-1"');
45
+ });
46
+ });
47
+
48
+ describe('formatComponent', () => {
49
+ it('should format an inspected element', () => {
50
+ const element: InspectedElement = {
51
+ id: 5,
52
+ displayName: 'UserProfile',
53
+ type: 'function',
54
+ key: null,
55
+ props: { userId: 42, theme: 'dark' },
56
+ state: { isEditing: false },
57
+ hooks: [
58
+ { name: 'useState', value: false },
59
+ { name: 'useEffect', value: undefined },
60
+ { name: 'useCallback', value: 'ƒ' },
61
+ ],
62
+ renderedAt: null,
63
+ };
64
+
65
+ const result = formatComponent(element, '@c5');
66
+ expect(result).toContain('@c5 [fn] "UserProfile"');
67
+ expect(result).toContain('props:');
68
+ expect(result).toContain(' userId: 42');
69
+ expect(result).toContain(' theme: "dark"');
70
+ expect(result).toContain('state:');
71
+ expect(result).toContain(' isEditing: false');
72
+ expect(result).toContain('hooks:');
73
+ expect(result).toContain(' useState: false');
74
+ });
75
+ });
76
+
77
+ describe('formatSearchResults', () => {
78
+ it('should format empty results', () => {
79
+ expect(formatSearchResults([])).toContain('No components found');
80
+ });
81
+
82
+ it('should format results', () => {
83
+ const results: TreeNode[] = [
84
+ { id: 2, label: '@c2', displayName: 'UserProfile', type: 'function', key: null, parentId: 1, children: [], depth: 1 },
85
+ { id: 3, label: '@c3', displayName: 'UserCard', type: 'memo', key: 'bob', parentId: 1, children: [], depth: 1 },
86
+ ];
87
+
88
+ const result = formatSearchResults(results);
89
+ expect(result).toContain('@c2 [fn] "UserProfile"');
90
+ expect(result).toContain('@c3 [memo] "UserCard"');
91
+ expect(result).toContain('key="bob"');
92
+ });
93
+ });
94
+
95
+ describe('formatCount', () => {
96
+ it('should format component counts', () => {
97
+ const counts = { function: 10, memo: 3, host: 25 };
98
+ const result = formatCount(counts);
99
+ expect(result).toContain('38 components');
100
+ expect(result).toContain('host:25');
101
+ expect(result).toContain('fn:10');
102
+ expect(result).toContain('memo:3');
103
+ });
104
+ });
105
+
106
+ describe('formatStatus', () => {
107
+ it('should format status info', () => {
108
+ const status: StatusInfo = {
109
+ daemonRunning: true,
110
+ port: 8097,
111
+ connectedApps: 1,
112
+ componentCount: 47,
113
+ profilingActive: false,
114
+ uptime: 12000,
115
+ };
116
+
117
+ const result = formatStatus(status);
118
+ expect(result).toContain('running');
119
+ expect(result).toContain('8097');
120
+ expect(result).toContain('1 connected');
121
+ expect(result).toContain('47 components');
122
+ });
123
+ });
124
+
125
+ describe('formatProfileReport', () => {
126
+ it('should format a render report', () => {
127
+ const report: ComponentRenderReport = {
128
+ id: 5,
129
+ displayName: 'UserProfile',
130
+ renderCount: 12,
131
+ totalDuration: 540,
132
+ avgDuration: 45,
133
+ maxDuration: 120,
134
+ causes: ['props-changed', 'state-changed'],
135
+ };
136
+
137
+ const result = formatProfileReport(report, '@c5');
138
+ expect(result).toContain('@c5 "UserProfile"');
139
+ expect(result).toContain('renders:12');
140
+ expect(result).toContain('avg:45.0ms');
141
+ expect(result).toContain('max:120.0ms');
142
+ expect(result).toContain('props-changed');
143
+ });
144
+ });
145
+
146
+ describe('formatSlowest', () => {
147
+ it('should format empty data', () => {
148
+ expect(formatSlowest([])).toContain('No profiling data');
149
+ });
150
+
151
+ it('should format slowest components', () => {
152
+ const reports: ComponentRenderReport[] = [
153
+ { id: 1, displayName: 'SlowComp', renderCount: 5, totalDuration: 250, avgDuration: 50, maxDuration: 100, causes: ['props-changed'] },
154
+ { id: 2, displayName: 'FastComp', renderCount: 10, totalDuration: 100, avgDuration: 10, maxDuration: 20, causes: ['state-changed'] },
155
+ ];
156
+
157
+ const result = formatSlowest(reports);
158
+ expect(result).toContain('Slowest');
159
+ expect(result).toContain('SlowComp');
160
+ expect(result).toContain('FastComp');
161
+ });
162
+ });
163
+
164
+ describe('formatRerenders', () => {
165
+ it('should format rerender data', () => {
166
+ const reports: ComponentRenderReport[] = [
167
+ { id: 1, displayName: 'Chatty', renderCount: 50, totalDuration: 100, avgDuration: 2, maxDuration: 5, causes: ['parent-rendered'] },
168
+ ];
169
+
170
+ const result = formatRerenders(reports);
171
+ expect(result).toContain('50 renders');
172
+ expect(result).toContain('parent-rendered');
173
+ });
174
+ });
175
+
176
+ describe('formatTimeline', () => {
177
+ it('should format timeline entries', () => {
178
+ const entries: TimelineEntry[] = [
179
+ { index: 0, timestamp: 1000, duration: 12.5, componentCount: 5 },
180
+ { index: 1, timestamp: 2000, duration: 8.3, componentCount: 3 },
181
+ ];
182
+
183
+ const result = formatTimeline(entries);
184
+ expect(result).toContain('#0');
185
+ expect(result).toContain('12.5ms');
186
+ expect(result).toContain('#1');
187
+ expect(result).toContain('8.3ms');
188
+ });
189
+ });
@@ -0,0 +1,264 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { Profiler } from '../profiler.js';
3
+ import { ComponentTree } from '../component-tree.js';
4
+
5
+ /**
6
+ * Operations encoding reference (protocol v2):
7
+ * [rendererID, rootFiberID, stringTableSize, ...stringTable, ...ops]
8
+ *
9
+ * String table: for each string, [length, ...charCodes]. String ID 0 = null.
10
+ *
11
+ * TREE_OPERATION_ADD (1):
12
+ * 1, id, elementType, parentId, ownerID, displayNameStringID, keyStringID
13
+ */
14
+
15
+ /** Build a string table and return [tableData, stringIdMap] */
16
+ function buildStringTable(strings: string[]): [number[], Map<string, number>] {
17
+ const idMap = new Map<string, number>();
18
+ const data: number[] = [];
19
+ for (const s of strings) {
20
+ const id = idMap.size + 1; // 0 is reserved for null
21
+ if (!idMap.has(s)) {
22
+ idMap.set(s, id);
23
+ data.push(s.length, ...Array.from(s).map((c) => c.charCodeAt(0)));
24
+ }
25
+ }
26
+ return [data, idMap];
27
+ }
28
+
29
+ /** Build a complete operations array with string table */
30
+ function buildOps(
31
+ rendererID: number,
32
+ rootID: number,
33
+ strings: string[],
34
+ opsFn: (strId: (s: string) => number) => number[],
35
+ ): number[] {
36
+ const [tableData, idMap] = buildStringTable(strings);
37
+ const strId = (s: string) => idMap.get(s) || 0;
38
+ const ops = opsFn(strId);
39
+ return [rendererID, rootID, tableData.length, ...tableData, ...ops];
40
+ }
41
+
42
+ function addOp(
43
+ id: number,
44
+ elementType: number,
45
+ parentId: number,
46
+ displayNameStrId: number,
47
+ keyStrId: number = 0,
48
+ ): number[] {
49
+ return [1, id, elementType, parentId, 0, displayNameStrId, keyStrId];
50
+ }
51
+
52
+ describe('Profiler', () => {
53
+ let profiler: Profiler;
54
+ let tree: ComponentTree;
55
+
56
+ beforeEach(() => {
57
+ profiler = new Profiler();
58
+ tree = new ComponentTree();
59
+
60
+ // Set up a basic tree (FUNCTION=5 in protocol v2)
61
+ const ops = buildOps(1, 100, ['App', 'Header', 'Content'], (s) => [
62
+ ...addOp(1, 5, 0, s('App')),
63
+ ...addOp(2, 5, 1, s('Header')),
64
+ ...addOp(3, 5, 1, s('Content')),
65
+ ]);
66
+ tree.applyOperations(ops);
67
+ });
68
+
69
+ it('should track active state', () => {
70
+ expect(profiler.isActive()).toBe(false);
71
+ profiler.start('test');
72
+ expect(profiler.isActive()).toBe(true);
73
+ profiler.stop();
74
+ expect(profiler.isActive()).toBe(false);
75
+ });
76
+
77
+ it('should return null when stopping without starting', () => {
78
+ expect(profiler.stop()).toBeNull();
79
+ });
80
+
81
+ it('should process flat profiling data', () => {
82
+ profiler.start('test');
83
+
84
+ profiler.processProfilingData({
85
+ commitData: [
86
+ {
87
+ timestamp: 1000,
88
+ duration: 15,
89
+ fiberActualDurations: [1, 10, 2, 3, 3, 2],
90
+ fiberSelfDurations: [1, 5, 2, 3, 3, 2],
91
+ },
92
+ {
93
+ timestamp: 2000,
94
+ duration: 8,
95
+ fiberActualDurations: [1, 5, 3, 3],
96
+ fiberSelfDurations: [1, 2, 3, 3],
97
+ },
98
+ ],
99
+ });
100
+
101
+ const summary = profiler.stop();
102
+ expect(summary).not.toBeNull();
103
+ expect(summary!.commitCount).toBe(2);
104
+ expect(summary!.componentRenderCounts.length).toBeGreaterThan(0);
105
+ });
106
+
107
+ it('should generate render reports', () => {
108
+ profiler.start('test');
109
+
110
+ profiler.processProfilingData({
111
+ commitData: [
112
+ {
113
+ timestamp: 1000,
114
+ duration: 15,
115
+ fiberActualDurations: [1, 10, 2, 3],
116
+ fiberSelfDurations: [1, 5, 2, 3],
117
+ changeDescriptions: [
118
+ [1, { props: ['theme'], isFirstMount: false }],
119
+ [2, { isFirstMount: true }],
120
+ ],
121
+ },
122
+ {
123
+ timestamp: 2000,
124
+ duration: 8,
125
+ fiberActualDurations: [1, 20],
126
+ fiberSelfDurations: [1, 15],
127
+ changeDescriptions: [
128
+ [1, { didHooksChange: true, isFirstMount: false }],
129
+ ],
130
+ },
131
+ ],
132
+ });
133
+
134
+ const report = profiler.getReport(1, tree);
135
+ expect(report).not.toBeNull();
136
+ expect(report!.displayName).toBe('App');
137
+ expect(report!.renderCount).toBe(2);
138
+ expect(report!.totalDuration).toBe(30);
139
+ expect(report!.avgDuration).toBe(15);
140
+ expect(report!.maxDuration).toBe(20);
141
+ expect(report!.causes).toContain('props-changed');
142
+ expect(report!.causes).toContain('hooks-changed');
143
+ });
144
+
145
+ it('should find slowest components', () => {
146
+ profiler.start('test');
147
+
148
+ profiler.processProfilingData({
149
+ commitData: [
150
+ {
151
+ timestamp: 1000,
152
+ duration: 15,
153
+ fiberActualDurations: [1, 50, 2, 5, 3, 30],
154
+ fiberSelfDurations: [1, 15, 2, 5, 3, 30],
155
+ },
156
+ ],
157
+ });
158
+
159
+ const slowest = profiler.getSlowest(tree, 2);
160
+ expect(slowest).toHaveLength(2);
161
+ expect(slowest[0].displayName).toBe('App');
162
+ expect(slowest[1].displayName).toBe('Content');
163
+ });
164
+
165
+ it('should find most rerenders', () => {
166
+ profiler.start('test');
167
+
168
+ profiler.processProfilingData({
169
+ commitData: [
170
+ {
171
+ timestamp: 1000,
172
+ duration: 5,
173
+ fiberActualDurations: [1, 1, 2, 1, 3, 1],
174
+ fiberSelfDurations: [1, 1, 2, 1, 3, 1],
175
+ },
176
+ {
177
+ timestamp: 2000,
178
+ duration: 5,
179
+ fiberActualDurations: [2, 1, 3, 1],
180
+ fiberSelfDurations: [2, 1, 3, 1],
181
+ },
182
+ {
183
+ timestamp: 3000,
184
+ duration: 5,
185
+ fiberActualDurations: [3, 1],
186
+ fiberSelfDurations: [3, 1],
187
+ },
188
+ ],
189
+ });
190
+
191
+ const rerenders = profiler.getMostRerenders(tree, 3);
192
+ expect(rerenders[0].displayName).toBe('Content');
193
+ expect(rerenders[0].renderCount).toBe(3);
194
+ });
195
+
196
+ it('should process dataForRoots nested format', () => {
197
+ profiler.start('test');
198
+
199
+ profiler.processProfilingData({
200
+ dataForRoots: [
201
+ {
202
+ commitData: [
203
+ {
204
+ timestamp: 1000,
205
+ duration: 12,
206
+ fiberActualDurations: [1, 8, 2, 4],
207
+ fiberSelfDurations: [1, 4, 2, 4],
208
+ changeDescriptions: [
209
+ [1, { props: ['count'], isFirstMount: false }],
210
+ [2, { state: ['value'], isFirstMount: false }],
211
+ ],
212
+ },
213
+ ],
214
+ },
215
+ ],
216
+ });
217
+
218
+ const summary = profiler.stop();
219
+ expect(summary).not.toBeNull();
220
+ expect(summary!.commitCount).toBe(1);
221
+
222
+ // Verify the state change was captured correctly
223
+ profiler.start('test2');
224
+ profiler.processProfilingData({
225
+ dataForRoots: [
226
+ {
227
+ commitData: [
228
+ {
229
+ timestamp: 2000,
230
+ duration: 5,
231
+ fiberActualDurations: [2, 3],
232
+ fiberSelfDurations: [2, 3],
233
+ changeDescriptions: [
234
+ [2, { state: ['value', 'count'], isFirstMount: false }],
235
+ ],
236
+ },
237
+ ],
238
+ },
239
+ ],
240
+ });
241
+
242
+ const report = profiler.getReport(2, tree);
243
+ expect(report).not.toBeNull();
244
+ expect(report!.causes).toContain('state-changed');
245
+ });
246
+
247
+ it('should generate timeline', () => {
248
+ profiler.start('test');
249
+
250
+ profiler.processProfilingData({
251
+ commitData: [
252
+ { timestamp: 1000, duration: 10, fiberActualDurations: [1, 5], fiberSelfDurations: [] },
253
+ { timestamp: 2000, duration: 20, fiberActualDurations: [1, 10, 2, 5], fiberSelfDurations: [] },
254
+ ],
255
+ });
256
+
257
+ const timeline = profiler.getTimeline();
258
+ expect(timeline).toHaveLength(2);
259
+ expect(timeline[0].duration).toBe(10);
260
+ expect(timeline[0].componentCount).toBe(1);
261
+ expect(timeline[1].duration).toBe(20);
262
+ expect(timeline[1].componentCount).toBe(2);
263
+ });
264
+ });