@desplega.ai/qa-use 2.1.7 → 2.2.2

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 (138) hide show
  1. package/dist/lib/api/browser-types.d.ts +175 -0
  2. package/dist/lib/api/browser-types.d.ts.map +1 -0
  3. package/dist/lib/api/browser-types.js +5 -0
  4. package/dist/lib/api/browser-types.js.map +1 -0
  5. package/dist/lib/api/browser.d.ts +66 -0
  6. package/dist/lib/api/browser.d.ts.map +1 -0
  7. package/dist/lib/api/browser.js +223 -0
  8. package/dist/lib/api/browser.js.map +1 -0
  9. package/dist/package.json +2 -1
  10. package/dist/src/cli/commands/browser/back.d.ts +6 -0
  11. package/dist/src/cli/commands/browser/back.d.ts.map +1 -0
  12. package/dist/src/cli/commands/browser/back.js +42 -0
  13. package/dist/src/cli/commands/browser/back.js.map +1 -0
  14. package/dist/src/cli/commands/browser/check.d.ts +6 -0
  15. package/dist/src/cli/commands/browser/check.d.ts.map +1 -0
  16. package/dist/src/cli/commands/browser/check.js +62 -0
  17. package/dist/src/cli/commands/browser/check.js.map +1 -0
  18. package/dist/src/cli/commands/browser/click.d.ts +6 -0
  19. package/dist/src/cli/commands/browser/click.d.ts.map +1 -0
  20. package/dist/src/cli/commands/browser/click.js +63 -0
  21. package/dist/src/cli/commands/browser/click.js.map +1 -0
  22. package/dist/src/cli/commands/browser/close.d.ts +6 -0
  23. package/dist/src/cli/commands/browser/close.d.ts.map +1 -0
  24. package/dist/src/cli/commands/browser/close.js +44 -0
  25. package/dist/src/cli/commands/browser/close.js.map +1 -0
  26. package/dist/src/cli/commands/browser/create.d.ts +6 -0
  27. package/dist/src/cli/commands/browser/create.d.ts.map +1 -0
  28. package/dist/src/cli/commands/browser/create.js +281 -0
  29. package/dist/src/cli/commands/browser/create.js.map +1 -0
  30. package/dist/src/cli/commands/browser/fill.d.ts +6 -0
  31. package/dist/src/cli/commands/browser/fill.d.ts.map +1 -0
  32. package/dist/src/cli/commands/browser/fill.js +83 -0
  33. package/dist/src/cli/commands/browser/fill.js.map +1 -0
  34. package/dist/src/cli/commands/browser/forward.d.ts +6 -0
  35. package/dist/src/cli/commands/browser/forward.d.ts.map +1 -0
  36. package/dist/src/cli/commands/browser/forward.js +42 -0
  37. package/dist/src/cli/commands/browser/forward.js.map +1 -0
  38. package/dist/src/cli/commands/browser/get-blocks.d.ts +6 -0
  39. package/dist/src/cli/commands/browser/get-blocks.d.ts.map +1 -0
  40. package/dist/src/cli/commands/browser/get-blocks.js +35 -0
  41. package/dist/src/cli/commands/browser/get-blocks.js.map +1 -0
  42. package/dist/src/cli/commands/browser/goto.d.ts +6 -0
  43. package/dist/src/cli/commands/browser/goto.d.ts.map +1 -0
  44. package/dist/src/cli/commands/browser/goto.js +53 -0
  45. package/dist/src/cli/commands/browser/goto.js.map +1 -0
  46. package/dist/src/cli/commands/browser/hover.d.ts +6 -0
  47. package/dist/src/cli/commands/browser/hover.d.ts.map +1 -0
  48. package/dist/src/cli/commands/browser/hover.js +63 -0
  49. package/dist/src/cli/commands/browser/hover.js.map +1 -0
  50. package/dist/src/cli/commands/browser/index.d.ts +9 -0
  51. package/dist/src/cli/commands/browser/index.d.ts.map +1 -0
  52. package/dist/src/cli/commands/browser/index.js +71 -0
  53. package/dist/src/cli/commands/browser/index.js.map +1 -0
  54. package/dist/src/cli/commands/browser/list.d.ts +6 -0
  55. package/dist/src/cli/commands/browser/list.d.ts.map +1 -0
  56. package/dist/src/cli/commands/browser/list.js +85 -0
  57. package/dist/src/cli/commands/browser/list.js.map +1 -0
  58. package/dist/src/cli/commands/browser/press.d.ts +6 -0
  59. package/dist/src/cli/commands/browser/press.d.ts.map +1 -0
  60. package/dist/src/cli/commands/browser/press.js +67 -0
  61. package/dist/src/cli/commands/browser/press.js.map +1 -0
  62. package/dist/src/cli/commands/browser/reload.d.ts +6 -0
  63. package/dist/src/cli/commands/browser/reload.d.ts.map +1 -0
  64. package/dist/src/cli/commands/browser/reload.js +42 -0
  65. package/dist/src/cli/commands/browser/reload.js.map +1 -0
  66. package/dist/src/cli/commands/browser/run.d.ts +6 -0
  67. package/dist/src/cli/commands/browser/run.d.ts.map +1 -0
  68. package/dist/src/cli/commands/browser/run.js +618 -0
  69. package/dist/src/cli/commands/browser/run.js.map +1 -0
  70. package/dist/src/cli/commands/browser/screenshot.d.ts +6 -0
  71. package/dist/src/cli/commands/browser/screenshot.d.ts.map +1 -0
  72. package/dist/src/cli/commands/browser/screenshot.js +72 -0
  73. package/dist/src/cli/commands/browser/screenshot.js.map +1 -0
  74. package/dist/src/cli/commands/browser/scroll-into-view.d.ts +6 -0
  75. package/dist/src/cli/commands/browser/scroll-into-view.d.ts.map +1 -0
  76. package/dist/src/cli/commands/browser/scroll-into-view.js +64 -0
  77. package/dist/src/cli/commands/browser/scroll-into-view.js.map +1 -0
  78. package/dist/src/cli/commands/browser/scroll.d.ts +6 -0
  79. package/dist/src/cli/commands/browser/scroll.d.ts.map +1 -0
  80. package/dist/src/cli/commands/browser/scroll.js +63 -0
  81. package/dist/src/cli/commands/browser/scroll.js.map +1 -0
  82. package/dist/src/cli/commands/browser/select.d.ts +6 -0
  83. package/dist/src/cli/commands/browser/select.d.ts.map +1 -0
  84. package/dist/src/cli/commands/browser/select.js +83 -0
  85. package/dist/src/cli/commands/browser/select.js.map +1 -0
  86. package/dist/src/cli/commands/browser/snapshot.d.ts +6 -0
  87. package/dist/src/cli/commands/browser/snapshot.d.ts.map +1 -0
  88. package/dist/src/cli/commands/browser/snapshot.js +72 -0
  89. package/dist/src/cli/commands/browser/snapshot.js.map +1 -0
  90. package/dist/src/cli/commands/browser/status.d.ts +6 -0
  91. package/dist/src/cli/commands/browser/status.d.ts.map +1 -0
  92. package/dist/src/cli/commands/browser/status.js +91 -0
  93. package/dist/src/cli/commands/browser/status.js.map +1 -0
  94. package/dist/src/cli/commands/browser/stream.d.ts +6 -0
  95. package/dist/src/cli/commands/browser/stream.d.ts.map +1 -0
  96. package/dist/src/cli/commands/browser/stream.js +135 -0
  97. package/dist/src/cli/commands/browser/stream.js.map +1 -0
  98. package/dist/src/cli/commands/browser/tunnel.d.ts +13 -0
  99. package/dist/src/cli/commands/browser/tunnel.d.ts.map +1 -0
  100. package/dist/src/cli/commands/browser/tunnel.js +225 -0
  101. package/dist/src/cli/commands/browser/tunnel.js.map +1 -0
  102. package/dist/src/cli/commands/browser/type.d.ts +6 -0
  103. package/dist/src/cli/commands/browser/type.d.ts.map +1 -0
  104. package/dist/src/cli/commands/browser/type.js +61 -0
  105. package/dist/src/cli/commands/browser/type.js.map +1 -0
  106. package/dist/src/cli/commands/browser/uncheck.d.ts +6 -0
  107. package/dist/src/cli/commands/browser/uncheck.d.ts.map +1 -0
  108. package/dist/src/cli/commands/browser/uncheck.js +62 -0
  109. package/dist/src/cli/commands/browser/uncheck.js.map +1 -0
  110. package/dist/src/cli/commands/browser/url.d.ts +6 -0
  111. package/dist/src/cli/commands/browser/url.d.ts.map +1 -0
  112. package/dist/src/cli/commands/browser/url.js +40 -0
  113. package/dist/src/cli/commands/browser/url.js.map +1 -0
  114. package/dist/src/cli/commands/browser/wait-for-load.d.ts +6 -0
  115. package/dist/src/cli/commands/browser/wait-for-load.d.ts.map +1 -0
  116. package/dist/src/cli/commands/browser/wait-for-load.js +50 -0
  117. package/dist/src/cli/commands/browser/wait-for-load.js.map +1 -0
  118. package/dist/src/cli/commands/browser/wait-for-selector.d.ts +6 -0
  119. package/dist/src/cli/commands/browser/wait-for-selector.d.ts.map +1 -0
  120. package/dist/src/cli/commands/browser/wait-for-selector.js +52 -0
  121. package/dist/src/cli/commands/browser/wait-for-selector.js.map +1 -0
  122. package/dist/src/cli/commands/browser/wait.d.ts +6 -0
  123. package/dist/src/cli/commands/browser/wait.d.ts.map +1 -0
  124. package/dist/src/cli/commands/browser/wait.js +60 -0
  125. package/dist/src/cli/commands/browser/wait.js.map +1 -0
  126. package/dist/src/cli/commands/test/run.d.ts.map +1 -1
  127. package/dist/src/cli/commands/test/run.js +38 -7
  128. package/dist/src/cli/commands/test/run.js.map +1 -1
  129. package/dist/src/cli/index.js +2 -0
  130. package/dist/src/cli/index.js.map +1 -1
  131. package/dist/src/cli/lib/browser-sessions.d.ts +72 -0
  132. package/dist/src/cli/lib/browser-sessions.d.ts.map +1 -0
  133. package/dist/src/cli/lib/browser-sessions.js +184 -0
  134. package/dist/src/cli/lib/browser-sessions.js.map +1 -0
  135. package/lib/api/browser-types.ts +278 -0
  136. package/lib/api/browser.test.ts +378 -0
  137. package/lib/api/browser.ts +279 -0
  138. package/package.json +2 -1
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Type definitions for the Browser API (/browsers/v1/)
3
+ */
4
+
5
+ // ==========================================
6
+ // Session Types
7
+ // ==========================================
8
+
9
+ export type BrowserSessionStatus = 'starting' | 'active' | 'closing' | 'closed';
10
+
11
+ export type ViewportType = 'desktop' | 'mobile' | 'tablet';
12
+
13
+ export interface CreateBrowserSessionOptions {
14
+ headless?: boolean;
15
+ viewport?: ViewportType;
16
+ timeout?: number; // Session timeout in seconds (60-3600)
17
+ ws_url?: string; // WebSocket URL for remote/tunneled browser
18
+ }
19
+
20
+ export interface BrowserSession {
21
+ id: string;
22
+ status: BrowserSessionStatus;
23
+ created_at: string;
24
+ updated_at?: string;
25
+ current_url?: string;
26
+ viewport?: ViewportType;
27
+ headless?: boolean;
28
+ timeout?: number;
29
+ }
30
+
31
+ // ==========================================
32
+ // Action Types
33
+ // ==========================================
34
+
35
+ export type BrowserActionType =
36
+ | 'goto'
37
+ | 'back'
38
+ | 'forward'
39
+ | 'reload'
40
+ | 'click'
41
+ | 'fill'
42
+ | 'type'
43
+ | 'press'
44
+ | 'hover'
45
+ | 'scroll'
46
+ | 'scroll_into_view'
47
+ | 'select'
48
+ | 'check'
49
+ | 'uncheck'
50
+ | 'wait'
51
+ | 'wait_for_selector'
52
+ | 'wait_for_load'
53
+ | 'snapshot'
54
+ | 'screenshot';
55
+
56
+ export type ScrollDirection = 'up' | 'down' | 'left' | 'right';
57
+
58
+ export interface GotoAction {
59
+ type: 'goto';
60
+ url: string;
61
+ }
62
+
63
+ export interface BackAction {
64
+ type: 'back';
65
+ }
66
+
67
+ export interface ForwardAction {
68
+ type: 'forward';
69
+ }
70
+
71
+ export interface ReloadAction {
72
+ type: 'reload';
73
+ }
74
+
75
+ export interface ClickAction {
76
+ type: 'click';
77
+ ref?: string;
78
+ text?: string; // AI-based semantic element selection (alternative to ref)
79
+ }
80
+
81
+ export interface FillAction {
82
+ type: 'fill';
83
+ ref?: string;
84
+ text?: string; // AI-based semantic element selection (alternative to ref)
85
+ value: string;
86
+ }
87
+
88
+ export interface TypeAction {
89
+ type: 'type';
90
+ ref: string;
91
+ text: string;
92
+ }
93
+
94
+ export interface PressAction {
95
+ type: 'press';
96
+ key: string;
97
+ }
98
+
99
+ export interface HoverAction {
100
+ type: 'hover';
101
+ ref?: string;
102
+ text?: string; // AI-based semantic element selection (alternative to ref)
103
+ }
104
+
105
+ export interface ScrollAction {
106
+ type: 'scroll';
107
+ direction: ScrollDirection;
108
+ amount?: number; // pixels, default 500
109
+ }
110
+
111
+ export interface SelectAction {
112
+ type: 'select';
113
+ ref?: string;
114
+ text?: string; // AI-based semantic element selection (alternative to ref)
115
+ value: string;
116
+ }
117
+
118
+ export interface CheckAction {
119
+ type: 'check';
120
+ ref?: string;
121
+ text?: string; // AI-based semantic element selection (alternative to ref)
122
+ }
123
+
124
+ export interface UncheckAction {
125
+ type: 'uncheck';
126
+ ref?: string;
127
+ text?: string; // AI-based semantic element selection (alternative to ref)
128
+ }
129
+
130
+ export interface ScrollIntoViewAction {
131
+ type: 'scroll_into_view';
132
+ ref?: string;
133
+ text?: string; // AI-based semantic element selection (alternative to ref)
134
+ }
135
+
136
+ export interface WaitAction {
137
+ type: 'wait';
138
+ duration_ms: number;
139
+ }
140
+
141
+ export interface WaitForSelectorAction {
142
+ type: 'wait_for_selector';
143
+ selector: string;
144
+ state?: 'visible' | 'hidden' | 'attached' | 'detached';
145
+ }
146
+
147
+ export interface WaitForLoadAction {
148
+ type: 'wait_for_load';
149
+ state?: 'load' | 'domcontentloaded' | 'networkidle';
150
+ }
151
+
152
+ export interface SnapshotAction {
153
+ type: 'snapshot';
154
+ }
155
+
156
+ export interface ScreenshotAction {
157
+ type: 'screenshot';
158
+ }
159
+
160
+ export type BrowserAction =
161
+ | GotoAction
162
+ | BackAction
163
+ | ForwardAction
164
+ | ReloadAction
165
+ | ClickAction
166
+ | FillAction
167
+ | TypeAction
168
+ | PressAction
169
+ | HoverAction
170
+ | ScrollAction
171
+ | ScrollIntoViewAction
172
+ | SelectAction
173
+ | CheckAction
174
+ | UncheckAction
175
+ | WaitAction
176
+ | WaitForSelectorAction
177
+ | WaitForLoadAction
178
+ | SnapshotAction
179
+ | ScreenshotAction;
180
+
181
+ // ==========================================
182
+ // API Response Types
183
+ // ==========================================
184
+
185
+ export interface ActionResult {
186
+ success: boolean;
187
+ error?: string;
188
+ data?: unknown;
189
+ }
190
+
191
+ export interface SnapshotResult {
192
+ snapshot: string;
193
+ url?: string;
194
+ }
195
+
196
+ export interface UrlResult {
197
+ url: string;
198
+ }
199
+
200
+ export interface BlocksResult {
201
+ blocks: unknown[]; // ExtendedStep[] - typed in BrowserApiClient
202
+ }
203
+
204
+ // ==========================================
205
+ // WebSocket Event Types
206
+ // ==========================================
207
+
208
+ export type WebSocketEventType =
209
+ | 'action_started'
210
+ | 'action_completed'
211
+ | 'status_changed'
212
+ | 'error'
213
+ | 'closed';
214
+
215
+ export interface WebSocketEvent {
216
+ type: WebSocketEventType;
217
+ data?: unknown;
218
+ timestamp?: string;
219
+ }
220
+
221
+ export interface ActionStartedEvent extends WebSocketEvent {
222
+ type: 'action_started';
223
+ data: {
224
+ action_type: BrowserActionType;
225
+ action_id?: string;
226
+ };
227
+ }
228
+
229
+ export interface ActionCompletedEvent extends WebSocketEvent {
230
+ type: 'action_completed';
231
+ data: {
232
+ action_id?: string;
233
+ success: boolean;
234
+ error?: string;
235
+ result?: unknown;
236
+ };
237
+ }
238
+
239
+ export interface StatusChangedEvent extends WebSocketEvent {
240
+ type: 'status_changed';
241
+ data: {
242
+ status: BrowserSessionStatus;
243
+ };
244
+ }
245
+
246
+ export interface ErrorEvent extends WebSocketEvent {
247
+ type: 'error';
248
+ data: {
249
+ message: string;
250
+ code?: string;
251
+ };
252
+ }
253
+
254
+ export interface ClosedEvent extends WebSocketEvent {
255
+ type: 'closed';
256
+ data?: {
257
+ reason?: string;
258
+ };
259
+ }
260
+
261
+ // ==========================================
262
+ // Client Messages (WebSocket)
263
+ // ==========================================
264
+
265
+ export interface PingMessage {
266
+ type: 'ping';
267
+ }
268
+
269
+ export interface ActionMessage {
270
+ type: 'action';
271
+ data: BrowserAction;
272
+ }
273
+
274
+ export interface GetStatusMessage {
275
+ type: 'get_status';
276
+ }
277
+
278
+ export type ClientMessage = PingMessage | ActionMessage | GetStatusMessage;
@@ -0,0 +1,378 @@
1
+ /**
2
+ * BrowserApiClient Unit Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, mock, spyOn } from 'bun:test';
6
+ import axios from 'axios';
7
+ import { BrowserApiClient } from './browser.js';
8
+
9
+ // Mock axios
10
+ const mockAxiosInstance = {
11
+ get: mock(() => Promise.resolve({ data: {} })),
12
+ post: mock(() => Promise.resolve({ data: {} })),
13
+ delete: mock(() => Promise.resolve({ data: {} })),
14
+ defaults: {
15
+ baseURL: 'https://api.desplega.ai/browsers/v1',
16
+ headers: {},
17
+ },
18
+ };
19
+
20
+ mock.module('axios', () => ({
21
+ default: {
22
+ create: () => mockAxiosInstance,
23
+ isAxiosError: (err: unknown) =>
24
+ typeof err === 'object' && err !== null && 'response' in err && 'isAxiosError' in err,
25
+ },
26
+ }));
27
+
28
+ describe('BrowserApiClient', () => {
29
+ let client: BrowserApiClient;
30
+
31
+ beforeEach(() => {
32
+ // Reset mocks
33
+ mockAxiosInstance.get.mockReset();
34
+ mockAxiosInstance.post.mockReset();
35
+ mockAxiosInstance.delete.mockReset();
36
+
37
+ // Clear environment variables
38
+ delete process.env.QA_USE_API_KEY;
39
+ delete process.env.QA_USE_API_URL;
40
+
41
+ client = new BrowserApiClient();
42
+ });
43
+
44
+ describe('constructor', () => {
45
+ it('should use default API URL', () => {
46
+ const c = new BrowserApiClient();
47
+ expect(c.getBaseUrl()).toContain('api.desplega.ai');
48
+ });
49
+
50
+ it('should use custom API URL', () => {
51
+ const c = new BrowserApiClient('https://custom.api.com');
52
+ // The URL should be set via axios.create
53
+ expect(mockAxiosInstance.defaults.baseURL).toBeDefined();
54
+ });
55
+ });
56
+
57
+ describe('setApiKey', () => {
58
+ it('should set API key', () => {
59
+ client.setApiKey('test-key-123');
60
+ expect(client.getApiKey()).toBe('test-key-123');
61
+ });
62
+ });
63
+
64
+ describe('createSession', () => {
65
+ it('should create session with default options', async () => {
66
+ const mockSession = {
67
+ id: 'session-123',
68
+ status: 'starting',
69
+ created_at: '2026-01-23T10:00:00Z',
70
+ };
71
+ mockAxiosInstance.post.mockResolvedValueOnce({ data: mockSession });
72
+
73
+ const session = await client.createSession();
74
+
75
+ expect(mockAxiosInstance.post).toHaveBeenCalledWith('/sessions', {
76
+ headless: true,
77
+ viewport: 'desktop',
78
+ timeout: 300,
79
+ });
80
+ expect(session.id).toBe('session-123');
81
+ expect(session.status).toBe('starting');
82
+ });
83
+
84
+ it('should create session with custom options', async () => {
85
+ const mockSession = {
86
+ id: 'session-456',
87
+ status: 'starting',
88
+ created_at: '2026-01-23T10:00:00Z',
89
+ };
90
+ mockAxiosInstance.post.mockResolvedValueOnce({ data: mockSession });
91
+
92
+ const session = await client.createSession({
93
+ headless: false,
94
+ viewport: 'mobile',
95
+ timeout: 600,
96
+ });
97
+
98
+ expect(mockAxiosInstance.post).toHaveBeenCalledWith('/sessions', {
99
+ headless: false,
100
+ viewport: 'mobile',
101
+ timeout: 600,
102
+ });
103
+ expect(session.id).toBe('session-456');
104
+ });
105
+
106
+ it('should create session with ws_url for remote browser', async () => {
107
+ const mockSession = {
108
+ id: 'session-789',
109
+ status: 'starting',
110
+ created_at: '2026-01-23T10:00:00Z',
111
+ };
112
+ mockAxiosInstance.post.mockResolvedValueOnce({ data: mockSession });
113
+
114
+ const session = await client.createSession({
115
+ headless: true,
116
+ viewport: 'desktop',
117
+ timeout: 300,
118
+ ws_url: 'wss://tunnel.example.com/devtools/browser/abc123',
119
+ });
120
+
121
+ expect(mockAxiosInstance.post).toHaveBeenCalledWith('/sessions', {
122
+ headless: true,
123
+ viewport: 'desktop',
124
+ timeout: 300,
125
+ ws_url: 'wss://tunnel.example.com/devtools/browser/abc123',
126
+ });
127
+ expect(session.id).toBe('session-789');
128
+ });
129
+
130
+ it('should not include ws_url when not provided', async () => {
131
+ const mockSession = {
132
+ id: 'session-abc',
133
+ status: 'starting',
134
+ created_at: '2026-01-23T10:00:00Z',
135
+ };
136
+ mockAxiosInstance.post.mockResolvedValueOnce({ data: mockSession });
137
+
138
+ await client.createSession({
139
+ headless: true,
140
+ viewport: 'desktop',
141
+ });
142
+
143
+ const callArgs = mockAxiosInstance.post.mock.calls[0];
144
+ expect(callArgs[1]).not.toHaveProperty('ws_url');
145
+ });
146
+ });
147
+
148
+ describe('listSessions', () => {
149
+ it('should list sessions from array response', async () => {
150
+ const mockSessions = [
151
+ { id: 'session-1', status: 'active' },
152
+ { id: 'session-2', status: 'starting' },
153
+ ];
154
+ mockAxiosInstance.get.mockResolvedValueOnce({ data: mockSessions });
155
+
156
+ const sessions = await client.listSessions();
157
+
158
+ expect(mockAxiosInstance.get).toHaveBeenCalledWith('/sessions');
159
+ expect(sessions).toHaveLength(2);
160
+ expect(sessions[0].id).toBe('session-1');
161
+ });
162
+
163
+ it('should list sessions from object response', async () => {
164
+ const mockResponse = {
165
+ sessions: [
166
+ { id: 'session-1', status: 'active' },
167
+ { id: 'session-2', status: 'starting' },
168
+ ],
169
+ };
170
+ mockAxiosInstance.get.mockResolvedValueOnce({ data: mockResponse });
171
+
172
+ const sessions = await client.listSessions();
173
+
174
+ expect(sessions).toHaveLength(2);
175
+ });
176
+ });
177
+
178
+ describe('getSession', () => {
179
+ it('should get session by ID', async () => {
180
+ const mockSession = {
181
+ id: 'session-123',
182
+ status: 'active',
183
+ url: 'https://example.com',
184
+ };
185
+ mockAxiosInstance.get.mockResolvedValueOnce({ data: mockSession });
186
+
187
+ const session = await client.getSession('session-123');
188
+
189
+ expect(mockAxiosInstance.get).toHaveBeenCalledWith('/sessions/session-123');
190
+ expect(session.id).toBe('session-123');
191
+ expect(session.status).toBe('active');
192
+ });
193
+ });
194
+
195
+ describe('deleteSession', () => {
196
+ it('should delete session by ID', async () => {
197
+ mockAxiosInstance.delete.mockResolvedValueOnce({ data: {} });
198
+
199
+ await client.deleteSession('session-123');
200
+
201
+ expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/sessions/session-123');
202
+ });
203
+ });
204
+
205
+ describe('executeAction', () => {
206
+ it('should execute goto action', async () => {
207
+ const mockResult = { success: true };
208
+ mockAxiosInstance.post.mockResolvedValueOnce({ data: mockResult });
209
+
210
+ const result = await client.executeAction('session-123', {
211
+ type: 'goto',
212
+ url: 'https://example.com',
213
+ });
214
+
215
+ expect(mockAxiosInstance.post).toHaveBeenCalledWith('/sessions/session-123/action', {
216
+ type: 'goto',
217
+ url: 'https://example.com',
218
+ });
219
+ expect(result.success).toBe(true);
220
+ });
221
+
222
+ it('should execute click action', async () => {
223
+ const mockResult = { success: true };
224
+ mockAxiosInstance.post.mockResolvedValueOnce({ data: mockResult });
225
+
226
+ const result = await client.executeAction('session-123', {
227
+ type: 'click',
228
+ ref: 'e3',
229
+ });
230
+
231
+ expect(mockAxiosInstance.post).toHaveBeenCalledWith('/sessions/session-123/action', {
232
+ type: 'click',
233
+ ref: 'e3',
234
+ });
235
+ expect(result.success).toBe(true);
236
+ });
237
+
238
+ it('should execute fill action', async () => {
239
+ const mockResult = { success: true };
240
+ mockAxiosInstance.post.mockResolvedValueOnce({ data: mockResult });
241
+
242
+ const result = await client.executeAction('session-123', {
243
+ type: 'fill',
244
+ ref: 'e4',
245
+ value: 'test@example.com',
246
+ });
247
+
248
+ expect(mockAxiosInstance.post).toHaveBeenCalledWith('/sessions/session-123/action', {
249
+ type: 'fill',
250
+ ref: 'e4',
251
+ value: 'test@example.com',
252
+ });
253
+ expect(result.success).toBe(true);
254
+ });
255
+
256
+ it('should execute scroll action', async () => {
257
+ const mockResult = { success: true };
258
+ mockAxiosInstance.post.mockResolvedValueOnce({ data: mockResult });
259
+
260
+ const result = await client.executeAction('session-123', {
261
+ type: 'scroll',
262
+ direction: 'down',
263
+ amount: 500,
264
+ });
265
+
266
+ expect(mockAxiosInstance.post).toHaveBeenCalledWith('/sessions/session-123/action', {
267
+ type: 'scroll',
268
+ direction: 'down',
269
+ amount: 500,
270
+ });
271
+ expect(result.success).toBe(true);
272
+ });
273
+ });
274
+
275
+ describe('getSnapshot', () => {
276
+ it('should get ARIA snapshot', async () => {
277
+ const mockSnapshot = {
278
+ snapshot: '- heading "Example" [ref=e1]',
279
+ url: 'https://example.com',
280
+ };
281
+ mockAxiosInstance.get.mockResolvedValueOnce({ data: mockSnapshot });
282
+
283
+ const snapshot = await client.getSnapshot('session-123');
284
+
285
+ expect(mockAxiosInstance.get).toHaveBeenCalledWith('/sessions/session-123/snapshot');
286
+ expect(snapshot.snapshot).toContain('Example');
287
+ expect(snapshot.url).toBe('https://example.com');
288
+ });
289
+ });
290
+
291
+ describe('getScreenshot', () => {
292
+ it('should get screenshot as buffer', async () => {
293
+ const mockData = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); // PNG header
294
+ mockAxiosInstance.get.mockResolvedValueOnce({ data: mockData });
295
+
296
+ const buffer = await client.getScreenshot('session-123');
297
+
298
+ expect(mockAxiosInstance.get).toHaveBeenCalledWith('/sessions/session-123/screenshot', {
299
+ responseType: 'arraybuffer',
300
+ });
301
+ expect(Buffer.isBuffer(buffer)).toBe(true);
302
+ });
303
+ });
304
+
305
+ describe('getUrl', () => {
306
+ it('should get current URL', async () => {
307
+ const mockResult = { url: 'https://example.com/page' };
308
+ mockAxiosInstance.get.mockResolvedValueOnce({ data: mockResult });
309
+
310
+ const url = await client.getUrl('session-123');
311
+
312
+ expect(mockAxiosInstance.get).toHaveBeenCalledWith('/sessions/session-123/url');
313
+ expect(url).toBe('https://example.com/page');
314
+ });
315
+ });
316
+
317
+ describe('getStreamUrl', () => {
318
+ it('should return WebSocket URL', () => {
319
+ const url = client.getStreamUrl('session-123');
320
+
321
+ expect(url).toContain('ws');
322
+ expect(url).toContain('session-123');
323
+ expect(url).toContain('/stream');
324
+ });
325
+ });
326
+
327
+ describe('waitForStatus', () => {
328
+ it('should return immediately if session is already active', async () => {
329
+ const mockSession = {
330
+ id: 'session-123',
331
+ status: 'active',
332
+ created_at: '2026-01-23T10:00:00Z',
333
+ };
334
+ mockAxiosInstance.get.mockResolvedValueOnce({ data: mockSession });
335
+
336
+ const session = await client.waitForStatus('session-123', 'active', 5000, 100);
337
+
338
+ expect(session.status).toBe('active');
339
+ expect(mockAxiosInstance.get).toHaveBeenCalledTimes(1);
340
+ });
341
+
342
+ it('should poll until session becomes active', async () => {
343
+ const startingSession = {
344
+ id: 'session-123',
345
+ status: 'starting',
346
+ created_at: '2026-01-23T10:00:00Z',
347
+ };
348
+ const activeSession = {
349
+ id: 'session-123',
350
+ status: 'active',
351
+ created_at: '2026-01-23T10:00:00Z',
352
+ };
353
+
354
+ mockAxiosInstance.get
355
+ .mockResolvedValueOnce({ data: startingSession })
356
+ .mockResolvedValueOnce({ data: startingSession })
357
+ .mockResolvedValueOnce({ data: activeSession });
358
+
359
+ const session = await client.waitForStatus('session-123', 'active', 5000, 50);
360
+
361
+ expect(session.status).toBe('active');
362
+ expect(mockAxiosInstance.get).toHaveBeenCalledTimes(3);
363
+ });
364
+
365
+ it('should throw error if session is closed', async () => {
366
+ const closedSession = {
367
+ id: 'session-123',
368
+ status: 'closed',
369
+ created_at: '2026-01-23T10:00:00Z',
370
+ };
371
+ mockAxiosInstance.get.mockResolvedValueOnce({ data: closedSession });
372
+
373
+ await expect(client.waitForStatus('session-123', 'active', 5000, 100)).rejects.toThrow(
374
+ 'closed'
375
+ );
376
+ });
377
+ });
378
+ });