cdp-skill 1.0.8 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -35
- package/SKILL.md +151 -239
- package/install.js +1 -0
- package/package.json +1 -1
- package/src/aria/index.js +8 -0
- package/src/aria/output-processor.js +173 -0
- package/src/aria/role-query.js +1229 -0
- package/src/aria/snapshot.js +459 -0
- package/src/aria.js +237 -43
- package/src/cdp/browser.js +22 -4
- package/src/cdp-skill.js +245 -69
- package/src/dom/click-executor.js +240 -76
- package/src/dom/element-locator.js +34 -25
- package/src/dom/fill-executor.js +55 -27
- package/src/page/dialog-handler.js +119 -0
- package/src/page/page-controller.js +190 -3
- package/src/runner/context-helpers.js +33 -55
- package/src/runner/execute-dynamic.js +8 -7
- package/src/runner/execute-form.js +11 -11
- package/src/runner/execute-input.js +2 -2
- package/src/runner/execute-interaction.js +99 -120
- package/src/runner/execute-navigation.js +11 -26
- package/src/runner/execute-query.js +8 -5
- package/src/runner/step-executors.js +225 -84
- package/src/runner/step-registry.js +1064 -0
- package/src/runner/step-validator.js +16 -754
- package/src/tests/Aria.test.js +1025 -0
- package/src/tests/ContextHelpers.test.js +39 -28
- package/src/tests/ExecuteBrowser.test.js +572 -0
- package/src/tests/ExecuteDynamic.test.js +2 -457
- package/src/tests/ExecuteForm.test.js +700 -0
- package/src/tests/ExecuteInput.test.js +540 -0
- package/src/tests/ExecuteInteraction.test.js +319 -0
- package/src/tests/ExecuteQuery.test.js +820 -0
- package/src/tests/FillExecutor.test.js +2 -2
- package/src/tests/StepValidator.test.js +222 -76
- package/src/tests/TestRunner.test.js +36 -25
- package/src/tests/integration.test.js +2 -1
- package/src/types.js +9 -9
- package/src/utils/backoff.js +118 -0
- package/src/utils/cdp-helpers.js +130 -0
- package/src/utils/devices.js +140 -0
- package/src/utils/errors.js +242 -0
- package/src/utils/index.js +65 -0
- package/src/utils/temp.js +75 -0
- package/src/utils/validators.js +433 -0
- package/src/utils.js +14 -1142
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
createQueryOutputProcessor,
|
|
5
|
+
createRoleQueryExecutor,
|
|
6
|
+
createAriaSnapshot
|
|
7
|
+
} from '../aria.js';
|
|
8
|
+
|
|
9
|
+
describe('ARIA Module', () => {
|
|
10
|
+
let mockSession;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
mockSession = {
|
|
14
|
+
send: mock.fn(async () => ({}))
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
mock.reset();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// QueryOutputProcessor Tests
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
describe('createQueryOutputProcessor', () => {
|
|
27
|
+
let processor;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
processor = createQueryOutputProcessor(mockSession);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should create processor with processOutput method', () => {
|
|
34
|
+
assert.ok(typeof processor.processOutput === 'function');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('processOutput with string mode', () => {
|
|
38
|
+
let mockElementHandle;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
mockElementHandle = {
|
|
42
|
+
evaluate: mock.fn(async () => 'test content')
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should extract text content', async () => {
|
|
47
|
+
const result = await processor.processOutput(mockElementHandle, 'text', { clean: false });
|
|
48
|
+
assert.strictEqual(result, 'test content');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should extract html content', async () => {
|
|
52
|
+
mockElementHandle.evaluate = mock.fn(async () => '<div>test</div>');
|
|
53
|
+
const result = await processor.processOutput(mockElementHandle, 'html', { clean: false });
|
|
54
|
+
assert.strictEqual(result, '<div>test</div>');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should extract href attribute', async () => {
|
|
58
|
+
mockElementHandle.evaluate = mock.fn(async () => 'https://example.com');
|
|
59
|
+
const result = await processor.processOutput(mockElementHandle, 'href', { clean: false });
|
|
60
|
+
assert.strictEqual(result, 'https://example.com');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should extract value attribute', async () => {
|
|
64
|
+
mockElementHandle.evaluate = mock.fn(async () => 'input value');
|
|
65
|
+
const result = await processor.processOutput(mockElementHandle, 'value', { clean: false });
|
|
66
|
+
assert.strictEqual(result, 'input value');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should extract tag name', async () => {
|
|
70
|
+
mockElementHandle.evaluate = mock.fn(async () => 'button');
|
|
71
|
+
const result = await processor.processOutput(mockElementHandle, 'tag', { clean: false });
|
|
72
|
+
assert.strictEqual(result, 'button');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should trim whitespace when clean=true', async () => {
|
|
76
|
+
mockElementHandle.evaluate = mock.fn(async () => ' trimmed ');
|
|
77
|
+
const result = await processor.processOutput(mockElementHandle, 'text', { clean: true });
|
|
78
|
+
assert.strictEqual(result, 'trimmed');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle empty/null values', async () => {
|
|
82
|
+
mockElementHandle.evaluate = mock.fn(async () => null);
|
|
83
|
+
const result = await processor.processOutput(mockElementHandle, 'text', { clean: false });
|
|
84
|
+
assert.strictEqual(result, '');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('processOutput with array of modes', () => {
|
|
89
|
+
let mockElementHandle;
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
mockElementHandle = {
|
|
93
|
+
evaluate: mock.fn(async () => 'test')
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should return object with values for multiple modes', async () => {
|
|
98
|
+
let callCount = 0;
|
|
99
|
+
mockElementHandle.evaluate = mock.fn(async () => {
|
|
100
|
+
callCount++;
|
|
101
|
+
return callCount === 1 ? 'text content' : 'button';
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const result = await processor.processOutput(mockElementHandle, ['text', 'tag'], { clean: false });
|
|
105
|
+
assert.ok(typeof result === 'object');
|
|
106
|
+
assert.strictEqual(result.text, 'text content');
|
|
107
|
+
assert.strictEqual(result.tag, 'button');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('processOutput with object specification', () => {
|
|
112
|
+
let mockElementHandle;
|
|
113
|
+
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
mockElementHandle = {
|
|
116
|
+
evaluate: mock.fn(async () => 'test')
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should handle object with attribute key', async () => {
|
|
121
|
+
mockElementHandle.evaluate = mock.fn(async () => 'data-value');
|
|
122
|
+
|
|
123
|
+
const result = await processor.processOutput(mockElementHandle, { attribute: 'data-test' }, { clean: false });
|
|
124
|
+
assert.ok(typeof result === 'string');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('getAttribute', () => {
|
|
129
|
+
it('should extract custom attribute', async () => {
|
|
130
|
+
const mockElementHandle = {
|
|
131
|
+
evaluate: mock.fn(async () => 'custom-value')
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// processOutput with @attr syntax uses getAttribute internally
|
|
135
|
+
const result = await processor.processOutput(mockElementHandle, '@data-id', { clean: false });
|
|
136
|
+
assert.ok(result !== undefined);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// RoleQueryExecutor Tests
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
describe('createRoleQueryExecutor', () => {
|
|
146
|
+
let executor;
|
|
147
|
+
let mockElementLocator;
|
|
148
|
+
|
|
149
|
+
beforeEach(() => {
|
|
150
|
+
mockElementLocator = {
|
|
151
|
+
querySelector: mock.fn(async () => ({ objectId: 'obj-123' }))
|
|
152
|
+
};
|
|
153
|
+
executor = createRoleQueryExecutor(mockSession, mockElementLocator);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should create executor with execute method', () => {
|
|
157
|
+
assert.ok(typeof executor.execute === 'function');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should create executor with queryByRoles method', () => {
|
|
161
|
+
assert.ok(typeof executor.queryByRoles === 'function');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('query by single role', () => {
|
|
165
|
+
it('should query button role', async () => {
|
|
166
|
+
mockSession.send = mock.fn(async (method) => {
|
|
167
|
+
if (method === 'Runtime.evaluate') {
|
|
168
|
+
return {
|
|
169
|
+
result: {
|
|
170
|
+
objectId: 'array-1',
|
|
171
|
+
value: undefined
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (method === 'Runtime.getProperties') {
|
|
176
|
+
return {
|
|
177
|
+
result: [
|
|
178
|
+
{ name: '0', value: { objectId: 'obj-1' } },
|
|
179
|
+
{ name: 'length', value: { value: 1 } }
|
|
180
|
+
]
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (method === 'Runtime.callFunctionOn') {
|
|
184
|
+
return {
|
|
185
|
+
result: { value: 'Submit' }
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return {};
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const result = await executor.execute({ role: 'button' });
|
|
192
|
+
assert.ok(Array.isArray(result.elements) || typeof result === 'object');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should query textbox role', async () => {
|
|
196
|
+
mockSession.send = mock.fn(async (method) => {
|
|
197
|
+
if (method === 'Runtime.evaluate') {
|
|
198
|
+
return { result: { objectId: 'array-1', value: undefined } };
|
|
199
|
+
}
|
|
200
|
+
if (method === 'Runtime.getProperties') {
|
|
201
|
+
return { result: [] };
|
|
202
|
+
}
|
|
203
|
+
if (method === 'Runtime.releaseObject') {
|
|
204
|
+
return {};
|
|
205
|
+
}
|
|
206
|
+
return {};
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await executor.execute({ role: 'textbox' });
|
|
210
|
+
const calls = mockSession.send.mock.calls;
|
|
211
|
+
assert.ok(calls.length > 0);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should query checkbox role', async () => {
|
|
215
|
+
mockSession.send = mock.fn(async (method) => {
|
|
216
|
+
if (method === 'Runtime.evaluate') {
|
|
217
|
+
return { result: { objectId: 'array-1', value: undefined } };
|
|
218
|
+
}
|
|
219
|
+
if (method === 'Runtime.getProperties') {
|
|
220
|
+
return { result: [] };
|
|
221
|
+
}
|
|
222
|
+
if (method === 'Runtime.releaseObject') {
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
225
|
+
return {};
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await executor.execute({ role: 'checkbox' });
|
|
229
|
+
const calls = mockSession.send.mock.calls;
|
|
230
|
+
assert.ok(calls.length > 0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should query link role', async () => {
|
|
234
|
+
mockSession.send = mock.fn(async (method) => {
|
|
235
|
+
if (method === 'Runtime.evaluate') {
|
|
236
|
+
return { result: { objectId: 'array-1', value: undefined } };
|
|
237
|
+
}
|
|
238
|
+
if (method === 'Runtime.getProperties') {
|
|
239
|
+
return { result: [] };
|
|
240
|
+
}
|
|
241
|
+
if (method === 'Runtime.releaseObject') {
|
|
242
|
+
return {};
|
|
243
|
+
}
|
|
244
|
+
return {};
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await executor.execute({ role: 'link' });
|
|
248
|
+
const calls = mockSession.send.mock.calls;
|
|
249
|
+
assert.ok(calls.length > 0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should query heading role', async () => {
|
|
253
|
+
mockSession.send = mock.fn(async (method) => {
|
|
254
|
+
if (method === 'Runtime.evaluate') {
|
|
255
|
+
return { result: { objectId: 'array-1', value: undefined } };
|
|
256
|
+
}
|
|
257
|
+
if (method === 'Runtime.getProperties') {
|
|
258
|
+
return { result: [] };
|
|
259
|
+
}
|
|
260
|
+
if (method === 'Runtime.releaseObject') {
|
|
261
|
+
return {};
|
|
262
|
+
}
|
|
263
|
+
return {};
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await executor.execute({ role: 'heading' });
|
|
267
|
+
const calls = mockSession.send.mock.calls;
|
|
268
|
+
assert.ok(calls.length > 0);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('query with name filter', () => {
|
|
273
|
+
it('should filter by name (contains)', async () => {
|
|
274
|
+
mockSession.send = mock.fn(async (method) => {
|
|
275
|
+
if (method === 'Runtime.evaluate') {
|
|
276
|
+
return {
|
|
277
|
+
result: { objectId: 'array-1', value: undefined }
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (method === 'Runtime.getProperties') {
|
|
281
|
+
return { result: [] };
|
|
282
|
+
}
|
|
283
|
+
return {};
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await executor.execute({ role: 'button', name: 'Submit' });
|
|
287
|
+
const evaluateCalls = mockSession.send.mock.calls.filter(call => call.arguments[0] === 'Runtime.evaluate');
|
|
288
|
+
assert.ok(evaluateCalls.length > 0);
|
|
289
|
+
// Check that name filter is in the expression
|
|
290
|
+
const expression = evaluateCalls[0].arguments[1].expression;
|
|
291
|
+
assert.ok(expression.includes('Submit') || expression.includes('nameFilter'));
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should filter by name (exact match)', async () => {
|
|
295
|
+
mockSession.send = mock.fn(async (method) => {
|
|
296
|
+
if (method === 'Runtime.evaluate') {
|
|
297
|
+
return {
|
|
298
|
+
result: { objectId: 'array-1', value: undefined }
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (method === 'Runtime.getProperties') {
|
|
302
|
+
return { result: [] };
|
|
303
|
+
}
|
|
304
|
+
return {};
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await executor.execute({ role: 'button', name: 'Submit', nameExact: true });
|
|
308
|
+
const evaluateCalls = mockSession.send.mock.calls.filter(call => call.arguments[0] === 'Runtime.evaluate');
|
|
309
|
+
assert.ok(evaluateCalls.length > 0);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should filter by nameRegex', async () => {
|
|
313
|
+
mockSession.send = mock.fn(async (method) => {
|
|
314
|
+
if (method === 'Runtime.evaluate') {
|
|
315
|
+
return {
|
|
316
|
+
result: { objectId: 'array-1', value: undefined }
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (method === 'Runtime.getProperties') {
|
|
320
|
+
return { result: [] };
|
|
321
|
+
}
|
|
322
|
+
return {};
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await executor.execute({ role: 'button', nameRegex: 'Sub.*' });
|
|
326
|
+
const evaluateCalls = mockSession.send.mock.calls.filter(call => call.arguments[0] === 'Runtime.evaluate');
|
|
327
|
+
assert.ok(evaluateCalls.length > 0);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('query with state filters', () => {
|
|
332
|
+
it('should filter by checked state', async () => {
|
|
333
|
+
mockSession.send = mock.fn(async (method) => {
|
|
334
|
+
if (method === 'Runtime.evaluate') {
|
|
335
|
+
return {
|
|
336
|
+
result: { objectId: 'array-1', value: undefined }
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (method === 'Runtime.getProperties') {
|
|
340
|
+
return { result: [] };
|
|
341
|
+
}
|
|
342
|
+
return {};
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
await executor.execute({ role: 'checkbox', checked: true });
|
|
346
|
+
const evaluateCalls = mockSession.send.mock.calls.filter(call => call.arguments[0] === 'Runtime.evaluate');
|
|
347
|
+
assert.ok(evaluateCalls.length > 0);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should filter by disabled state', async () => {
|
|
351
|
+
mockSession.send = mock.fn(async (method) => {
|
|
352
|
+
if (method === 'Runtime.evaluate') {
|
|
353
|
+
return {
|
|
354
|
+
result: { objectId: 'array-1', value: undefined }
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (method === 'Runtime.getProperties') {
|
|
358
|
+
return { result: [] };
|
|
359
|
+
}
|
|
360
|
+
return {};
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
await executor.execute({ role: 'button', disabled: false });
|
|
364
|
+
const evaluateCalls = mockSession.send.mock.calls.filter(call => call.arguments[0] === 'Runtime.evaluate');
|
|
365
|
+
assert.ok(evaluateCalls.length > 0);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should filter by heading level', async () => {
|
|
369
|
+
mockSession.send = mock.fn(async (method) => {
|
|
370
|
+
if (method === 'Runtime.evaluate') {
|
|
371
|
+
return {
|
|
372
|
+
result: { objectId: 'array-1', value: undefined }
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (method === 'Runtime.getProperties') {
|
|
376
|
+
return { result: [] };
|
|
377
|
+
}
|
|
378
|
+
return {};
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
await executor.execute({ role: 'heading', level: 2 });
|
|
382
|
+
const evaluateCalls = mockSession.send.mock.calls.filter(call => call.arguments[0] === 'Runtime.evaluate');
|
|
383
|
+
assert.ok(evaluateCalls.length > 0);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe('query with compound roles', () => {
|
|
388
|
+
it('should handle array of roles', async () => {
|
|
389
|
+
mockSession.send = mock.fn(async (method) => {
|
|
390
|
+
if (method === 'Runtime.evaluate') {
|
|
391
|
+
return {
|
|
392
|
+
result: { objectId: 'array-1', value: undefined }
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (method === 'Runtime.getProperties') {
|
|
396
|
+
return { result: [] };
|
|
397
|
+
}
|
|
398
|
+
return {};
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
await executor.execute({ role: ['button', 'link'] });
|
|
402
|
+
const evaluateCalls = mockSession.send.mock.calls.filter(call => call.arguments[0] === 'Runtime.evaluate');
|
|
403
|
+
assert.ok(evaluateCalls.length > 0);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe('query with output specification', () => {
|
|
408
|
+
it('should support output: "text"', async () => {
|
|
409
|
+
mockSession.send = mock.fn(async (method) => {
|
|
410
|
+
if (method === 'Runtime.evaluate') {
|
|
411
|
+
return {
|
|
412
|
+
result: { objectId: 'array-1', value: undefined }
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (method === 'Runtime.getProperties') {
|
|
416
|
+
return {
|
|
417
|
+
result: [
|
|
418
|
+
{ name: '0', value: { objectId: 'obj-1' } },
|
|
419
|
+
{ name: 'length', value: { value: 1 } }
|
|
420
|
+
]
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (method === 'Runtime.callFunctionOn') {
|
|
424
|
+
return {
|
|
425
|
+
result: { value: 'Button text' }
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
return {};
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
await executor.execute({ role: 'button', output: 'text' });
|
|
432
|
+
const callFunctionCalls = mockSession.send.mock.calls.filter(call => call.arguments[0] === 'Runtime.callFunctionOn');
|
|
433
|
+
assert.ok(callFunctionCalls.length > 0);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should support output: array', async () => {
|
|
437
|
+
mockSession.send = mock.fn(async (method) => {
|
|
438
|
+
if (method === 'Runtime.evaluate') {
|
|
439
|
+
return {
|
|
440
|
+
result: { objectId: 'array-1', value: undefined }
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (method === 'Runtime.getProperties') {
|
|
444
|
+
return {
|
|
445
|
+
result: [
|
|
446
|
+
{ name: '0', value: { objectId: 'obj-1' } },
|
|
447
|
+
{ name: 'length', value: { value: 1 } }
|
|
448
|
+
]
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
if (method === 'Runtime.callFunctionOn') {
|
|
452
|
+
return {
|
|
453
|
+
result: { value: 'text' }
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
return {};
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
await executor.execute({ role: 'button', output: ['text', 'tag'] });
|
|
460
|
+
assert.ok(mockSession.send.mock.calls.length > 0);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('should support output: object', async () => {
|
|
464
|
+
mockSession.send = mock.fn(async (method) => {
|
|
465
|
+
if (method === 'Runtime.evaluate') {
|
|
466
|
+
return {
|
|
467
|
+
result: { objectId: 'array-1', value: undefined }
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
if (method === 'Runtime.getProperties') {
|
|
471
|
+
return {
|
|
472
|
+
result: [
|
|
473
|
+
{ name: '0', value: { objectId: 'obj-1' } },
|
|
474
|
+
{ name: 'length', value: { value: 1 } }
|
|
475
|
+
]
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (method === 'Runtime.callFunctionOn') {
|
|
479
|
+
return {
|
|
480
|
+
result: { value: 'value' }
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
return {};
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
await executor.execute({ role: 'button', output: { label: 'text', element: 'tag' } });
|
|
487
|
+
assert.ok(mockSession.send.mock.calls.length > 0);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe('query with limit option', () => {
|
|
492
|
+
it('should limit number of results', async () => {
|
|
493
|
+
mockSession.send = mock.fn(async (method) => {
|
|
494
|
+
if (method === 'Runtime.evaluate') {
|
|
495
|
+
return {
|
|
496
|
+
result: { objectId: 'array-1', value: undefined }
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
if (method === 'Runtime.getProperties') {
|
|
500
|
+
return {
|
|
501
|
+
result: [
|
|
502
|
+
{ name: '0', value: { objectId: 'obj-1' } },
|
|
503
|
+
{ name: '1', value: { objectId: 'obj-2' } },
|
|
504
|
+
{ name: '2', value: { objectId: 'obj-3' } },
|
|
505
|
+
{ name: 'length', value: { value: 3 } }
|
|
506
|
+
]
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
if (method === 'Runtime.callFunctionOn') {
|
|
510
|
+
return {
|
|
511
|
+
result: { value: 'text' }
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
return {};
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const result = await executor.execute({ role: 'button', limit: 2 });
|
|
518
|
+
// Should only process up to limit
|
|
519
|
+
assert.ok(result);
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
describe('error handling', () => {
|
|
524
|
+
it('should handle CDP errors gracefully', async () => {
|
|
525
|
+
mockSession.send = mock.fn(async () => {
|
|
526
|
+
throw new Error('CDP connection failed');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
await assert.rejects(
|
|
530
|
+
async () => await executor.execute({ role: 'button' }),
|
|
531
|
+
Error
|
|
532
|
+
);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('should handle evaluation exceptions', async () => {
|
|
536
|
+
mockSession.send = mock.fn(async (method) => {
|
|
537
|
+
if (method === 'Runtime.evaluate') {
|
|
538
|
+
return {
|
|
539
|
+
exceptionDetails: {
|
|
540
|
+
text: 'JavaScript error'
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return {};
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
await assert.rejects(
|
|
548
|
+
async () => await executor.execute({ role: 'button' }),
|
|
549
|
+
Error
|
|
550
|
+
);
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// ============================================================================
|
|
556
|
+
// AriaSnapshot Tests
|
|
557
|
+
// ============================================================================
|
|
558
|
+
|
|
559
|
+
describe('createAriaSnapshot', () => {
|
|
560
|
+
let snapshot;
|
|
561
|
+
|
|
562
|
+
beforeEach(() => {
|
|
563
|
+
snapshot = createAriaSnapshot(mockSession);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('should create snapshot with generate method', () => {
|
|
567
|
+
assert.ok(typeof snapshot.generate === 'function');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('should create snapshot with getElementByRef method', () => {
|
|
571
|
+
assert.ok(typeof snapshot.getElementByRef === 'function');
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
describe('generate snapshot', () => {
|
|
575
|
+
it('should generate basic snapshot', async () => {
|
|
576
|
+
mockSession.send = mock.fn(async () => ({
|
|
577
|
+
result: {
|
|
578
|
+
value: {
|
|
579
|
+
tree: { role: 'document', children: [] },
|
|
580
|
+
yaml: 'document:\n',
|
|
581
|
+
refs: new Map(),
|
|
582
|
+
snapshotId: 's1'
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}));
|
|
586
|
+
|
|
587
|
+
const result = await snapshot.generate();
|
|
588
|
+
assert.ok(result.tree);
|
|
589
|
+
assert.ok(result.snapshotId);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('should support mode: "ai"', async () => {
|
|
593
|
+
mockSession.send = mock.fn(async (method, params) => ({
|
|
594
|
+
result: {
|
|
595
|
+
value: {
|
|
596
|
+
tree: { role: 'document', children: [] },
|
|
597
|
+
yaml: 'document:\n',
|
|
598
|
+
refs: new Map(),
|
|
599
|
+
snapshotId: 's1'
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}));
|
|
603
|
+
|
|
604
|
+
await snapshot.generate({ mode: 'ai' });
|
|
605
|
+
const calls = mockSession.send.mock.calls;
|
|
606
|
+
assert.ok(calls.length > 0);
|
|
607
|
+
// Check expression includes mode parameter
|
|
608
|
+
const expression = calls[0].arguments[1].expression;
|
|
609
|
+
assert.ok(expression.includes('ai'));
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('should support mode: "full"', async () => {
|
|
613
|
+
mockSession.send = mock.fn(async () => ({
|
|
614
|
+
result: {
|
|
615
|
+
value: {
|
|
616
|
+
tree: { role: 'document', children: [] },
|
|
617
|
+
yaml: 'document:\n',
|
|
618
|
+
refs: new Map(),
|
|
619
|
+
snapshotId: 's1'
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}));
|
|
623
|
+
|
|
624
|
+
await snapshot.generate({ mode: 'full' });
|
|
625
|
+
const calls = mockSession.send.mock.calls;
|
|
626
|
+
assert.ok(calls.length > 0);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('should support detail: "summary"', async () => {
|
|
630
|
+
mockSession.send = mock.fn(async () => ({
|
|
631
|
+
result: {
|
|
632
|
+
value: {
|
|
633
|
+
tree: { role: 'document', children: [
|
|
634
|
+
{ role: 'main', name: 'Main content', children: [] },
|
|
635
|
+
{ role: 'button', name: 'Submit', children: [] }
|
|
636
|
+
]},
|
|
637
|
+
yaml: 'document:\n',
|
|
638
|
+
refs: new Map(),
|
|
639
|
+
snapshotId: 's1'
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}));
|
|
643
|
+
|
|
644
|
+
const result = await snapshot.generate({ detail: 'summary' });
|
|
645
|
+
assert.ok(result.snapshotId);
|
|
646
|
+
// Summary view should have landmarks or stats
|
|
647
|
+
assert.ok(result.landmarks || result.stats || result.tree);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('should support detail: "interactive"', async () => {
|
|
651
|
+
mockSession.send = mock.fn(async () => ({
|
|
652
|
+
result: {
|
|
653
|
+
value: {
|
|
654
|
+
tree: { role: 'document', children: [
|
|
655
|
+
{ role: 'button', name: 'Click me', children: [] },
|
|
656
|
+
{ role: 'textbox', name: 'Username', children: [] }
|
|
657
|
+
]},
|
|
658
|
+
yaml: 'document:\n',
|
|
659
|
+
refs: new Map(),
|
|
660
|
+
snapshotId: 's1'
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}));
|
|
664
|
+
|
|
665
|
+
const result = await snapshot.generate({ detail: 'interactive' });
|
|
666
|
+
assert.ok(result.snapshotId);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('should support detail: "full"', async () => {
|
|
670
|
+
mockSession.send = mock.fn(async () => ({
|
|
671
|
+
result: {
|
|
672
|
+
value: {
|
|
673
|
+
tree: { role: 'document', children: [] },
|
|
674
|
+
yaml: 'document:\n',
|
|
675
|
+
refs: new Map(),
|
|
676
|
+
snapshotId: 's1'
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}));
|
|
680
|
+
|
|
681
|
+
const result = await snapshot.generate({ detail: 'full' });
|
|
682
|
+
assert.ok(result.tree);
|
|
683
|
+
assert.ok(result.snapshotId);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('should support maxDepth option', async () => {
|
|
687
|
+
mockSession.send = mock.fn(async (method, params) => ({
|
|
688
|
+
result: {
|
|
689
|
+
value: {
|
|
690
|
+
tree: { role: 'document', children: [] },
|
|
691
|
+
yaml: 'document:\n',
|
|
692
|
+
refs: new Map(),
|
|
693
|
+
snapshotId: 's1'
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}));
|
|
697
|
+
|
|
698
|
+
await snapshot.generate({ maxDepth: 3 });
|
|
699
|
+
const calls = mockSession.send.mock.calls;
|
|
700
|
+
const expression = calls[0].arguments[1].expression;
|
|
701
|
+
assert.ok(expression.includes('maxDepth'));
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('should support maxElements option', async () => {
|
|
705
|
+
mockSession.send = mock.fn(async () => ({
|
|
706
|
+
result: {
|
|
707
|
+
value: {
|
|
708
|
+
tree: { role: 'document', children: [] },
|
|
709
|
+
yaml: 'document:\n',
|
|
710
|
+
refs: new Map(),
|
|
711
|
+
snapshotId: 's1'
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}));
|
|
715
|
+
|
|
716
|
+
await snapshot.generate({ maxElements: 100 });
|
|
717
|
+
const calls = mockSession.send.mock.calls;
|
|
718
|
+
assert.ok(calls.length > 0);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it('should support viewportOnly option', async () => {
|
|
722
|
+
mockSession.send = mock.fn(async () => ({
|
|
723
|
+
result: {
|
|
724
|
+
value: {
|
|
725
|
+
tree: { role: 'document', children: [] },
|
|
726
|
+
yaml: 'document:\n',
|
|
727
|
+
refs: new Map(),
|
|
728
|
+
snapshotId: 's1'
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}));
|
|
732
|
+
|
|
733
|
+
await snapshot.generate({ viewportOnly: true });
|
|
734
|
+
const calls = mockSession.send.mock.calls;
|
|
735
|
+
const expression = calls[0].arguments[1].expression;
|
|
736
|
+
assert.ok(expression.includes('viewportOnly'));
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('should support pierceShadow option', async () => {
|
|
740
|
+
mockSession.send = mock.fn(async () => ({
|
|
741
|
+
result: {
|
|
742
|
+
value: {
|
|
743
|
+
tree: { role: 'document', children: [] },
|
|
744
|
+
yaml: 'document:\n',
|
|
745
|
+
refs: new Map(),
|
|
746
|
+
snapshotId: 's1'
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}));
|
|
750
|
+
|
|
751
|
+
await snapshot.generate({ pierceShadow: true });
|
|
752
|
+
const calls = mockSession.send.mock.calls;
|
|
753
|
+
const expression = calls[0].arguments[1].expression;
|
|
754
|
+
assert.ok(expression.includes('pierceShadow'));
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it('should support root selector option', async () => {
|
|
758
|
+
mockSession.send = mock.fn(async () => ({
|
|
759
|
+
result: {
|
|
760
|
+
value: {
|
|
761
|
+
tree: { role: 'main', children: [] },
|
|
762
|
+
yaml: 'main:\n',
|
|
763
|
+
refs: new Map(),
|
|
764
|
+
snapshotId: 's1'
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}));
|
|
768
|
+
|
|
769
|
+
await snapshot.generate({ root: 'main' });
|
|
770
|
+
const calls = mockSession.send.mock.calls;
|
|
771
|
+
const expression = calls[0].arguments[1].expression;
|
|
772
|
+
assert.ok(expression.includes('main'));
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('should support since option for change detection', async () => {
|
|
776
|
+
mockSession.send = mock.fn(async () => ({
|
|
777
|
+
result: {
|
|
778
|
+
value: {
|
|
779
|
+
unchanged: true,
|
|
780
|
+
snapshotId: 's2',
|
|
781
|
+
message: 'Page unchanged since s1'
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}));
|
|
785
|
+
|
|
786
|
+
const result = await snapshot.generate({ since: 's1' });
|
|
787
|
+
assert.strictEqual(result.unchanged, true);
|
|
788
|
+
assert.ok(result.message);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it('should handle snapshot generation errors', async () => {
|
|
792
|
+
mockSession.send = mock.fn(async () => ({
|
|
793
|
+
exceptionDetails: {
|
|
794
|
+
text: 'Failed to generate snapshot'
|
|
795
|
+
}
|
|
796
|
+
}));
|
|
797
|
+
|
|
798
|
+
await assert.rejects(
|
|
799
|
+
async () => await snapshot.generate(),
|
|
800
|
+
/Snapshot generation failed/
|
|
801
|
+
);
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
describe('getElementByRef', () => {
|
|
806
|
+
it('should retrieve element info by ref', async () => {
|
|
807
|
+
mockSession.send = mock.fn(async (method) => {
|
|
808
|
+
if (method === 'Runtime.evaluate') {
|
|
809
|
+
return {
|
|
810
|
+
result: {
|
|
811
|
+
value: {
|
|
812
|
+
selector: '#submit-btn',
|
|
813
|
+
box: { x: 10, y: 20, width: 100, height: 30 },
|
|
814
|
+
isConnected: true,
|
|
815
|
+
isVisible: true
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
return {};
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
const refInfo = await snapshot.getElementByRef('s1e1');
|
|
824
|
+
|
|
825
|
+
// Should return ref info with box, isConnected, etc
|
|
826
|
+
assert.ok(refInfo);
|
|
827
|
+
assert.ok(refInfo.box);
|
|
828
|
+
assert.ok(typeof refInfo.isConnected === 'boolean');
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it('should return null for non-existent ref', async () => {
|
|
832
|
+
mockSession.send = mock.fn(async () => ({
|
|
833
|
+
result: { value: null }
|
|
834
|
+
}));
|
|
835
|
+
|
|
836
|
+
const refInfo = await snapshot.getElementByRef('s99e99');
|
|
837
|
+
|
|
838
|
+
assert.strictEqual(refInfo, null);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('should detect stale refs', async () => {
|
|
842
|
+
mockSession.send = mock.fn(async () => ({
|
|
843
|
+
result: {
|
|
844
|
+
value: {
|
|
845
|
+
stale: true,
|
|
846
|
+
message: 'Element no longer attached to DOM'
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}));
|
|
850
|
+
|
|
851
|
+
const refInfo = await snapshot.getElementByRef('s1e1');
|
|
852
|
+
|
|
853
|
+
// Stale refs return object with stale flag
|
|
854
|
+
if (refInfo) {
|
|
855
|
+
assert.ok(refInfo.stale === true || typeof refInfo === 'object');
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
describe('ref management', () => {
|
|
861
|
+
it('should generate unique snapshot IDs', async () => {
|
|
862
|
+
mockSession.send = mock.fn(async () => ({
|
|
863
|
+
result: {
|
|
864
|
+
value: {
|
|
865
|
+
tree: { role: 'document', children: [] },
|
|
866
|
+
yaml: 'document:\n',
|
|
867
|
+
refs: new Map(),
|
|
868
|
+
snapshotId: 's1'
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}));
|
|
872
|
+
|
|
873
|
+
const result1 = await snapshot.generate();
|
|
874
|
+
|
|
875
|
+
mockSession.send = mock.fn(async () => ({
|
|
876
|
+
result: {
|
|
877
|
+
value: {
|
|
878
|
+
tree: { role: 'document', children: [] },
|
|
879
|
+
yaml: 'document:\n',
|
|
880
|
+
refs: new Map(),
|
|
881
|
+
snapshotId: 's2'
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}));
|
|
885
|
+
|
|
886
|
+
const result2 = await snapshot.generate();
|
|
887
|
+
|
|
888
|
+
// IDs should be tracked (may or may not be different depending on implementation)
|
|
889
|
+
assert.ok(result1.snapshotId);
|
|
890
|
+
assert.ok(result2.snapshotId);
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it('should support preserveRefs option', async () => {
|
|
894
|
+
mockSession.send = mock.fn(async () => ({
|
|
895
|
+
result: {
|
|
896
|
+
value: {
|
|
897
|
+
tree: { role: 'document', children: [] },
|
|
898
|
+
yaml: 'document:\n',
|
|
899
|
+
refs: new Map(),
|
|
900
|
+
snapshotId: 's1'
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}));
|
|
904
|
+
|
|
905
|
+
await snapshot.generate({ preserveRefs: true });
|
|
906
|
+
const calls = mockSession.send.mock.calls;
|
|
907
|
+
const expression = calls[0].arguments[1].expression;
|
|
908
|
+
assert.ok(expression.includes('preserveRefs'));
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
describe('frame context support', () => {
|
|
913
|
+
it('should inject contextId when getFrameContext is provided', async () => {
|
|
914
|
+
const mockGetFrameContext = () => 'frame-123';
|
|
915
|
+
const frameSnapshot = createAriaSnapshot(mockSession, { getFrameContext: mockGetFrameContext });
|
|
916
|
+
|
|
917
|
+
mockSession.send = mock.fn(async (method, params) => {
|
|
918
|
+
// Verify contextId is included
|
|
919
|
+
if (method === 'Runtime.evaluate') {
|
|
920
|
+
assert.strictEqual(params.contextId, 'frame-123');
|
|
921
|
+
}
|
|
922
|
+
return {
|
|
923
|
+
result: {
|
|
924
|
+
value: {
|
|
925
|
+
tree: { role: 'document', children: [] },
|
|
926
|
+
yaml: 'document:\n',
|
|
927
|
+
refs: new Map(),
|
|
928
|
+
snapshotId: 's1'
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
await frameSnapshot.generate();
|
|
935
|
+
assert.ok(mockSession.send.mock.calls.length > 0);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it('should work without frame context', async () => {
|
|
939
|
+
mockSession.send = mock.fn(async (method, params) => {
|
|
940
|
+
// Verify contextId is NOT included when no getFrameContext
|
|
941
|
+
if (method === 'Runtime.evaluate') {
|
|
942
|
+
assert.strictEqual(params.contextId, undefined);
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
result: {
|
|
946
|
+
value: {
|
|
947
|
+
tree: { role: 'document', children: [] },
|
|
948
|
+
yaml: 'document:\n',
|
|
949
|
+
refs: new Map(),
|
|
950
|
+
snapshotId: 's1'
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
await snapshot.generate();
|
|
957
|
+
assert.ok(mockSession.send.mock.calls.length > 0);
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
// ============================================================================
|
|
963
|
+
// Integration Tests
|
|
964
|
+
// ============================================================================
|
|
965
|
+
|
|
966
|
+
describe('Integration', () => {
|
|
967
|
+
it('should create all three factory functions', () => {
|
|
968
|
+
const processor = createQueryOutputProcessor(mockSession);
|
|
969
|
+
const executor = createRoleQueryExecutor(mockSession, {});
|
|
970
|
+
const snapshot = createAriaSnapshot(mockSession);
|
|
971
|
+
|
|
972
|
+
assert.ok(processor);
|
|
973
|
+
assert.ok(executor);
|
|
974
|
+
assert.ok(snapshot);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
it('should handle role queries with snapshot refs', async () => {
|
|
978
|
+
const snapshot = createAriaSnapshot(mockSession);
|
|
979
|
+
|
|
980
|
+
let callCount = 0;
|
|
981
|
+
mockSession.send = mock.fn(async (method) => {
|
|
982
|
+
callCount++;
|
|
983
|
+
if (callCount === 1) {
|
|
984
|
+
// First call: generate snapshot
|
|
985
|
+
return {
|
|
986
|
+
result: {
|
|
987
|
+
value: {
|
|
988
|
+
tree: {
|
|
989
|
+
role: 'document',
|
|
990
|
+
children: [
|
|
991
|
+
{ role: 'button', name: 'Click me', ref: 's1e1' }
|
|
992
|
+
]
|
|
993
|
+
},
|
|
994
|
+
yaml: 'document:\n button: Click me [s1e1]\n',
|
|
995
|
+
refs: new Map([
|
|
996
|
+
['s1e1', { ref: 's1e1', role: 'button', name: 'Click me' }]
|
|
997
|
+
]),
|
|
998
|
+
snapshotId: 's1'
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
} else {
|
|
1003
|
+
// Second call: getElementByRef
|
|
1004
|
+
return {
|
|
1005
|
+
result: {
|
|
1006
|
+
value: {
|
|
1007
|
+
selector: '#btn',
|
|
1008
|
+
box: { x: 0, y: 0, width: 50, height: 30 },
|
|
1009
|
+
isConnected: true,
|
|
1010
|
+
isVisible: true
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
const result = await snapshot.generate();
|
|
1018
|
+
assert.ok(result.snapshotId);
|
|
1019
|
+
|
|
1020
|
+
// Refs should be accessible via getElementByRef
|
|
1021
|
+
const refInfo = await snapshot.getElementByRef('s1e1');
|
|
1022
|
+
assert.ok(refInfo === null || typeof refInfo === 'object');
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
});
|