appium-session-recorder 0.0.2 → 0.0.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.
Files changed (73) hide show
  1. package/dist/index.js +32422 -0
  2. package/dist/ui/assets/index-CUcJNRfB.css +1 -0
  3. package/dist/ui/assets/index-Cl_X3tPj.js +4 -0
  4. package/{src → dist}/ui/index.html +2 -1
  5. package/package.json +10 -3
  6. package/bun.lock +0 -731
  7. package/src/cli/arg-parser.ts +0 -311
  8. package/src/cli/commands/drive.ts +0 -147
  9. package/src/cli/commands/index.ts +0 -54
  10. package/src/cli/commands/proxy.ts +0 -41
  11. package/src/cli/commands/screen.ts +0 -73
  12. package/src/cli/commands/selectors.ts +0 -42
  13. package/src/cli/commands/session.ts +0 -64
  14. package/src/cli/commands/types.ts +0 -11
  15. package/src/cli/index.ts +0 -158
  16. package/src/cli/prompts.ts +0 -64
  17. package/src/cli/response.ts +0 -44
  18. package/src/core/appium/client.ts +0 -248
  19. package/src/core/index.ts +0 -5
  20. package/src/core/selectors/generate-candidates.ts +0 -155
  21. package/src/core/selectors/score-candidates.ts +0 -184
  22. package/src/core/types.ts +0 -79
  23. package/src/core/xml/parse-source.ts +0 -197
  24. package/src/index.ts +0 -7
  25. package/src/server/appium-client.ts +0 -24
  26. package/src/server/index.ts +0 -6
  27. package/src/server/interaction-recorder.ts +0 -74
  28. package/src/server/proxy-middleware.ts +0 -68
  29. package/src/server/routes.ts +0 -64
  30. package/src/server/server.ts +0 -43
  31. package/src/server/types.ts +0 -34
  32. package/src/ui/bun.lock +0 -311
  33. package/src/ui/package.json +0 -20
  34. package/src/ui/src/App.css +0 -12
  35. package/src/ui/src/App.tsx +0 -41
  36. package/src/ui/src/components/ActionCarousel.css +0 -128
  37. package/src/ui/src/components/ActionCarousel.tsx +0 -92
  38. package/src/ui/src/components/Inspector.css +0 -314
  39. package/src/ui/src/components/Inspector.tsx +0 -265
  40. package/src/ui/src/components/InteractionCard.css +0 -159
  41. package/src/ui/src/components/InteractionCard.tsx +0 -60
  42. package/src/ui/src/components/MainInspector.css +0 -304
  43. package/src/ui/src/components/MainInspector.tsx +0 -304
  44. package/src/ui/src/components/Stats.css +0 -27
  45. package/src/ui/src/components/Timeline.css +0 -31
  46. package/src/ui/src/components/Timeline.tsx +0 -37
  47. package/src/ui/src/hooks/useInteractions.ts +0 -73
  48. package/src/ui/src/index.tsx +0 -11
  49. package/src/ui/src/services/api.ts +0 -41
  50. package/src/ui/src/styles/tokens.css +0 -126
  51. package/src/ui/src/types.ts +0 -34
  52. package/src/ui/src/utils/__tests__/locators.test.ts +0 -304
  53. package/src/ui/src/utils/__tests__/xml-parser.test.ts +0 -326
  54. package/src/ui/src/utils/locators.ts +0 -14
  55. package/src/ui/src/utils/xml-parser.ts +0 -45
  56. package/src/ui/tsconfig.json +0 -34
  57. package/src/ui/tsconfig.node.json +0 -11
  58. package/src/ui/vite.config.ts +0 -22
  59. package/tests/cli/arg-parser.test.ts +0 -397
  60. package/tests/cli/drive-commands.test.ts +0 -151
  61. package/tests/cli/selectors-best.test.ts +0 -42
  62. package/tests/cli/session-commands.test.ts +0 -53
  63. package/tests/core/selector-candidates.test.ts +0 -83
  64. package/tests/core/selector-scoring.test.ts +0 -75
  65. package/tests/core/xml-parser.test.ts +0 -56
  66. package/tests/server/appium-client.test.ts +0 -229
  67. package/tests/server/interaction-recorder.test.ts +0 -377
  68. package/tests/server/proxy-middleware.test.ts +0 -343
  69. package/tests/server/routes.test.ts +0 -305
  70. package/tsconfig.json +0 -26
  71. package/vitest.config.ts +0 -16
  72. package/vitest.ui.config.ts +0 -15
  73. package/workflow.gif +0 -0
