cdp-skill 1.0.14 → 1.0.16
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/SKILL.md +8 -4
- package/package.json +1 -1
- package/src/aria.js +14 -7
- package/src/cdp-skill.js +2 -1
- package/src/dom/LazyResolver.js +634 -0
- package/src/dom/click-executor.js +162 -54
- package/src/dom/fill-executor.js +32 -27
- package/src/dom/index.js +3 -0
- package/src/page/page-controller.js +46 -0
- package/src/runner/execute-interaction.js +6 -6
- package/src/runner/execute-navigation.js +3 -3
- package/src/runner/execute-query.js +9 -6
- package/src/runner/step-registry.js +4 -4
- package/src/tests/Aria.test.js +5 -5
- package/src/tests/ClickExecutor.test.js +170 -50
- package/src/tests/ContextHelpers.test.js +2 -2
- package/src/tests/ExecuteInteraction.test.js +2 -2
- package/src/tests/ExecuteQuery.test.js +33 -33
- package/src/tests/FillExecutor.test.js +87 -35
- package/src/tests/LazyResolver.test.js +383 -0
- package/src/tests/StepValidator.test.js +2 -2
- package/src/tests/TestRunner.test.js +2 -2
|
@@ -126,9 +126,24 @@ describe('FillExecutor', () => {
|
|
|
126
126
|
it('should fill by ref', async () => {
|
|
127
127
|
mockSession.send = mock.fn(async (method, params) => {
|
|
128
128
|
if (method === 'Runtime.evaluate') {
|
|
129
|
+
// LazyResolver: queries __ariaRefMeta for metadata
|
|
130
|
+
if (params?.expression?.includes('__ariaRefMeta') && params?.expression?.includes('get')) {
|
|
131
|
+
return { result: { value: { selector: '#input', role: 'textbox', name: 'Username' } } };
|
|
132
|
+
}
|
|
133
|
+
// LazyResolver: element found
|
|
134
|
+
if (params?.expression?.includes('found') && params?.expression?.includes('box')) {
|
|
135
|
+
return { result: { value: { found: true, box: { x: 50, y: 50, width: 200, height: 30 } } } };
|
|
136
|
+
}
|
|
137
|
+
if (params?.expression?.includes('querySelector')) {
|
|
138
|
+
return { result: { objectId: 'obj-123' } };
|
|
139
|
+
}
|
|
129
140
|
return { result: { objectId: 'obj-123' } };
|
|
130
141
|
}
|
|
131
142
|
if (method === 'Runtime.callFunctionOn') {
|
|
143
|
+
// Visibility and box check after lazy resolution
|
|
144
|
+
if (params?.functionDeclaration?.includes('getComputedStyle') && params?.functionDeclaration?.includes('isVisible')) {
|
|
145
|
+
return { result: { value: { isVisible: true, box: { x: 50, y: 50, width: 200, height: 30 } } } };
|
|
146
|
+
}
|
|
132
147
|
if (params?.functionDeclaration?.includes('scrollIntoView')) {
|
|
133
148
|
return { result: {} };
|
|
134
149
|
}
|
|
@@ -143,18 +158,32 @@ describe('FillExecutor', () => {
|
|
|
143
158
|
return {};
|
|
144
159
|
});
|
|
145
160
|
|
|
146
|
-
const result = await executor.execute({ ref: '
|
|
161
|
+
const result = await executor.execute({ ref: 'f0s1e1', value: 'ref value' });
|
|
147
162
|
assert.strictEqual(result.filled, true);
|
|
148
|
-
assert.strictEqual(result.ref, '
|
|
163
|
+
assert.strictEqual(result.ref, 'f0s1e1');
|
|
149
164
|
assert.strictEqual(result.method, 'insertText');
|
|
150
165
|
});
|
|
151
166
|
|
|
152
167
|
it('should detect ref from selector pattern', async () => {
|
|
153
168
|
mockSession.send = mock.fn(async (method, params) => {
|
|
154
169
|
if (method === 'Runtime.evaluate') {
|
|
170
|
+
// LazyResolver: queries __ariaRefMeta for metadata
|
|
171
|
+
if (params?.expression?.includes('__ariaRefMeta') && params?.expression?.includes('get')) {
|
|
172
|
+
return { result: { value: { selector: '#input', role: 'textbox', name: 'Username' } } };
|
|
173
|
+
}
|
|
174
|
+
// LazyResolver: element found
|
|
175
|
+
if (params?.expression?.includes('found') && params?.expression?.includes('box')) {
|
|
176
|
+
return { result: { value: { found: true, box: { x: 50, y: 50, width: 200, height: 30 } } } };
|
|
177
|
+
}
|
|
178
|
+
if (params?.expression?.includes('querySelector')) {
|
|
179
|
+
return { result: { objectId: 'obj-123' } };
|
|
180
|
+
}
|
|
155
181
|
return { result: { objectId: 'obj-123' } };
|
|
156
182
|
}
|
|
157
183
|
if (method === 'Runtime.callFunctionOn') {
|
|
184
|
+
if (params?.functionDeclaration?.includes('getComputedStyle') && params?.functionDeclaration?.includes('isVisible')) {
|
|
185
|
+
return { result: { value: { isVisible: true, box: { x: 50, y: 50, width: 200, height: 30 } } } };
|
|
186
|
+
}
|
|
158
187
|
if (params?.functionDeclaration?.includes('scrollIntoView')) {
|
|
159
188
|
return { result: {} };
|
|
160
189
|
}
|
|
@@ -166,9 +195,9 @@ describe('FillExecutor', () => {
|
|
|
166
195
|
return {};
|
|
167
196
|
});
|
|
168
197
|
|
|
169
|
-
const result = await executor.execute({ selector: '
|
|
198
|
+
const result = await executor.execute({ selector: 'f0s1e5', value: 'test' });
|
|
170
199
|
assert.strictEqual(result.filled, true);
|
|
171
|
-
assert.strictEqual(result.ref, '
|
|
200
|
+
assert.strictEqual(result.ref, 'f0s1e5');
|
|
172
201
|
});
|
|
173
202
|
|
|
174
203
|
it('should fill by label', async () => {
|
|
@@ -441,9 +470,23 @@ describe('FillExecutor', () => {
|
|
|
441
470
|
it('should handle refs in batch', async () => {
|
|
442
471
|
mockSession.send = mock.fn(async (method, params) => {
|
|
443
472
|
if (method === 'Runtime.evaluate') {
|
|
473
|
+
// LazyResolver: queries __ariaRefMeta for metadata
|
|
474
|
+
if (params?.expression?.includes('__ariaRefMeta') && params?.expression?.includes('get')) {
|
|
475
|
+
return { result: { value: { selector: '#input', role: 'textbox', name: 'Input' } } };
|
|
476
|
+
}
|
|
477
|
+
// LazyResolver: element found
|
|
478
|
+
if (params?.expression?.includes('found') && params?.expression?.includes('box')) {
|
|
479
|
+
return { result: { value: { found: true, box: { x: 50, y: 50, width: 200, height: 30 } } } };
|
|
480
|
+
}
|
|
481
|
+
if (params?.expression?.includes('querySelector')) {
|
|
482
|
+
return { result: { objectId: 'obj-123' } };
|
|
483
|
+
}
|
|
444
484
|
return { result: { objectId: 'obj-123' } };
|
|
445
485
|
}
|
|
446
486
|
if (method === 'Runtime.callFunctionOn') {
|
|
487
|
+
if (params?.functionDeclaration?.includes('getComputedStyle') && params?.functionDeclaration?.includes('isVisible')) {
|
|
488
|
+
return { result: { value: { isVisible: true, box: { x: 50, y: 50, width: 200, height: 30 } } } };
|
|
489
|
+
}
|
|
447
490
|
if (params?.functionDeclaration?.includes('scrollIntoView')) {
|
|
448
491
|
return { result: {} };
|
|
449
492
|
}
|
|
@@ -456,8 +499,8 @@ describe('FillExecutor', () => {
|
|
|
456
499
|
});
|
|
457
500
|
|
|
458
501
|
const result = await executor.executeBatch({
|
|
459
|
-
'
|
|
460
|
-
'
|
|
502
|
+
'f0s1e1': 'value1',
|
|
503
|
+
'f0s1e2': 'value2'
|
|
461
504
|
});
|
|
462
505
|
|
|
463
506
|
assert.strictEqual(result.total, 2);
|
|
@@ -466,45 +509,54 @@ describe('FillExecutor', () => {
|
|
|
466
509
|
});
|
|
467
510
|
|
|
468
511
|
describe('ref-based fill edge cases', () => {
|
|
469
|
-
it('should
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
// May fail with "requires selector, ref, or label" because ref is only valid with ariaSnapshot
|
|
478
|
-
return err.message.includes('requires') || err.message.includes('ariaSnapshot');
|
|
512
|
+
it('should throw when ref element cannot be resolved (lazy resolution)', async () => {
|
|
513
|
+
// LazyResolver returns null when metadata not found
|
|
514
|
+
mockSession.send = mock.fn(async (method, params) => {
|
|
515
|
+
if (method === 'Runtime.evaluate') {
|
|
516
|
+
if (params?.expression?.includes('__ariaRefMeta')) {
|
|
517
|
+
return { result: { value: null } };
|
|
518
|
+
}
|
|
519
|
+
return { result: { value: null } };
|
|
479
520
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
it('should throw when ref element is stale', async () => {
|
|
484
|
-
mockAriaSnapshot.getElementByRef = mock.fn(async () => ({
|
|
485
|
-
box: { x: 50, y: 50, width: 200, height: 30 },
|
|
486
|
-
isVisible: true,
|
|
487
|
-
stale: true
|
|
488
|
-
}));
|
|
521
|
+
return {};
|
|
522
|
+
});
|
|
489
523
|
|
|
490
524
|
await assert.rejects(
|
|
491
|
-
() => executor.execute({ ref: '
|
|
525
|
+
() => executor.execute({ ref: 'f0s1e1', value: 'test' }),
|
|
492
526
|
(err) => {
|
|
493
|
-
|
|
494
|
-
return true;
|
|
527
|
+
return err.message.includes('not found');
|
|
495
528
|
}
|
|
496
529
|
);
|
|
497
530
|
});
|
|
498
531
|
|
|
499
532
|
it('should throw when ref element is not visible', async () => {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
533
|
+
mockSession.send = mock.fn(async (method, params) => {
|
|
534
|
+
if (method === 'Runtime.evaluate') {
|
|
535
|
+
// LazyResolver: metadata found
|
|
536
|
+
if (params?.expression?.includes('__ariaRefMeta') && params?.expression?.includes('get')) {
|
|
537
|
+
return { result: { value: { selector: '#input', role: 'textbox', name: 'Input' } } };
|
|
538
|
+
}
|
|
539
|
+
// LazyResolver: element found
|
|
540
|
+
if (params?.expression?.includes('found') && params?.expression?.includes('box')) {
|
|
541
|
+
return { result: { value: { found: true, box: { x: 50, y: 50, width: 200, height: 30 } } } };
|
|
542
|
+
}
|
|
543
|
+
if (params?.expression?.includes('querySelector')) {
|
|
544
|
+
return { result: { objectId: 'obj-123' } };
|
|
545
|
+
}
|
|
546
|
+
return { result: { objectId: 'obj-123' } };
|
|
547
|
+
}
|
|
548
|
+
if (method === 'Runtime.callFunctionOn') {
|
|
549
|
+
// Visibility check returns not visible
|
|
550
|
+
if (params?.functionDeclaration?.includes('getComputedStyle') && params?.functionDeclaration?.includes('isVisible')) {
|
|
551
|
+
return { result: { value: { isVisible: false, box: { x: 50, y: 50, width: 200, height: 30 } } } };
|
|
552
|
+
}
|
|
553
|
+
return { result: { value: { editable: true } } };
|
|
554
|
+
}
|
|
555
|
+
return {};
|
|
556
|
+
});
|
|
505
557
|
|
|
506
558
|
await assert.rejects(
|
|
507
|
-
() => executor.execute({ ref: '
|
|
559
|
+
() => executor.execute({ ref: 'f0s1e1', value: 'test' }),
|
|
508
560
|
(err) => {
|
|
509
561
|
assert.ok(err.message.includes('not visible'));
|
|
510
562
|
return true;
|
|
@@ -516,7 +568,7 @@ describe('FillExecutor', () => {
|
|
|
516
568
|
mockAriaSnapshot.getElementByRef = mock.fn(async () => null);
|
|
517
569
|
|
|
518
570
|
await assert.rejects(
|
|
519
|
-
() => executor.execute({ ref: '
|
|
571
|
+
() => executor.execute({ ref: 'f0s1e99', value: 'test' }),
|
|
520
572
|
(err) => {
|
|
521
573
|
assert.ok(err.message.includes('not found'));
|
|
522
574
|
return true;
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { describe, it, beforeEach, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { createLazyResolver } from '../dom/LazyResolver.js';
|
|
4
|
+
|
|
5
|
+
describe('LazyResolver', () => {
|
|
6
|
+
let mockSession;
|
|
7
|
+
let resolver;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockSession = {
|
|
11
|
+
send: mock.fn()
|
|
12
|
+
};
|
|
13
|
+
resolver = createLazyResolver(mockSession);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('createLazyResolver', () => {
|
|
17
|
+
it('should throw if session is not provided', () => {
|
|
18
|
+
assert.throws(() => createLazyResolver(null), /CDP session is required/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should create a resolver with all methods', () => {
|
|
22
|
+
assert.ok(typeof resolver.resolveRef === 'function');
|
|
23
|
+
assert.ok(typeof resolver.resolveSelector === 'function');
|
|
24
|
+
assert.ok(typeof resolver.resolveText === 'function');
|
|
25
|
+
assert.ok(typeof resolver.resolveByRoleAndName === 'function');
|
|
26
|
+
assert.ok(typeof resolver.resolveThroughShadowDOM === 'function');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('resolveSelector', () => {
|
|
31
|
+
it('should return null for empty selector', async () => {
|
|
32
|
+
const result = await resolver.resolveSelector('');
|
|
33
|
+
assert.strictEqual(result, null);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should return null for non-string selector', async () => {
|
|
37
|
+
const result = await resolver.resolveSelector(123);
|
|
38
|
+
assert.strictEqual(result, null);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return null when element not found', async () => {
|
|
42
|
+
mockSession.send.mock.mockImplementation((method) => {
|
|
43
|
+
if (method === 'Runtime.evaluate') {
|
|
44
|
+
return { result: { value: null } };
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const result = await resolver.resolveSelector('#nonexistent');
|
|
49
|
+
assert.strictEqual(result, null);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return objectId and box when element found', async () => {
|
|
53
|
+
let callCount = 0;
|
|
54
|
+
mockSession.send.mock.mockImplementation((method) => {
|
|
55
|
+
callCount++;
|
|
56
|
+
if (method === 'Runtime.evaluate') {
|
|
57
|
+
if (callCount === 1) {
|
|
58
|
+
// First call - check existence and get box
|
|
59
|
+
return {
|
|
60
|
+
result: {
|
|
61
|
+
value: { found: true, box: { x: 10, y: 20, width: 100, height: 50 } }
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
} else {
|
|
65
|
+
// Second call - get objectId
|
|
66
|
+
return {
|
|
67
|
+
result: { objectId: 'obj-123' }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const result = await resolver.resolveSelector('#myButton');
|
|
74
|
+
assert.ok(result);
|
|
75
|
+
assert.strictEqual(result.objectId, 'obj-123');
|
|
76
|
+
assert.deepStrictEqual(result.box, { x: 10, y: 20, width: 100, height: 50 });
|
|
77
|
+
assert.strictEqual(result.resolvedBy, 'selector');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle CDP errors gracefully', async () => {
|
|
81
|
+
mockSession.send.mock.mockImplementation(() => {
|
|
82
|
+
throw new Error('CDP connection lost');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const result = await resolver.resolveSelector('#myButton');
|
|
86
|
+
assert.strictEqual(result, null);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('resolveRef', () => {
|
|
91
|
+
it('should return null for empty ref', async () => {
|
|
92
|
+
const result = await resolver.resolveRef('');
|
|
93
|
+
assert.strictEqual(result, null);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return null for non-string ref', async () => {
|
|
97
|
+
const result = await resolver.resolveRef(123);
|
|
98
|
+
assert.strictEqual(result, null);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should return null when metadata not found', async () => {
|
|
102
|
+
mockSession.send.mock.mockImplementation(() => {
|
|
103
|
+
return { result: { value: null } };
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await resolver.resolveRef('f0s1e5');
|
|
107
|
+
assert.strictEqual(result, null);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should resolve by selector from metadata', async () => {
|
|
111
|
+
let callCount = 0;
|
|
112
|
+
mockSession.send.mock.mockImplementation((method, params) => {
|
|
113
|
+
callCount++;
|
|
114
|
+
if (method === 'Runtime.evaluate') {
|
|
115
|
+
if (callCount === 1) {
|
|
116
|
+
// First call - get metadata
|
|
117
|
+
return {
|
|
118
|
+
result: {
|
|
119
|
+
value: { selector: '#submitBtn', role: 'button', name: 'Submit' }
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
} else if (callCount === 2) {
|
|
123
|
+
// Second call - check element exists
|
|
124
|
+
return {
|
|
125
|
+
result: {
|
|
126
|
+
value: { found: true, box: { x: 100, y: 200, width: 80, height: 30 } }
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
} else {
|
|
130
|
+
// Third call - get objectId
|
|
131
|
+
return {
|
|
132
|
+
result: { objectId: 'resolved-obj-456' }
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const result = await resolver.resolveRef('f0s1e5');
|
|
139
|
+
assert.ok(result);
|
|
140
|
+
assert.strictEqual(result.objectId, 'resolved-obj-456');
|
|
141
|
+
assert.strictEqual(result.ref, 'f0s1e5');
|
|
142
|
+
assert.strictEqual(result.resolvedBy, 'selector');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should fall back to role+name search when selector fails', async () => {
|
|
146
|
+
let callCount = 0;
|
|
147
|
+
mockSession.send.mock.mockImplementation((method) => {
|
|
148
|
+
callCount++;
|
|
149
|
+
if (method === 'Runtime.evaluate') {
|
|
150
|
+
if (callCount === 1) {
|
|
151
|
+
// Get metadata
|
|
152
|
+
return {
|
|
153
|
+
result: {
|
|
154
|
+
value: { selector: '#oldId', role: 'button', name: 'Submit' }
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
} else if (callCount === 2) {
|
|
158
|
+
// Selector check - fails (element not found)
|
|
159
|
+
return { result: { value: null } };
|
|
160
|
+
} else if (callCount === 3) {
|
|
161
|
+
// Role+name search - succeeds
|
|
162
|
+
return {
|
|
163
|
+
result: {
|
|
164
|
+
value: { found: true, box: { x: 50, y: 60, width: 100, height: 40 }, index: 0 }
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
} else {
|
|
168
|
+
// Get objectId via role+name
|
|
169
|
+
return {
|
|
170
|
+
result: { objectId: 'fallback-obj-789' }
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const result = await resolver.resolveRef('f0s1e5');
|
|
177
|
+
assert.ok(result);
|
|
178
|
+
assert.strictEqual(result.objectId, 'fallback-obj-789');
|
|
179
|
+
assert.strictEqual(result.resolvedBy, 'role+name');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('resolveByRoleAndName', () => {
|
|
184
|
+
it('should return null for empty role', async () => {
|
|
185
|
+
const result = await resolver.resolveByRoleAndName('', 'test');
|
|
186
|
+
assert.strictEqual(result, null);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should find element by role and name', async () => {
|
|
190
|
+
let callCount = 0;
|
|
191
|
+
mockSession.send.mock.mockImplementation((method) => {
|
|
192
|
+
callCount++;
|
|
193
|
+
if (method === 'Runtime.evaluate') {
|
|
194
|
+
if (callCount === 1) {
|
|
195
|
+
return {
|
|
196
|
+
result: {
|
|
197
|
+
value: { found: true, box: { x: 10, y: 20, width: 100, height: 50 }, index: 2 }
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
} else {
|
|
201
|
+
return {
|
|
202
|
+
result: { objectId: 'role-obj-123' }
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const result = await resolver.resolveByRoleAndName('button', 'Save');
|
|
209
|
+
assert.ok(result);
|
|
210
|
+
assert.strictEqual(result.objectId, 'role-obj-123');
|
|
211
|
+
assert.strictEqual(result.resolvedBy, 'role+name');
|
|
212
|
+
assert.strictEqual(result.role, 'button');
|
|
213
|
+
assert.strictEqual(result.name, 'Save');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('resolveThroughShadowDOM', () => {
|
|
218
|
+
it('should return null for empty shadow host path', async () => {
|
|
219
|
+
const result = await resolver.resolveThroughShadowDOM([], '#button');
|
|
220
|
+
assert.strictEqual(result, null);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should resolve element through shadow DOM', async () => {
|
|
224
|
+
let callCount = 0;
|
|
225
|
+
mockSession.send.mock.mockImplementation((method) => {
|
|
226
|
+
callCount++;
|
|
227
|
+
if (method === 'Runtime.evaluate') {
|
|
228
|
+
if (callCount === 1) {
|
|
229
|
+
return {
|
|
230
|
+
result: {
|
|
231
|
+
value: { found: true, box: { x: 30, y: 40, width: 80, height: 30 } }
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
} else {
|
|
235
|
+
return {
|
|
236
|
+
result: { objectId: 'shadow-obj-999' }
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const result = await resolver.resolveThroughShadowDOM(['#host1', '#host2'], '.inner-button');
|
|
243
|
+
assert.ok(result);
|
|
244
|
+
assert.strictEqual(result.objectId, 'shadow-obj-999');
|
|
245
|
+
assert.strictEqual(result.resolvedBy, 'shadow-dom');
|
|
246
|
+
assert.deepStrictEqual(result.shadowHostPath, ['#host1', '#host2']);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('resolveText', () => {
|
|
251
|
+
it('should return null for empty text', async () => {
|
|
252
|
+
const result = await resolver.resolveText('');
|
|
253
|
+
assert.strictEqual(result, null);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should return null for non-string text', async () => {
|
|
257
|
+
const result = await resolver.resolveText(123);
|
|
258
|
+
assert.strictEqual(result, null);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should find element by text', async () => {
|
|
262
|
+
let callCount = 0;
|
|
263
|
+
mockSession.send.mock.mockImplementation((method) => {
|
|
264
|
+
callCount++;
|
|
265
|
+
if (method === 'Runtime.evaluate') {
|
|
266
|
+
if (callCount === 1) {
|
|
267
|
+
return {
|
|
268
|
+
result: {
|
|
269
|
+
value: {
|
|
270
|
+
found: true,
|
|
271
|
+
box: { x: 100, y: 150, width: 120, height: 35 },
|
|
272
|
+
selectors: 'button, input[type="button"]'
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
} else {
|
|
277
|
+
return {
|
|
278
|
+
result: { objectId: 'text-obj-555' }
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const result = await resolver.resolveText('Submit Form');
|
|
285
|
+
assert.ok(result);
|
|
286
|
+
assert.strictEqual(result.objectId, 'text-obj-555');
|
|
287
|
+
assert.strictEqual(result.resolvedBy, 'text');
|
|
288
|
+
assert.strictEqual(result.text, 'Submit Form');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('frame context support', () => {
|
|
293
|
+
it('should include contextId in eval params when getFrameContext returns a value', async () => {
|
|
294
|
+
const getFrameContext = () => 12345;
|
|
295
|
+
const resolverWithFrame = createLazyResolver(mockSession, { getFrameContext });
|
|
296
|
+
|
|
297
|
+
mockSession.send.mock.mockImplementation((method, params) => {
|
|
298
|
+
if (method === 'Runtime.evaluate') {
|
|
299
|
+
// Verify contextId is included
|
|
300
|
+
assert.strictEqual(params.contextId, 12345);
|
|
301
|
+
return { result: { value: null } };
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await resolverWithFrame.resolveSelector('#test');
|
|
306
|
+
assert.ok(mockSession.send.mock.calls.length > 0);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should not include contextId when getFrameContext returns null', async () => {
|
|
310
|
+
const getFrameContext = () => null;
|
|
311
|
+
const resolverWithFrame = createLazyResolver(mockSession, { getFrameContext });
|
|
312
|
+
|
|
313
|
+
mockSession.send.mock.mockImplementation((method, params) => {
|
|
314
|
+
if (method === 'Runtime.evaluate') {
|
|
315
|
+
// Verify contextId is NOT included
|
|
316
|
+
assert.strictEqual(params.contextId, undefined);
|
|
317
|
+
return { result: { value: null } };
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await resolverWithFrame.resolveSelector('#test');
|
|
322
|
+
assert.ok(mockSession.send.mock.calls.length > 0);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('edge cases', () => {
|
|
327
|
+
it('should handle selector returning subtype null', async () => {
|
|
328
|
+
mockSession.send.mock.mockImplementation((method) => {
|
|
329
|
+
if (method === 'Runtime.evaluate') {
|
|
330
|
+
return {
|
|
331
|
+
result: { value: { found: true, box: { x: 0, y: 0, width: 0, height: 0 } } }
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Mock second call to return null subtype
|
|
337
|
+
let firstCall = true;
|
|
338
|
+
mockSession.send.mock.mockImplementation(() => {
|
|
339
|
+
if (firstCall) {
|
|
340
|
+
firstCall = false;
|
|
341
|
+
return {
|
|
342
|
+
result: { value: { found: true, box: { x: 0, y: 0, width: 10, height: 10 } } }
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
return { result: { subtype: 'null' } };
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const result = await resolver.resolveSelector('#test');
|
|
349
|
+
assert.strictEqual(result, null);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should handle metadata with shadowHostPath', async () => {
|
|
353
|
+
let callCount = 0;
|
|
354
|
+
mockSession.send.mock.mockImplementation(() => {
|
|
355
|
+
callCount++;
|
|
356
|
+
if (callCount === 1) {
|
|
357
|
+
// Metadata with shadow path
|
|
358
|
+
return {
|
|
359
|
+
result: {
|
|
360
|
+
value: {
|
|
361
|
+
selector: '.shadow-button',
|
|
362
|
+
role: 'button',
|
|
363
|
+
name: 'Click',
|
|
364
|
+
shadowHostPath: ['#shadow-host']
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
} else if (callCount === 2) {
|
|
369
|
+
// Shadow DOM resolution succeeds
|
|
370
|
+
return {
|
|
371
|
+
result: { value: { found: true, box: { x: 10, y: 20, width: 50, height: 30 } } }
|
|
372
|
+
};
|
|
373
|
+
} else {
|
|
374
|
+
return { result: { objectId: 'shadow-resolved-obj' } };
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const result = await resolver.resolveRef('f0s2e3');
|
|
379
|
+
assert.ok(result);
|
|
380
|
+
assert.strictEqual(result.objectId, 'shadow-resolved-obj');
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
});
|
|
@@ -170,7 +170,7 @@ describe('StepValidator', () => {
|
|
|
170
170
|
});
|
|
171
171
|
|
|
172
172
|
it('should accept object with ref', () => {
|
|
173
|
-
const errors = validateStepInternal({ click: { ref: '
|
|
173
|
+
const errors = validateStepInternal({ click: { ref: 'f0s1e1' } });
|
|
174
174
|
assert.strictEqual(errors.length, 0);
|
|
175
175
|
});
|
|
176
176
|
|
|
@@ -224,7 +224,7 @@ describe('StepValidator', () => {
|
|
|
224
224
|
});
|
|
225
225
|
|
|
226
226
|
it('should accept object with ref and value', () => {
|
|
227
|
-
const errors = validateStepInternal({ fill: { ref: '
|
|
227
|
+
const errors = validateStepInternal({ fill: { ref: 'f0s1e1', value: 'test' } });
|
|
228
228
|
assert.strictEqual(errors.length, 0);
|
|
229
229
|
});
|
|
230
230
|
|
|
@@ -603,7 +603,7 @@ describe('TestRunner', () => {
|
|
|
603
603
|
});
|
|
604
604
|
|
|
605
605
|
it('should accept fill with ref instead of selector', () => {
|
|
606
|
-
const steps = [{ fill: { ref: '
|
|
606
|
+
const steps = [{ fill: { ref: 'f0s1e3', value: 'test' } }];
|
|
607
607
|
|
|
608
608
|
const result = testRunner.validateSteps(steps);
|
|
609
609
|
assert.strictEqual(result.valid, true);
|
|
@@ -716,7 +716,7 @@ describe('TestRunner', () => {
|
|
|
716
716
|
});
|
|
717
717
|
|
|
718
718
|
it('should accept valid hover with ref', () => {
|
|
719
|
-
const result = validateSteps([{ hover: { ref: '
|
|
719
|
+
const result = validateSteps([{ hover: { ref: 'f0s1e4' } }]);
|
|
720
720
|
assert.strictEqual(result.valid, true);
|
|
721
721
|
});
|
|
722
722
|
|