btcp-browser-agent 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.
Files changed (117) hide show
  1. package/CLAUDE.md +230 -0
  2. package/LICENSE +21 -0
  3. package/README.md +309 -0
  4. package/SKILL.md +143 -0
  5. package/SNAPSHOT_IMPROVEMENTS.md +302 -0
  6. package/USAGE.md +146 -0
  7. package/dist/index.d.ts +34 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +35 -0
  10. package/dist/index.js.map +1 -0
  11. package/docs/browser-cli-design.md +500 -0
  12. package/examples/chrome-extension/CHANGELOG.md +210 -0
  13. package/examples/chrome-extension/DEBUG.md +231 -0
  14. package/examples/chrome-extension/ERROR_FIXED.md +147 -0
  15. package/examples/chrome-extension/QUICK_TEST.md +189 -0
  16. package/examples/chrome-extension/README.md +149 -0
  17. package/examples/chrome-extension/SESSION_ONLY_MODE.md +305 -0
  18. package/examples/chrome-extension/TEST_WITH_YOUR_TABS.md +97 -0
  19. package/examples/chrome-extension/build.js +43 -0
  20. package/examples/chrome-extension/manifest.json +37 -0
  21. package/examples/chrome-extension/package-lock.json +1063 -0
  22. package/examples/chrome-extension/package.json +21 -0
  23. package/examples/chrome-extension/popup.html +195 -0
  24. package/examples/chrome-extension/src/background.ts +12 -0
  25. package/examples/chrome-extension/src/content.ts +7 -0
  26. package/examples/chrome-extension/src/popup.ts +303 -0
  27. package/examples/chrome-extension/src/scenario-google-github.ts +389 -0
  28. package/examples/chrome-extension/test-page.html +127 -0
  29. package/examples/chrome-extension/tests/README.md +206 -0
  30. package/examples/chrome-extension/tests/scenario-google-to-github-star.ts +380 -0
  31. package/examples/chrome-extension/tsconfig.json +14 -0
  32. package/examples/snapshots/README.md +207 -0
  33. package/examples/snapshots/amazon-com-detail.html +9528 -0
  34. package/examples/snapshots/amazon-com-detail.snapshot.txt +997 -0
  35. package/examples/snapshots/convert-snapshots.ts +97 -0
  36. package/examples/snapshots/edition-cnn-com.html +13292 -0
  37. package/examples/snapshots/edition-cnn-com.snapshot.txt +562 -0
  38. package/examples/snapshots/github-com-microsoft-vscode.html +2916 -0
  39. package/examples/snapshots/github-com-microsoft-vscode.snapshot.txt +455 -0
  40. package/examples/snapshots/google-search.html +20012 -0
  41. package/examples/snapshots/google-search.snapshot.txt +195 -0
  42. package/examples/snapshots/metadata.json +86 -0
  43. package/examples/snapshots/npr-org-templates.html +2031 -0
  44. package/examples/snapshots/npr-org-templates.snapshot.txt +224 -0
  45. package/examples/snapshots/stackoverflow-com.html +5216 -0
  46. package/examples/snapshots/stackoverflow-com.snapshot.txt +2404 -0
  47. package/examples/snapshots/test-all-mode.html +46 -0
  48. package/examples/snapshots/test-all-mode.snapshot.txt +5 -0
  49. package/examples/snapshots/validate.test.ts +296 -0
  50. package/package.json +65 -0
  51. package/packages/cli/package.json +42 -0
  52. package/packages/cli/src/__tests__/cli.test.ts +434 -0
  53. package/packages/cli/src/__tests__/errors.test.ts +226 -0
  54. package/packages/cli/src/__tests__/executor.test.ts +275 -0
  55. package/packages/cli/src/__tests__/formatter.test.ts +260 -0
  56. package/packages/cli/src/__tests__/parser.test.ts +288 -0
  57. package/packages/cli/src/__tests__/suggestions.test.ts +255 -0
  58. package/packages/cli/src/commands/back.ts +22 -0
  59. package/packages/cli/src/commands/check.ts +33 -0
  60. package/packages/cli/src/commands/clear.ts +33 -0
  61. package/packages/cli/src/commands/click.ts +32 -0
  62. package/packages/cli/src/commands/closetab.ts +31 -0
  63. package/packages/cli/src/commands/eval.ts +41 -0
  64. package/packages/cli/src/commands/fill.ts +30 -0
  65. package/packages/cli/src/commands/focus.ts +33 -0
  66. package/packages/cli/src/commands/forward.ts +22 -0
  67. package/packages/cli/src/commands/goto.ts +34 -0
  68. package/packages/cli/src/commands/help.ts +162 -0
  69. package/packages/cli/src/commands/hover.ts +34 -0
  70. package/packages/cli/src/commands/index.ts +129 -0
  71. package/packages/cli/src/commands/newtab.ts +35 -0
  72. package/packages/cli/src/commands/press.ts +40 -0
  73. package/packages/cli/src/commands/reload.ts +25 -0
  74. package/packages/cli/src/commands/screenshot.ts +27 -0
  75. package/packages/cli/src/commands/scroll.ts +64 -0
  76. package/packages/cli/src/commands/select.ts +35 -0
  77. package/packages/cli/src/commands/snapshot.ts +21 -0
  78. package/packages/cli/src/commands/tab.ts +32 -0
  79. package/packages/cli/src/commands/tabs.ts +26 -0
  80. package/packages/cli/src/commands/text.ts +27 -0
  81. package/packages/cli/src/commands/title.ts +17 -0
  82. package/packages/cli/src/commands/type.ts +38 -0
  83. package/packages/cli/src/commands/uncheck.ts +33 -0
  84. package/packages/cli/src/commands/url.ts +17 -0
  85. package/packages/cli/src/commands/wait.ts +54 -0
  86. package/packages/cli/src/errors.ts +164 -0
  87. package/packages/cli/src/executor.ts +68 -0
  88. package/packages/cli/src/formatter.ts +215 -0
  89. package/packages/cli/src/index.ts +257 -0
  90. package/packages/cli/src/parser.ts +195 -0
  91. package/packages/cli/src/suggestions.ts +207 -0
  92. package/packages/cli/src/terminal/Terminal.ts +365 -0
  93. package/packages/cli/src/terminal/index.ts +5 -0
  94. package/packages/cli/src/types.ts +155 -0
  95. package/packages/cli/tsconfig.json +20 -0
  96. package/packages/core/package.json +35 -0
  97. package/packages/core/src/actions.ts +1210 -0
  98. package/packages/core/src/errors.ts +296 -0
  99. package/packages/core/src/index.test.ts +638 -0
  100. package/packages/core/src/index.ts +220 -0
  101. package/packages/core/src/ref-map.ts +107 -0
  102. package/packages/core/src/snapshot.ts +873 -0
  103. package/packages/core/src/types.ts +536 -0
  104. package/packages/core/tsconfig.json +23 -0
  105. package/packages/extension/README.md +129 -0
  106. package/packages/extension/package.json +43 -0
  107. package/packages/extension/src/background.ts +888 -0
  108. package/packages/extension/src/content.ts +172 -0
  109. package/packages/extension/src/index.ts +579 -0
  110. package/packages/extension/src/session-manager.ts +385 -0
  111. package/packages/extension/src/session-types.ts +144 -0
  112. package/packages/extension/src/types.ts +162 -0
  113. package/packages/extension/tsconfig.json +28 -0
  114. package/src/index.ts +64 -0
  115. package/tsconfig.build.json +12 -0
  116. package/tsconfig.json +26 -0
  117. package/vitest.config.ts +13 -0