@@ -1,343 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import type { Request, Response, NextFunction } from 'express';
3
- import { createSessionMiddleware } from '../../src/server/proxy-middleware';
4
- import { InteractionRecorder } from '../../src/server/interaction-recorder';
5
- import { AppiumClient } from '../../src/server/appium-client';
6
-
7
- // Mock Express request
8
- function createMockRequest(overrides: Partial<Request> = {}): Request {
9
- return {
10
- method: 'POST',
11
- originalUrl: '/session/abc123/element',
12
- path: '/session/abc123/element',
13
- params: { sessionId: 'abc123' },
14
- body: {},
15
- ...overrides,
16
- } as Request;
17
- }
18
-
19
- // Mock Express response
20
- function createMockResponse(): Response {
21
- const listeners: Record<string, Function[]> = {};
22
- return {
23
- on: vi.fn((event: string, callback: Function) => {
24
- if (!listeners[event]) listeners[event] = [];
25
- listeners[event].push(callback);
26
- }),
27
- // Helper to trigger events
28
- emit: (event: string) => {
29
- listeners[event]?.forEach(cb => cb());
30
- },
31
- } as unknown as Response;
32
- }
33
-
34
- describe('createSessionMiddleware', () => {
35
- let recorder: InteractionRecorder;
36
- let appiumClient: AppiumClient;
37
- let middleware: ReturnType<typeof createSessionMiddleware>;
38
- let mockNext: NextFunction;
39
-
40
- beforeEach(() => {
41
- recorder = new InteractionRecorder();
42
- appiumClient = new AppiumClient('http://localhost:4723');
43
- middleware = createSessionMiddleware(recorder, appiumClient);
44
- mockNext = vi.fn();
45
-
46
- // Mock console.log to prevent output during tests
47
- vi.spyOn(console, 'log').mockImplementation(() => {});
48
- });
49
-
50
- describe('request filtering', () => {
51
- it('should skip non-POST requests', async () => {
52
- const req = createMockRequest({ method: 'GET' });
53
- const res = createMockResponse();
54
-
55
- await middleware(req, res, mockNext);
56
-
57
- expect(mockNext).toHaveBeenCalled();
58
- expect(recorder.getHistory()).toHaveLength(0);
59
- });
60
-
61
- it('should record POST requests', async () => {
62
- const req = createMockRequest({ method: 'POST' });
63
- const res = createMockResponse();
64
-
65
- await middleware(req, res, mockNext);
66
-
67
- expect(mockNext).toHaveBeenCalled();
68
- expect(recorder.getHistory()).toHaveLength(1);
69
- });
70
-
71
- it('should skip DELETE requests (not recorded by shouldRecord)', async () => {
72
- const req = createMockRequest({ method: 'DELETE' });
73
- const res = createMockResponse();
74
-
75
- await middleware(req, res, mockNext);
76
-
77
- expect(mockNext).toHaveBeenCalled();
78
- expect(recorder.getHistory()).toHaveLength(0);
79
- });
80
- });
81
-
82
- describe('interaction recording', () => {
83
- it('should record interaction with method and path', async () => {
84
- const req = createMockRequest({
85
- method: 'POST',
86
- originalUrl: '/session/abc123/element',
87
- });
88
- const res = createMockResponse();
89
-
90
- await middleware(req, res, mockNext);
91
-
92
- const history = recorder.getHistory();
93
- expect(history[0].method).toBe('POST');
94
- expect(history[0].path).toBe('/session/abc123/element');
95
- });
96
-
97
- it('should record body when present', async () => {
98
- const req = createMockRequest({
99
- body: { using: 'xpath', value: '//button' },
100
- });
101
- const res = createMockResponse();
102
-
103
- await middleware(req, res, mockNext);
104
-
105
- const history = recorder.getHistory();
106
- expect(history[0].body).toEqual({ using: 'xpath', value: '//button' });
107
- });
108
-
109
- it('should not record body when empty', async () => {
110
- const req = createMockRequest({
111
- body: {},
112
- });
113
- const res = createMockResponse();
114
-
115
- await middleware(req, res, mockNext);
116
-
117
- const history = recorder.getHistory();
118
- expect(history[0].body).toBeUndefined();
119
- });
120
-
121
- it('should not record body when undefined', async () => {
122
- const req = createMockRequest({
123
- body: undefined,
124
- });
125
- const res = createMockResponse();
126
-
127
- await middleware(req, res, mockNext);
128
-
129
- const history = recorder.getHistory();
130
- expect(history[0].body).toBeUndefined();
131
- });
132
- });
133
-
134
- describe('element info extraction', () => {
135
- it('should extract element info from find element request', async () => {
136
- const req = createMockRequest({
137
- body: { using: 'accessibility id', value: 'loginButton' },
138
- });
139
- const res = createMockResponse();
140
-
141
- await middleware(req, res, mockNext);
142
-
143
- const history = recorder.getHistory();
144
- expect(history[0].elementInfo).toEqual({
145
- using: 'accessibility id',
146
- value: 'loginButton',
147
- });
148
- });
149
-
150
- it('should extract element info with xpath strategy', async () => {
151
- const req = createMockRequest({
152
- body: { using: 'xpath', value: '//XCUIElementTypeButton[@name="Login"]' },
153
- });
154
- const res = createMockResponse();
155
-
156
- await middleware(req, res, mockNext);
157
-
158
- const history = recorder.getHistory();
159
- expect(history[0].elementInfo).toEqual({
160
- using: 'xpath',
161
- value: '//XCUIElementTypeButton[@name="Login"]',
162
- });
163
- });
164
-
165
- it('should not set elementInfo when using is missing', async () => {
166
- const req = createMockRequest({
167
- body: { value: 'loginButton' },
168
- });
169
- const res = createMockResponse();
170
-
171
- await middleware(req, res, mockNext);
172
-
173
- const history = recorder.getHistory();
174
- expect(history[0].elementInfo).toBeUndefined();
175
- });
176
-
177
- it('should not set elementInfo when value is missing', async () => {
178
- const req = createMockRequest({
179
- body: { using: 'accessibility id' },
180
- });
181
- const res = createMockResponse();
182
-
183
- await middleware(req, res, mockNext);
184
-
185
- const history = recorder.getHistory();
186
- expect(history[0].elementInfo).toBeUndefined();
187
- });
188
- });
189
-
190
- describe('action endpoints and state capture', () => {
191
- it('should register finish listener for action endpoints', async () => {
192
- const req = createMockRequest({
193
- method: 'POST',
194
- path: '/session/abc123/element/xyz/click',
195
- originalUrl: '/session/abc123/element/xyz/click',
196
- });
197
- const res = createMockResponse();
198
-
199
- await middleware(req, res, mockNext);
200
-
201
- expect(res.on).toHaveBeenCalledWith('finish', expect.any(Function));
202
- });
203
-
204
- it('should not register finish listener for non-action endpoints', async () => {
205
- const req = createMockRequest({
206
- method: 'POST',
207
- path: '/session/abc123/screenshot',
208
- originalUrl: '/session/abc123/screenshot',
209
- });
210
- const res = createMockResponse();
211
-
212
- await middleware(req, res, mockNext);
213
-
214
- expect(res.on).not.toHaveBeenCalled();
215
- });
216
-
217
- it('should capture state on response finish for action endpoints', async () => {
218
- const req = createMockRequest({
219
- method: 'POST',
220
- path: '/session/abc123/element/xyz/click',
221
- originalUrl: '/session/abc123/element/xyz/click',
222
- });
223
- const res = createMockResponse();
224
-
225
- // Mock captureState
226
- vi.spyOn(appiumClient, 'captureState').mockResolvedValue({
227
- screenshot: 'base64Screenshot',
228
- source: '<xml>source</xml>',
229
- });
230
-
231
- await middleware(req, res, mockNext);
232
-
233
- // Trigger finish event
234
- (res as any).emit('finish');
235
-
236
- // Wait for async operation
237
- await new Promise(resolve => setTimeout(resolve, 10));
238
-
239
- expect(appiumClient.captureState).toHaveBeenCalledWith('abc123');
240
-
241
- const history = recorder.getHistory();
242
- expect(history[0].screenshot).toBe('base64Screenshot');
243
- expect(history[0].source).toBe('<xml>source</xml>');
244
- });
245
-
246
- it('should handle captureState returning partial data', async () => {
247
- const req = createMockRequest({
248
- method: 'POST',
249
- path: '/session/abc123/element/xyz/click',
250
- originalUrl: '/session/abc123/element/xyz/click',
251
- });
252
- const res = createMockResponse();
253
-
254
- vi.spyOn(appiumClient, 'captureState').mockResolvedValue({
255
- screenshot: 'base64Screenshot',
256
- });
257
-
258
- await middleware(req, res, mockNext);
259
-
260
- (res as any).emit('finish');
261
- await new Promise(resolve => setTimeout(resolve, 10));
262
-
263
- const history = recorder.getHistory();
264
- expect(history[0].screenshot).toBe('base64Screenshot');
265
- expect(history[0].source).toBeUndefined();
266
- });
267
- });
268
-
269
- describe('next() behavior', () => {
270
- it('should always call next()', async () => {
271
- const req = createMockRequest();
272
- const res = createMockResponse();
273
-
274
- await middleware(req, res, mockNext);
275
-
276
- expect(mockNext).toHaveBeenCalledTimes(1);
277
- });
278
-
279
- it('should call next() even for skipped requests', async () => {
280
- const req = createMockRequest({ method: 'GET' });
281
- const res = createMockResponse();
282
-
283
- await middleware(req, res, mockNext);
284
-
285
- expect(mockNext).toHaveBeenCalledTimes(1);
286
- });
287
- });
288
-
289
- describe('various action endpoints', () => {
290
- const actionEndpoints = [
291
- '/session/abc123/element/xyz/click',
292
- '/session/abc123/element/xyz/value',
293
- '/session/abc123/element/xyz/clear',
294
- '/session/abc123/element',
295
- '/session/abc123/elements',
296
- '/session/abc123/touch/perform',
297
- '/session/abc123/actions',
298
- '/session/abc123/back',
299
- '/session/abc123/forward',
300
- '/session/abc123/refresh',
301
- ];
302
-
303
- actionEndpoints.forEach(endpoint => {
304
- it(`should recognize ${endpoint} as action endpoint`, async () => {
305
- const req = createMockRequest({
306
- method: 'POST',
307
- path: endpoint,
308
- originalUrl: endpoint,
309
- });
310
- const res = createMockResponse();
311
-
312
- await middleware(req, res, mockNext);
313
-
314
- expect(res.on).toHaveBeenCalledWith('finish', expect.any(Function));
315
- });
316
- });
317
- });
318
-
319
- describe('non-action endpoints', () => {
320
- const nonActionEndpoints = [
321
- '/session/abc123/screenshot',
322
- '/session/abc123/source',
323
- '/session/abc123/window',
324
- '/session/abc123/url',
325
- '/session/abc123/title',
326
- ];
327
-
328
- nonActionEndpoints.forEach(endpoint => {
329
- it(`should not recognize ${endpoint} as action endpoint`, async () => {
330
- const req = createMockRequest({
331
- method: 'POST',
332
- path: endpoint,
333
- originalUrl: endpoint,
334
- });
335
- const res = createMockResponse();
336
-
337
- await middleware(req, res, mockNext);
338
-
339
- expect(res.on).not.toHaveBeenCalled();
340
- });
341
- });
342
- });
343
- });
@@ -1,305 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
- import type { Request, Response } from 'express';
3
- import { createRoutes } from '../../src/server/routes';
4
- import { InteractionRecorder } from '../../src/server/interaction-recorder';
5
-
6
- // Mock request
7
- function createMockRequest(overrides: Partial<Request> = {}): Request {
8
- const listeners: Record<string, Function[]> = {};
9
- return {
10
- on: vi.fn((event: string, callback: Function) => {
11
- if (!listeners[event]) listeners[event] = [];
12
- listeners[event].push(callback);
13
- return this;
14
- }),
15
- emit: (event: string) => {
16
- listeners[event]?.forEach(cb => cb());
17
- },
18
- ...overrides,
19
- } as unknown as Request;
20
- }
21
-
22
- // Mock response
23
- function createMockResponse(): Response & { _written: string[]; _json: any } {
24
- const res = {
25
- _written: [] as string[],
26
- _json: null as any,
27
- json: vi.fn(function(data: any) {
28
- res._json = data;
29
- return res;
30
- }),
31
- setHeader: vi.fn().mockReturnThis(),
32
- write: vi.fn(function(data: string) {
33
- res._written.push(data);
34
- return res;
35
- }),
36
- sendFile: vi.fn().mockReturnThis(),
37
- };
38
- return res as unknown as Response & { _written: string[]; _json: any };
39
- }
40
-
41
- describe('createRoutes', () => {
42
- let recorder: InteractionRecorder;
43
- let router: ReturnType<typeof createRoutes>;
44
-
45
- beforeEach(() => {
46
- recorder = new InteractionRecorder();
47
- router = createRoutes(recorder);
48
- });
49
-
50
- // Helper to find route handler
51
- function getRouteHandler(method: string, path: string) {
52
- const stack = (router as any).stack;
53
- for (const layer of stack) {
54
- if (layer.route) {
55
- const routePath = layer.route.path;
56
- const routeMethods = Object.keys(layer.route.methods);
57
- if (routePath === path && routeMethods.includes(method.toLowerCase())) {
58
- return layer.route.stack[0].handle;
59
- }
60
- }
61
- }
62
- return null;
63
- }
64
-
65
- describe('GET /_recorder/api/history', () => {
66
- it('should return empty array when no history', () => {
67
- const handler = getRouteHandler('GET', '/_recorder/api/history');
68
- expect(handler).not.toBeNull();
69
-
70
- const req = createMockRequest();
71
- const res = createMockResponse();
72
-
73
- handler(req, res);
74
-
75
- expect(res.json).toHaveBeenCalledWith([]);
76
- });
77
-
78
- it('should return recorded interactions', () => {
79
- recorder.recordInteraction({ method: 'POST', path: '/test1' });
80
- recorder.recordInteraction({ method: 'POST', path: '/test2' });
81
-
82
- const handler = getRouteHandler('GET', '/_recorder/api/history');
83
- const req = createMockRequest();
84
- const res = createMockResponse();
85
-
86
- handler(req, res);
87
-
88
- expect(res._json).toHaveLength(2);
89
- expect(res._json[0].path).toBe('/test1');
90
- expect(res._json[1].path).toBe('/test2');
91
- });
92
- });
93
-
94
- describe('DELETE /_recorder/api/history', () => {
95
- it('should clear history', () => {
96
- recorder.recordInteraction({ method: 'POST', path: '/test' });
97
- expect(recorder.getHistory()).toHaveLength(1);
98
-
99
- const handler = getRouteHandler('DELETE', '/_recorder/api/history');
100
- const req = createMockRequest();
101
- const res = createMockResponse();
102
-
103
- handler(req, res);
104
-
105
- expect(recorder.getHistory()).toHaveLength(0);
106
- expect(res.json).toHaveBeenCalledWith({ ok: true });
107
- });
108
- });
109
-
110
- describe('GET /_recorder/api/stream (SSE)', () => {
111
- it('should set correct headers for SSE', () => {
112
- const handler = getRouteHandler('GET', '/_recorder/api/stream');
113
- const req = createMockRequest();
114
- const res = createMockResponse();
115
-
116
- handler(req, res);
117
-
118
- expect(res.setHeader).toHaveBeenCalledWith('Content-Type', 'text/event-stream');
119
- expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', 'no-cache');
120
- expect(res.setHeader).toHaveBeenCalledWith('Connection', 'keep-alive');
121
- });
122
-
123
- it('should send initial history on connection', () => {
124
- recorder.recordInteraction({ method: 'POST', path: '/existing' });
125
-
126
- const handler = getRouteHandler('GET', '/_recorder/api/stream');
127
- const req = createMockRequest();
128
- const res = createMockResponse();
129
-
130
- handler(req, res);
131
-
132
- expect(res._written.length).toBeGreaterThan(0);
133
- const initData = JSON.parse(res._written[0].replace('data: ', '').replace('\n\n', ''));
134
- expect(initData.type).toBe('init');
135
- expect(initData.data).toHaveLength(1);
136
- });
137
-
138
- it('should stream new interactions', () => {
139
- const handler = getRouteHandler('GET', '/_recorder/api/stream');
140
- const req = createMockRequest();
141
- const res = createMockResponse();
142
-
143
- handler(req, res);
144
-
145
- const initialWriteCount = res._written.length;
146
-
147
- // Record new interaction
148
- recorder.recordInteraction({ method: 'POST', path: '/new' });
149
-
150
- expect(res._written.length).toBe(initialWriteCount + 1);
151
- const eventData = JSON.parse(res._written[res._written.length - 1].replace('data: ', '').replace('\n\n', ''));
152
- expect(eventData.type).toBe('interaction');
153
- expect(eventData.data.path).toBe('/new');
154
- });
155
-
156
- it('should stream interaction updates', () => {
157
- const handler = getRouteHandler('GET', '/_recorder/api/stream');
158
- const req = createMockRequest();
159
- const res = createMockResponse();
160
-
161
- handler(req, res);
162
-
163
- const interaction = recorder.recordInteraction({ method: 'POST', path: '/test' });
164
- const afterRecordCount = res._written.length;
165
-
166
- recorder.updateInteraction(interaction.id, { screenshot: 'base64data' });
167
-
168
- expect(res._written.length).toBe(afterRecordCount + 1);
169
- const eventData = JSON.parse(res._written[res._written.length - 1].replace('data: ', '').replace('\n\n', ''));
170
- expect(eventData.data.screenshot).toBe('base64data');
171
- });
172
-
173
- it('should stream clear events', () => {
174
- const handler = getRouteHandler('GET', '/_recorder/api/stream');
175
- const req = createMockRequest();
176
- const res = createMockResponse();
177
-
178
- handler(req, res);
179
-
180
- recorder.recordInteraction({ method: 'POST', path: '/test' });
181
- const beforeClearCount = res._written.length;
182
-
183
- recorder.clearHistory();
184
-
185
- expect(res._written.length).toBe(beforeClearCount + 1);
186
- const eventData = JSON.parse(res._written[res._written.length - 1].replace('data: ', '').replace('\n\n', ''));
187
- expect(eventData.type).toBe('clear');
188
- expect(eventData.data).toBeNull();
189
- });
190
-
191
- it('should unsubscribe on client disconnect', () => {
192
- const handler = getRouteHandler('GET', '/_recorder/api/stream');
193
- const req = createMockRequest();
194
- const res = createMockResponse();
195
-
196
- handler(req, res);
197
-
198
- // Simulate client disconnect
199
- (req as any).emit('close');
200
-
201
- // Record interaction after disconnect
202
- const currentCount = res._written.length;
203
- recorder.recordInteraction({ method: 'POST', path: '/after-disconnect' });
204
-
205
- // Should not receive new events
206
- expect(res._written.length).toBe(currentCount);
207
- });
208
- });
209
-
210
- describe('GET /_recorder', () => {
211
- it('should have route for serving UI', () => {
212
- const handler = getRouteHandler('GET', '/_recorder');
213
- expect(handler).not.toBeNull();
214
- });
215
-
216
- it('should call sendFile with UI path', () => {
217
- const handler = getRouteHandler('GET', '/_recorder');
218
- const req = createMockRequest();
219
- const res = createMockResponse();
220
-
221
- handler(req, res);
222
-
223
- expect(res.sendFile).toHaveBeenCalled();
224
- const sentPath = (res.sendFile as any).mock.calls[0][0];
225
- expect(sentPath).toContain('dist/ui/index.html');
226
- });
227
- });
228
-
229
- describe('SSE data format', () => {
230
- it('should format SSE data correctly with proper line endings', () => {
231
- const handler = getRouteHandler('GET', '/_recorder/api/stream');
232
- const req = createMockRequest();
233
- const res = createMockResponse();
234
-
235
- handler(req, res);
236
-
237
- // Check format of init message
238
- const initMessage = res._written[0];
239
- expect(initMessage).toMatch(/^data: .+\n\n$/);
240
- });
241
-
242
- it('should send valid JSON in SSE data field', () => {
243
- recorder.recordInteraction({ method: 'POST', path: '/test', body: { key: 'value' } });
244
-
245
- const handler = getRouteHandler('GET', '/_recorder/api/stream');
246
- const req = createMockRequest();
247
- const res = createMockResponse();
248
-
249
- handler(req, res);
250
-
251
- // Verify all written data can be parsed as JSON
252
- res._written.forEach(msg => {
253
- const jsonStr = msg.replace('data: ', '').replace('\n\n', '');
254
- expect(() => JSON.parse(jsonStr)).not.toThrow();
255
- });
256
- });
257
- });
258
-
259
- describe('multiple SSE clients', () => {
260
- it('should support multiple simultaneous SSE connections', () => {
261
- const handler = getRouteHandler('GET', '/_recorder/api/stream');
262
-
263
- const req1 = createMockRequest();
264
- const res1 = createMockResponse();
265
- handler(req1, res1);
266
-
267
- const req2 = createMockRequest();
268
- const res2 = createMockResponse();
269
- handler(req2, res2);
270
-
271
- // Record interaction
272
- recorder.recordInteraction({ method: 'POST', path: '/test' });
273
-
274
- // Both clients should receive the event
275
- // Each client receives: init + interaction = at least 2 writes
276
- expect(res1._written.length).toBeGreaterThanOrEqual(2);
277
- expect(res2._written.length).toBeGreaterThanOrEqual(2);
278
- });
279
-
280
- it('should only unsubscribe disconnected client', () => {
281
- const handler = getRouteHandler('GET', '/_recorder/api/stream');
282
-
283
- const req1 = createMockRequest();
284
- const res1 = createMockResponse();
285
- handler(req1, res1);
286
-
287
- const req2 = createMockRequest();
288
- const res2 = createMockResponse();
289
- handler(req2, res2);
290
-
291
- // Disconnect first client
292
- (req1 as any).emit('close');
293
-
294
- const res1CountBeforeNew = res1._written.length;
295
- const res2CountBeforeNew = res2._written.length;
296
-
297
- // Record new interaction
298
- recorder.recordInteraction({ method: 'POST', path: '/after-disconnect' });
299
-
300
- // First client should not receive, second should
301
- expect(res1._written.length).toBe(res1CountBeforeNew);
302
- expect(res2._written.length).toBe(res2CountBeforeNew + 1);
303
- });
304
- });
305
- });
package/tsconfig.json DELETED
@@ -1,26 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "ES2020",
5
- "lib": [
6
- "ES2020"
7
- ],
8
- "moduleResolution": "node",
9
- "esModuleInterop": true,
10
- "allowSyntheticDefaultImports": true,
11
- "strict": true,
12
- "skipLibCheck": true,
13
- "resolveJsonModule": true,
14
- "types": [
15
- "bun-types"
16
- ]
17
- },
18
- "include": [
19
- "src/**/*"
20
- ],
21
- "exclude": [
22
- "src/ui/**/*",
23
- "node_modules",
24
- "dist"
25
- ]
26
- }
package/vitest.config.ts DELETED
@@ -1,16 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
-
3
- export default defineConfig({
4
- test: {
5
- globals: true,
6
- environment: 'node',
7
- include: ['src/**/*.test.ts', 'tests/**/*.test.ts'],
8
- exclude: ['src/ui/**/*'],
9
- coverage: {
10
- provider: 'v8',
11
- reporter: ['text', 'json', 'html'],
12
- include: ['src/server/**/*.ts', 'src/cli/**/*.ts'],
13
- exclude: ['src/ui/**/*', '**/*.test.ts', '**/index.ts'],
14
- },
15
- },
16
- });