@@ -0,0 +1,638 @@
1
+ /**
2
+ * @btcp/core - Tests for DOM actions and snapshot
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
6
+ import { createAgent, createSnapshot, createRefMap, DOMActions } from './index.js';
7
+ import type { Command } from './types.js';
8
+
9
+ describe('@btcp/core', () => {
10
+ beforeEach(() => {
11
+ document.body.innerHTML = '';
12
+ });
13
+
14
+ describe('createAgent', () => {
15
+ it('should create an agent instance', () => {
16
+ const agent = createAgent(document, window);
17
+ expect(agent).toBeDefined();
18
+ expect(agent.execute).toBeDefined();
19
+ expect(agent.executeJson).toBeDefined();
20
+ });
21
+ });
22
+
23
+ describe('snapshot', () => {
24
+ it('should generate snapshot for button', async () => {
25
+ document.body.innerHTML = '<button>Click me</button>';
26
+ const agent = createAgent(document, window);
27
+
28
+ const response = await agent.execute({
29
+ id: '1',
30
+ action: 'snapshot',
31
+ });
32
+
33
+ expect(response.success).toBe(true);
34
+ if (response.success) {
35
+ expect(response.data).toContain('button');
36
+ expect(response.data).toContain('Click me');
37
+ expect(response.data).toContain('@ref:');
38
+ }
39
+ });
40
+
41
+ it('should generate refs for interactive elements', async () => {
42
+ document.body.innerHTML = `
43
+ <button>Submit</button>
44
+ <a href="/home">Home</a>
45
+ <input type="text" placeholder="Name">
46
+ `;
47
+ const agent = createAgent(document, window);
48
+
49
+ const response = await agent.execute({ id: '1', action: 'snapshot' });
50
+
51
+ expect(response.success).toBe(true);
52
+ if (response.success) {
53
+ // Snapshot now returns string directly (refs are internal)
54
+ expect(typeof response.data).toBe('string');
55
+ expect(response.data.length).toBeGreaterThan(0);
56
+ }
57
+ });
58
+
59
+ it('should skip hidden elements', async () => {
60
+ document.body.innerHTML = `
61
+ <button>Visible</button>
62
+ <button style="display: none">Hidden</button>
63
+ `;
64
+ const agent = createAgent(document, window);
65
+
66
+ const response = await agent.execute({ id: '1', action: 'snapshot' });
67
+
68
+ expect(response.success).toBe(true);
69
+ if (response.success) {
70
+ expect(response.data).toContain('Visible');
71
+ expect(response.data).not.toContain('Hidden');
72
+ }
73
+ });
74
+
75
+ it('should use aria-label for name', async () => {
76
+ document.body.innerHTML = '<button aria-label="Close dialog">X</button>';
77
+ const agent = createAgent(document, window);
78
+
79
+ const response = await agent.execute({ id: '1', action: 'snapshot' });
80
+
81
+ expect(response.success).toBe(true);
82
+ if (response.success) {
83
+ expect(response.data).toContain('Close dialog');
84
+ }
85
+ });
86
+
87
+ it('should filter snapshot with grep option', async () => {
88
+ document.body.innerHTML = `
89
+ <button>Submit</button>
90
+ <button>Cancel</button>
91
+ <a href="/home">Home</a>
92
+ `;
93
+ const agent = createAgent(document, window);
94
+
95
+ const response = await agent.execute({
96
+ id: '1',
97
+ action: 'snapshot',
98
+ grep: 'Submit',
99
+ });
100
+
101
+ expect(response.success).toBe(true);
102
+ if (response.success) {
103
+ expect(response.data).toContain('Submit');
104
+ expect(response.data).not.toContain('Cancel');
105
+ expect(response.data).not.toContain('Home');
106
+ expect(response.data).toContain('grep=Submit');
107
+ expect(response.data).toContain('matches=1');
108
+ }
109
+ });
110
+
111
+ it('should return all elements when grep matches none', async () => {
112
+ document.body.innerHTML = '<button>Click</button>';
113
+ const agent = createAgent(document, window);
114
+
115
+ const response = await agent.execute({
116
+ id: '1',
117
+ action: 'snapshot',
118
+ grep: 'nonexistent',
119
+ });
120
+
121
+ expect(response.success).toBe(true);
122
+ if (response.success) {
123
+ expect(response.data).toContain('PAGE:');
124
+ expect(response.data).not.toContain('Click');
125
+ }
126
+ });
127
+
128
+ it('should support grep ignoreCase option (-i)', async () => {
129
+ document.body.innerHTML = `
130
+ <button>SUBMIT</button>
131
+ <button>Cancel</button>
132
+ `;
133
+ const agent = createAgent(document, window);
134
+
135
+ const response = await agent.execute({
136
+ id: '1',
137
+ action: 'snapshot',
138
+ grep: { pattern: 'submit', ignoreCase: true },
139
+ });
140
+
141
+ expect(response.success).toBe(true);
142
+ if (response.success) {
143
+ expect(response.data).toContain('SUBMIT');
144
+ expect(response.data).not.toContain('Cancel');
145
+ }
146
+ });
147
+
148
+ it('should support grep invert option (-v)', async () => {
149
+ document.body.innerHTML = `
150
+ <button>Submit</button>
151
+ <button>Cancel</button>
152
+ <a href="/home">Home</a>
153
+ `;
154
+ const agent = createAgent(document, window);
155
+
156
+ const response = await agent.execute({
157
+ id: '1',
158
+ action: 'snapshot',
159
+ grep: { pattern: 'BUTTON', invert: true },
160
+ });
161
+
162
+ expect(response.success).toBe(true);
163
+ if (response.success) {
164
+ expect(response.data).not.toContain('Submit');
165
+ expect(response.data).not.toContain('Cancel');
166
+ expect(response.data).toContain('Home');
167
+ }
168
+ });
169
+
170
+ it('should support grep fixedStrings option (-F)', async () => {
171
+ document.body.innerHTML = `
172
+ <button>Click [here]</button>
173
+ <button>Other</button>
174
+ `;
175
+ const agent = createAgent(document, window);
176
+
177
+ const response = await agent.execute({
178
+ id: '1',
179
+ action: 'snapshot',
180
+ grep: { pattern: '[here]', fixedStrings: true },
181
+ });
182
+
183
+ expect(response.success).toBe(true);
184
+ if (response.success) {
185
+ expect(response.data).toContain('[here]');
186
+ expect(response.data).not.toContain('Other');
187
+ }
188
+ });
189
+ });
190
+
191
+ describe('click', () => {
192
+ it('should click element by CSS selector', async () => {
193
+ document.body.innerHTML = '<button id="btn">Click me</button>';
194
+ const button = document.getElementById('btn')!;
195
+ const handler = vi.fn();
196
+ button.addEventListener('click', handler);
197
+
198
+ const agent = createAgent(document, window);
199
+ const response = await agent.execute({
200
+ id: '1',
201
+ action: 'click',
202
+ selector: '#btn',
203
+ });
204
+
205
+ expect(response.success).toBe(true);
206
+ expect(handler).toHaveBeenCalled();
207
+ });
208
+
209
+ it('should click element by ref', async () => {
210
+ document.body.innerHTML = '<button>Click me</button>';
211
+ const button = document.querySelector('button')!;
212
+ const handler = vi.fn();
213
+ button.addEventListener('click', handler);
214
+
215
+ const agent = createAgent(document, window);
216
+ // First get snapshot to generate refs
217
+ await agent.execute({ id: '1', action: 'snapshot' });
218
+
219
+ const response = await agent.execute({
220
+ id: '2',
221
+ action: 'click',
222
+ selector: '@ref:0',
223
+ });
224
+
225
+ expect(response.success).toBe(true);
226
+ expect(handler).toHaveBeenCalled();
227
+ });
228
+
229
+ it('should return error for non-existent element', async () => {
230
+ const agent = createAgent(document, window);
231
+ const response = await agent.execute({
232
+ id: '1',
233
+ action: 'click',
234
+ selector: '#non-existent',
235
+ });
236
+
237
+ expect(response.success).toBe(false);
238
+ if (!response.success) {
239
+ expect(response.error).toContain('not found');
240
+ }
241
+ });
242
+ });
243
+
244
+ describe('type', () => {
245
+ it('should type text into input', async () => {
246
+ document.body.innerHTML = '<input id="input" type="text">';
247
+ const input = document.getElementById('input') as HTMLInputElement;
248
+
249
+ const agent = createAgent(document, window);
250
+ const response = await agent.execute({
251
+ id: '1',
252
+ action: 'type',
253
+ selector: '#input',
254
+ text: 'Hello',
255
+ });
256
+
257
+ expect(response.success).toBe(true);
258
+ expect(input.value).toBe('Hello');
259
+ });
260
+
261
+ it('should clear input when clear option is true', async () => {
262
+ document.body.innerHTML = '<input id="input" type="text" value="Existing">';
263
+ const input = document.getElementById('input') as HTMLInputElement;
264
+
265
+ const agent = createAgent(document, window);
266
+ const response = await agent.execute({
267
+ id: '1',
268
+ action: 'type',
269
+ selector: '#input',
270
+ text: 'New',
271
+ clear: true,
272
+ });
273
+
274
+ expect(response.success).toBe(true);
275
+ expect(input.value).toBe('New');
276
+ });
277
+ });
278
+
279
+ describe('fill', () => {
280
+ it('should fill input value instantly', async () => {
281
+ document.body.innerHTML = '<input id="input" type="text">';
282
+ const input = document.getElementById('input') as HTMLInputElement;
283
+
284
+ const agent = createAgent(document, window);
285
+ const response = await agent.execute({
286
+ id: '1',
287
+ action: 'fill',
288
+ selector: '#input',
289
+ value: 'Test Value',
290
+ });
291
+
292
+ expect(response.success).toBe(true);
293
+ expect(input.value).toBe('Test Value');
294
+ });
295
+
296
+ it('should dispatch input and change events', async () => {
297
+ document.body.innerHTML = '<input id="input" type="text">';
298
+ const input = document.getElementById('input') as HTMLInputElement;
299
+ const inputHandler = vi.fn();
300
+ const changeHandler = vi.fn();
301
+ input.addEventListener('input', inputHandler);
302
+ input.addEventListener('change', changeHandler);
303
+
304
+ const agent = createAgent(document, window);
305
+ await agent.execute({
306
+ id: '1',
307
+ action: 'fill',
308
+ selector: '#input',
309
+ value: 'Test',
310
+ });
311
+
312
+ expect(inputHandler).toHaveBeenCalled();
313
+ expect(changeHandler).toHaveBeenCalled();
314
+ });
315
+ });
316
+
317
+ describe('check/uncheck', () => {
318
+ it('should check checkbox', async () => {
319
+ document.body.innerHTML = '<input id="cb" type="checkbox">';
320
+ const checkbox = document.getElementById('cb') as HTMLInputElement;
321
+
322
+ const agent = createAgent(document, window);
323
+ const response = await agent.execute({
324
+ id: '1',
325
+ action: 'check',
326
+ selector: '#cb',
327
+ });
328
+
329
+ expect(response.success).toBe(true);
330
+ expect(checkbox.checked).toBe(true);
331
+ });
332
+
333
+ it('should uncheck checkbox', async () => {
334
+ document.body.innerHTML = '<input id="cb" type="checkbox" checked>';
335
+ const checkbox = document.getElementById('cb') as HTMLInputElement;
336
+
337
+ const agent = createAgent(document, window);
338
+ const response = await agent.execute({
339
+ id: '1',
340
+ action: 'uncheck',
341
+ selector: '#cb',
342
+ });
343
+
344
+ expect(response.success).toBe(true);
345
+ expect(checkbox.checked).toBe(false);
346
+ });
347
+ });
348
+
349
+ describe('select', () => {
350
+ it('should select option', async () => {
351
+ document.body.innerHTML = `
352
+ <select id="sel">
353
+ <option value="a">A</option>
354
+ <option value="b">B</option>
355
+ </select>
356
+ `;
357
+ const select = document.getElementById('sel') as HTMLSelectElement;
358
+
359
+ const agent = createAgent(document, window);
360
+ const response = await agent.execute({
361
+ id: '1',
362
+ action: 'select',
363
+ selector: '#sel',
364
+ values: 'b',
365
+ });
366
+
367
+ expect(response.success).toBe(true);
368
+ expect(select.value).toBe('b');
369
+ });
370
+
371
+ it('should select multiple options', async () => {
372
+ document.body.innerHTML = `
373
+ <select id="sel" multiple>
374
+ <option value="a">A</option>
375
+ <option value="b">B</option>
376
+ <option value="c">C</option>
377
+ </select>
378
+ `;
379
+ const select = document.getElementById('sel') as HTMLSelectElement;
380
+
381
+ const agent = createAgent(document, window);
382
+ const response = await agent.execute({
383
+ id: '1',
384
+ action: 'select',
385
+ selector: '#sel',
386
+ values: ['a', 'c'],
387
+ });
388
+
389
+ expect(response.success).toBe(true);
390
+ expect(select.options[0].selected).toBe(true);
391
+ expect(select.options[1].selected).toBe(false);
392
+ expect(select.options[2].selected).toBe(true);
393
+ });
394
+ });
395
+
396
+ describe('hover', () => {
397
+ it('should dispatch hover events', async () => {
398
+ document.body.innerHTML = '<button id="btn">Hover</button>';
399
+ const button = document.getElementById('btn')!;
400
+ const enterHandler = vi.fn();
401
+ const overHandler = vi.fn();
402
+ button.addEventListener('mouseenter', enterHandler);
403
+ button.addEventListener('mouseover', overHandler);
404
+
405
+ const agent = createAgent(document, window);
406
+ const response = await agent.execute({
407
+ id: '1',
408
+ action: 'hover',
409
+ selector: '#btn',
410
+ });
411
+
412
+ expect(response.success).toBe(true);
413
+ expect(enterHandler).toHaveBeenCalled();
414
+ expect(overHandler).toHaveBeenCalled();
415
+ });
416
+ });
417
+
418
+ describe('getText', () => {
419
+ it('should get element text', async () => {
420
+ document.body.innerHTML = '<div id="text">Hello World</div>';
421
+
422
+ const agent = createAgent(document, window);
423
+ const response = await agent.execute({
424
+ id: '1',
425
+ action: 'getText',
426
+ selector: '#text',
427
+ });
428
+
429
+ expect(response.success).toBe(true);
430
+ if (response.success) {
431
+ expect(response.data.text).toBe('Hello World');
432
+ }
433
+ });
434
+ });
435
+
436
+ describe('getAttribute', () => {
437
+ it('should get attribute value', async () => {
438
+ document.body.innerHTML = '<div id="el" data-value="123"></div>';
439
+
440
+ const agent = createAgent(document, window);
441
+ const response = await agent.execute({
442
+ id: '1',
443
+ action: 'getAttribute',
444
+ selector: '#el',
445
+ attribute: 'data-value',
446
+ });
447
+
448
+ expect(response.success).toBe(true);
449
+ if (response.success) {
450
+ expect(response.data.value).toBe('123');
451
+ }
452
+ });
453
+ });
454
+
455
+ describe('isVisible', () => {
456
+ it('should return true for visible element', async () => {
457
+ document.body.innerHTML = '<button id="btn">Visible</button>';
458
+
459
+ const agent = createAgent(document, window);
460
+ const response = await agent.execute({
461
+ id: '1',
462
+ action: 'isVisible',
463
+ selector: '#btn',
464
+ });
465
+
466
+ expect(response.success).toBe(true);
467
+ if (response.success) {
468
+ expect(response.data.visible).toBe(true);
469
+ }
470
+ });
471
+
472
+ it('should return false for hidden element', async () => {
473
+ document.body.innerHTML = '<button id="btn" style="display: none">Hidden</button>';
474
+
475
+ const agent = createAgent(document, window);
476
+ const response = await agent.execute({
477
+ id: '1',
478
+ action: 'isVisible',
479
+ selector: '#btn',
480
+ });
481
+
482
+ expect(response.success).toBe(true);
483
+ if (response.success) {
484
+ expect(response.data.visible).toBe(false);
485
+ }
486
+ });
487
+ });
488
+
489
+ describe('isEnabled', () => {
490
+ it('should return false for disabled element', async () => {
491
+ document.body.innerHTML = '<button id="btn" disabled>Disabled</button>';
492
+
493
+ const agent = createAgent(document, window);
494
+ const response = await agent.execute({
495
+ id: '1',
496
+ action: 'isEnabled',
497
+ selector: '#btn',
498
+ });
499
+
500
+ expect(response.success).toBe(true);
501
+ if (response.success) {
502
+ expect(response.data.enabled).toBe(false);
503
+ }
504
+ });
505
+ });
506
+
507
+ describe('isChecked', () => {
508
+ it('should return true for checked checkbox', async () => {
509
+ document.body.innerHTML = '<input id="cb" type="checkbox" checked>';
510
+
511
+ const agent = createAgent(document, window);
512
+ const response = await agent.execute({
513
+ id: '1',
514
+ action: 'isChecked',
515
+ selector: '#cb',
516
+ });
517
+
518
+ expect(response.success).toBe(true);
519
+ if (response.success) {
520
+ expect(response.data.checked).toBe(true);
521
+ }
522
+ });
523
+ });
524
+
525
+ describe('scroll', () => {
526
+ it('should scroll window', async () => {
527
+ const scrollBy = vi.spyOn(window, 'scrollBy').mockImplementation(() => {});
528
+
529
+ const agent = createAgent(document, window);
530
+ const response = await agent.execute({
531
+ id: '1',
532
+ action: 'scroll',
533
+ y: 100,
534
+ });
535
+
536
+ expect(response.success).toBe(true);
537
+ expect(scrollBy).toHaveBeenCalled();
538
+ });
539
+ });
540
+
541
+ describe('scrollIntoView', () => {
542
+ it('should scroll element into view', async () => {
543
+ document.body.innerHTML = '<div id="target">Target</div>';
544
+ const element = document.getElementById('target')!;
545
+ element.scrollIntoView = vi.fn();
546
+
547
+ const agent = createAgent(document, window);
548
+ const response = await agent.execute({
549
+ id: '1',
550
+ action: 'scrollIntoView',
551
+ selector: '#target',
552
+ });
553
+
554
+ expect(response.success).toBe(true);
555
+ expect(element.scrollIntoView).toHaveBeenCalled();
556
+ });
557
+ });
558
+
559
+ describe('evaluate', () => {
560
+ it('should evaluate JavaScript expression', async () => {
561
+ document.title = 'Test Page';
562
+
563
+ const agent = createAgent(document, window);
564
+ const response = await agent.execute({
565
+ id: '1',
566
+ action: 'evaluate',
567
+ script: 'document.title',
568
+ });
569
+
570
+ expect(response.success).toBe(true);
571
+ if (response.success) {
572
+ expect(response.data.result).toBe('Test Page');
573
+ }
574
+ });
575
+ });
576
+
577
+ describe('executeJson', () => {
578
+ it('should execute command from JSON string', async () => {
579
+ document.body.innerHTML = '<button id="btn">Click</button>';
580
+ const button = document.getElementById('btn')!;
581
+ const handler = vi.fn();
582
+ button.addEventListener('click', handler);
583
+
584
+ const agent = createAgent(document, window);
585
+ const responseJson = await agent.executeJson(
586
+ JSON.stringify({ id: '1', action: 'click', selector: '#btn' })
587
+ );
588
+
589
+ const response = JSON.parse(responseJson);
590
+ expect(response.success).toBe(true);
591
+ expect(handler).toHaveBeenCalled();
592
+ });
593
+
594
+ it('should return error for invalid JSON', async () => {
595
+ const agent = createAgent(document, window);
596
+ const responseJson = await agent.executeJson('invalid json');
597
+
598
+ const response = JSON.parse(responseJson);
599
+ expect(response.success).toBe(false);
600
+ expect(response.error).toContain('parse');
601
+ });
602
+ });
603
+
604
+ describe('refMap', () => {
605
+ it('should create and use ref map', () => {
606
+ document.body.innerHTML = '<button id="btn">Click</button>';
607
+ const button = document.getElementById('btn')!;
608
+
609
+ const refMap = createRefMap();
610
+ const ref = refMap.generateRef(button);
611
+
612
+ expect(ref).toMatch(/^@ref:\d+$/);
613
+ expect(refMap.get(ref)).toBe(button);
614
+ });
615
+
616
+ it('should return same ref for same element', () => {
617
+ document.body.innerHTML = '<button id="btn">Click</button>';
618
+ const button = document.getElementById('btn')!;
619
+
620
+ const refMap = createRefMap();
621
+ const ref1 = refMap.generateRef(button);
622
+ const ref2 = refMap.generateRef(button);
623
+
624
+ expect(ref1).toBe(ref2);
625
+ });
626
+
627
+ it('should clear refs', () => {
628
+ document.body.innerHTML = '<button id="btn">Click</button>';
629
+ const button = document.getElementById('btn')!;
630
+
631
+ const refMap = createRefMap();
632
+ const ref = refMap.generateRef(button);
633
+ refMap.clear();
634
+
635
+ expect(refMap.get(ref)).toBeNull();
636
+ });
637
+ });
638
+ });