chrometools-mcp 1.8.2 → 2.2.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.
- package/CHANGELOG.md +757 -494
- package/README.md +219 -41
- package/browser/browser-manager.js +206 -0
- package/browser/page-manager.js +298 -0
- package/index.js +525 -1892
- package/package.json +55 -55
- package/recorder/page-object-generator.js +720 -0
- package/recorder/recorder-script.js +118 -12
- package/recorder/scenario-executor.js +970 -946
- package/recorder/scenario-storage.js +253 -29
- package/server/tool-definitions.js +620 -0
- package/server/tool-schemas.js +295 -0
- package/utils/code-generators/code-generator-base.js +61 -0
- package/utils/code-generators/file-appender.js +202 -0
- package/utils/code-generators/playwright-python.js +84 -0
- package/utils/code-generators/playwright-typescript.js +95 -0
- package/utils/code-generators/selenium-java.js +123 -0
- package/utils/code-generators/selenium-python.js +82 -0
- package/utils/css-utils.js +151 -0
- package/utils/image-processing.js +236 -0
- package/utils/platform-utils.js +62 -0
- package/utils/url-to-project.js +141 -0
- package/index.js.backup +0 -3674
- package/utils/project-detector.js +0 -87
|
@@ -1,946 +1,970 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* recorder/scenario-executor.js
|
|
3
|
-
*
|
|
4
|
-
* Executes recorded scenarios with:
|
|
5
|
-
* 1. Action playback with error handling
|
|
6
|
-
* 2. Parameter substitution
|
|
7
|
-
* 3. Secret injection
|
|
8
|
-
* 4. Dependency resolution and chaining
|
|
9
|
-
* 5. Retry logic with fallback selectors
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { resolveDependencies, checkDependencyCondition } from './dependency-resolver.js';
|
|
13
|
-
import { loadScenario, loadSecrets, loadIndex } from './scenario-storage.js';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*
|
|
21
|
-
* @
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
result.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
parts.push(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
parts.push(
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
case '
|
|
498
|
-
await
|
|
499
|
-
break;
|
|
500
|
-
|
|
501
|
-
case '
|
|
502
|
-
await
|
|
503
|
-
break;
|
|
504
|
-
|
|
505
|
-
case '
|
|
506
|
-
await
|
|
507
|
-
break;
|
|
508
|
-
|
|
509
|
-
case '
|
|
510
|
-
await
|
|
511
|
-
break;
|
|
512
|
-
|
|
513
|
-
case '
|
|
514
|
-
|
|
515
|
-
break;
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
async function
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
//
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
await page.
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
await
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
async function
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1
|
+
/**
|
|
2
|
+
* recorder/scenario-executor.js
|
|
3
|
+
*
|
|
4
|
+
* Executes recorded scenarios with:
|
|
5
|
+
* 1. Action playback with error handling
|
|
6
|
+
* 2. Parameter substitution
|
|
7
|
+
* 3. Secret injection
|
|
8
|
+
* 4. Dependency resolution and chaining
|
|
9
|
+
* 5. Retry logic with fallback selectors
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { resolveDependencies, checkDependencyCondition } from './dependency-resolver.js';
|
|
13
|
+
import { loadScenario, loadSecrets, loadIndex } from './scenario-storage.js';
|
|
14
|
+
|
|
15
|
+
// Debug mode - avoid polluting STDIO with logs (breaks MCP JSON-RPC)
|
|
16
|
+
const DEBUG_MODE = process.env.CHROMETOOLS_DEBUG === 'true';
|
|
17
|
+
const debugLog = DEBUG_MODE ? console.error : () => {};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Execute scenario with dependencies
|
|
21
|
+
* @param {string} scenarioName - Scenario to execute
|
|
22
|
+
* @param {Object} page - Puppeteer page instance
|
|
23
|
+
* @param {Object} params - Parameters for scenario
|
|
24
|
+
* @param {Object} options - Execution options { executeDependencies, skipConditions, maxRetries, timeout }
|
|
25
|
+
* @returns {Object} - Execution result
|
|
26
|
+
*/
|
|
27
|
+
export async function executeScenario(scenarioName, page, params = {}, options = {}) {
|
|
28
|
+
const {
|
|
29
|
+
executeDependencies = true, // Execute dependencies by default
|
|
30
|
+
skipConditions = false,
|
|
31
|
+
maxRetries = 3,
|
|
32
|
+
timeout = 30000,
|
|
33
|
+
projectId = null // Optional projectId for disambiguation
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
const result = {
|
|
37
|
+
success: false,
|
|
38
|
+
scenarioName,
|
|
39
|
+
executedScenarios: [],
|
|
40
|
+
errors: [],
|
|
41
|
+
outputs: {},
|
|
42
|
+
duration: 0
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const startTime = Date.now();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Load scenario to get its metadata (needed for dependency resolution)
|
|
49
|
+
const initialScenario = await loadScenario(scenarioName, false, projectId);
|
|
50
|
+
|
|
51
|
+
if (!initialScenario) {
|
|
52
|
+
result.errors.push(`Scenario "${scenarioName}" not found`);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check for name collision
|
|
57
|
+
if (initialScenario.collision) {
|
|
58
|
+
result.errors.push(initialScenario.message);
|
|
59
|
+
result.availableProjectIds = initialScenario.availableProjectIds;
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let chain = [{ name: scenarioName, projectId }]; // Default: execute only the requested scenario
|
|
64
|
+
|
|
65
|
+
// Resolve and execute dependencies if enabled
|
|
66
|
+
if (executeDependencies && initialScenario.metadata?.dependencies) {
|
|
67
|
+
// Build a simplified index from metadata for dependency resolution
|
|
68
|
+
// In the new system, we need to resolve cross-project dependencies
|
|
69
|
+
// Dependencies inherit parent's projectId unless they specify their own
|
|
70
|
+
chain = [
|
|
71
|
+
...initialScenario.metadata.dependencies.map(dep => ({
|
|
72
|
+
name: dep.scenario,
|
|
73
|
+
projectId: dep.projectId || projectId // Use explicit projectId or inherit from parent
|
|
74
|
+
})),
|
|
75
|
+
{ name: scenarioName, projectId }
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Execute chain in order
|
|
80
|
+
for (const item of chain) {
|
|
81
|
+
const scenario = await loadScenario(item.name, true, item.projectId); // Load with secrets, using item's projectId
|
|
82
|
+
|
|
83
|
+
if (!scenario) {
|
|
84
|
+
result.errors.push(`Scenario "${item.name}" not found`);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for name collision in dependencies
|
|
89
|
+
if (scenario.collision) {
|
|
90
|
+
result.errors.push(`Dependency "${item.name}": ${scenario.message}`);
|
|
91
|
+
result.availableProjectIds = scenario.availableProjectIds;
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check dependency conditions
|
|
96
|
+
if (scenario.metadata?.dependencies) {
|
|
97
|
+
for (const dep of scenario.metadata.dependencies) {
|
|
98
|
+
if (dep.condition) {
|
|
99
|
+
const context = { page, variables: params };
|
|
100
|
+
const shouldExecute = await checkDependencyCondition(dep.condition, context);
|
|
101
|
+
|
|
102
|
+
if (!shouldExecute) {
|
|
103
|
+
debugLog(`Skipping scenario "${item.name}" due to condition`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Merge secrets with params
|
|
111
|
+
const executionParams = { ...params, ...(scenario.secrets || {}) };
|
|
112
|
+
|
|
113
|
+
// Execute scenario
|
|
114
|
+
const scenarioResult = await executeSingleScenario(scenario, page, executionParams, {
|
|
115
|
+
maxRetries,
|
|
116
|
+
timeout
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
result.executedScenarios.push(item.name);
|
|
120
|
+
|
|
121
|
+
if (!scenarioResult.success) {
|
|
122
|
+
result.errors.push(...scenarioResult.errors);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Collect outputs for next scenarios
|
|
127
|
+
if (scenarioResult.outputs) {
|
|
128
|
+
Object.assign(result.outputs, scenarioResult.outputs);
|
|
129
|
+
Object.assign(params, scenarioResult.outputs);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
result.success = true;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
result.errors.push(`Execution failed: ${error.message}`);
|
|
136
|
+
} finally {
|
|
137
|
+
result.duration = Date.now() - startTime;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Execute single scenario (without dependencies)
|
|
145
|
+
* @param {Object} scenario - Scenario data
|
|
146
|
+
* @param {Object} page - Puppeteer page
|
|
147
|
+
* @param {Object} params - Parameters
|
|
148
|
+
* @param {Object} options - Options
|
|
149
|
+
* @returns {Object} - Execution result
|
|
150
|
+
*/
|
|
151
|
+
async function executeSingleScenario(scenario, page, params = {}, options = {}) {
|
|
152
|
+
const { maxRetries = 3, timeout = 30000 } = options;
|
|
153
|
+
|
|
154
|
+
const result = {
|
|
155
|
+
success: false,
|
|
156
|
+
errors: [],
|
|
157
|
+
outputs: {},
|
|
158
|
+
actionResults: []
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
for (const action of scenario.chain) {
|
|
163
|
+
// Substitute parameters in action
|
|
164
|
+
const resolvedAction = substituteParameters(action, params);
|
|
165
|
+
|
|
166
|
+
// Execute action with retry
|
|
167
|
+
const actionResult = await executeActionWithRetry(
|
|
168
|
+
resolvedAction,
|
|
169
|
+
page,
|
|
170
|
+
maxRetries,
|
|
171
|
+
timeout
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
result.actionResults.push(actionResult);
|
|
175
|
+
|
|
176
|
+
if (!actionResult.success) {
|
|
177
|
+
result.errors.push(`Action failed: ${actionResult.error}`);
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Store outputs if action produces any
|
|
182
|
+
if (actionResult.output) {
|
|
183
|
+
Object.assign(result.outputs, actionResult.output);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Validate final URL if exitUrl is specified in metadata
|
|
188
|
+
if (scenario.metadata?.exitUrl) {
|
|
189
|
+
// Wait a bit for any pending navigation/redirects to complete
|
|
190
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
191
|
+
|
|
192
|
+
// Get current URL from the page (more reliable than page.url() for recent navigation)
|
|
193
|
+
const currentUrl = await page.evaluate(() => window.location.href);
|
|
194
|
+
const expectedUrl = scenario.metadata.exitUrl;
|
|
195
|
+
|
|
196
|
+
// Normalize URLs for comparison (remove trailing slashes, fragments)
|
|
197
|
+
const normalizeUrl = (url) => {
|
|
198
|
+
try {
|
|
199
|
+
const parsed = new URL(url);
|
|
200
|
+
// Remove fragment and trailing slash
|
|
201
|
+
return `${parsed.origin}${parsed.pathname.replace(/\/$/, '')}${parsed.search}`;
|
|
202
|
+
} catch {
|
|
203
|
+
return url.replace(/\/$/, '');
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const normalizedCurrent = normalizeUrl(currentUrl);
|
|
208
|
+
const normalizedExpected = normalizeUrl(expectedUrl);
|
|
209
|
+
|
|
210
|
+
if (normalizedCurrent !== normalizedExpected) {
|
|
211
|
+
result.errors.push(
|
|
212
|
+
`❌ URL Validation Failed\n\n` +
|
|
213
|
+
`Expected final URL: ${expectedUrl}\n` +
|
|
214
|
+
`Actual final URL: ${currentUrl}\n\n` +
|
|
215
|
+
`The scenario ended on a different page than expected.\n` +
|
|
216
|
+
`This may indicate:\n` +
|
|
217
|
+
` - Navigation flow has changed\n` +
|
|
218
|
+
` - An action failed silently\n` +
|
|
219
|
+
` - Page redirected unexpectedly\n\n` +
|
|
220
|
+
`💡 Suggestion: Check if the page flow or redirects have changed since recording.`
|
|
221
|
+
);
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// URL validation passed
|
|
226
|
+
result.urlValidation = {
|
|
227
|
+
success: true,
|
|
228
|
+
expectedUrl,
|
|
229
|
+
actualUrl: currentUrl
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
result.success = true;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
result.errors.push(`Scenario execution error: ${error.message}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Execute action with retry and fallback selectors
|
|
243
|
+
*/
|
|
244
|
+
async function executeActionWithRetry(action, page, maxRetries, timeout) {
|
|
245
|
+
const result = {
|
|
246
|
+
success: false,
|
|
247
|
+
action: action.type,
|
|
248
|
+
error: null,
|
|
249
|
+
errorDetails: {
|
|
250
|
+
attempts: [],
|
|
251
|
+
selector: action.selector?.value || action.selector?.primary,
|
|
252
|
+
context: null
|
|
253
|
+
},
|
|
254
|
+
output: null,
|
|
255
|
+
attempts: 0
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
259
|
+
result.attempts = attempt;
|
|
260
|
+
const attemptInfo = {
|
|
261
|
+
number: attempt,
|
|
262
|
+
selector: action.selector?.value || action.selector?.primary,
|
|
263
|
+
error: null,
|
|
264
|
+
timestamp: new Date().toISOString()
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
// Execute action based on type
|
|
269
|
+
const actionResult = await executeAction(action, page, timeout);
|
|
270
|
+
|
|
271
|
+
result.success = true;
|
|
272
|
+
result.output = actionResult.output;
|
|
273
|
+
attemptInfo.success = true;
|
|
274
|
+
result.errorDetails.attempts.push(attemptInfo);
|
|
275
|
+
return result;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
attemptInfo.error = error.message;
|
|
278
|
+
attemptInfo.success = false;
|
|
279
|
+
|
|
280
|
+
// Capture page context for error reporting
|
|
281
|
+
if (attempt === maxRetries) {
|
|
282
|
+
try {
|
|
283
|
+
result.errorDetails.context = await capturePageContext(page, action);
|
|
284
|
+
} catch (contextError) {
|
|
285
|
+
debugLog('Failed to capture page context:', contextError);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
result.errorDetails.attempts.push(attemptInfo);
|
|
290
|
+
result.error = error.message;
|
|
291
|
+
|
|
292
|
+
// If this is a selector error and we have fallbacks, try them
|
|
293
|
+
if (action.selector?.fallbacks && action.selector.fallbacks.length > 0) {
|
|
294
|
+
const fallback = action.selector.fallbacks[0];
|
|
295
|
+
debugLog(`[Retry ${attempt}] Trying fallback selector: ${fallback}`);
|
|
296
|
+
|
|
297
|
+
action.selector.value = fallback;
|
|
298
|
+
action.selector.fallbacks = action.selector.fallbacks.slice(1);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// If we have element description, try smartFindElement
|
|
303
|
+
if (action.selector?.elementInfo?.text && attempt < maxRetries) {
|
|
304
|
+
debugLog(`[Retry ${attempt}] Selector failed, trying smartFindElement with description: ${action.selector.elementInfo.text}`);
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
// Inject element finder utilities if not already done
|
|
308
|
+
await page.evaluate(elementFinderUtilsCode);
|
|
309
|
+
|
|
310
|
+
const smartResult = await page.evaluate((description) => {
|
|
311
|
+
return window.smartFindElement({ description, maxResults: 3 });
|
|
312
|
+
}, action.selector.elementInfo.text);
|
|
313
|
+
|
|
314
|
+
if (smartResult.candidates && smartResult.candidates.length > 0) {
|
|
315
|
+
action.selector.value = smartResult.candidates[0].selector;
|
|
316
|
+
action.selector.fallbacks = smartResult.candidates.slice(1).map(c => c.selector);
|
|
317
|
+
debugLog(`[Retry ${attempt}] Found alternative selector: ${action.selector.value}`);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
} catch (smartError) {
|
|
321
|
+
debugLog('[Retry] smartFindElement failed:', smartError.message);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Last attempt failed
|
|
326
|
+
if (attempt === maxRetries) {
|
|
327
|
+
// Create comprehensive error message
|
|
328
|
+
result.error = formatDetailedError(action, result.errorDetails);
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Wait before retry
|
|
333
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Capture page context for error reporting
|
|
342
|
+
*/
|
|
343
|
+
async function capturePageContext(page, action) {
|
|
344
|
+
try {
|
|
345
|
+
const context = {
|
|
346
|
+
url: page.url(),
|
|
347
|
+
title: await page.title(),
|
|
348
|
+
elementExists: false,
|
|
349
|
+
elementVisible: false,
|
|
350
|
+
elementInfo: null,
|
|
351
|
+
pageState: null
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const selector = action.selector?.value || action.selector?.primary;
|
|
355
|
+
|
|
356
|
+
if (selector) {
|
|
357
|
+
// Check if element exists
|
|
358
|
+
context.elementExists = await page.evaluate((sel) => {
|
|
359
|
+
return document.querySelector(sel) !== null;
|
|
360
|
+
}, selector);
|
|
361
|
+
|
|
362
|
+
// If exists, check visibility and get info
|
|
363
|
+
if (context.elementExists) {
|
|
364
|
+
context.elementInfo = await page.evaluate((sel) => {
|
|
365
|
+
const el = document.querySelector(sel);
|
|
366
|
+
const rect = el.getBoundingClientRect();
|
|
367
|
+
const styles = window.getComputedStyle(el);
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
tagName: el.tagName,
|
|
371
|
+
id: el.id,
|
|
372
|
+
className: el.className,
|
|
373
|
+
visible: rect.width > 0 && rect.height > 0 && styles.display !== 'none' && styles.visibility !== 'hidden',
|
|
374
|
+
disabled: el.disabled || el.getAttribute('aria-disabled') === 'true',
|
|
375
|
+
readonly: el.readOnly || el.getAttribute('aria-readonly') === 'true',
|
|
376
|
+
position: {
|
|
377
|
+
top: rect.top,
|
|
378
|
+
left: rect.left,
|
|
379
|
+
width: rect.width,
|
|
380
|
+
height: rect.height
|
|
381
|
+
},
|
|
382
|
+
styles: {
|
|
383
|
+
display: styles.display,
|
|
384
|
+
visibility: styles.visibility,
|
|
385
|
+
opacity: styles.opacity,
|
|
386
|
+
pointerEvents: styles.pointerEvents
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}, selector);
|
|
390
|
+
|
|
391
|
+
context.elementVisible = context.elementInfo.visible;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Get page state
|
|
396
|
+
context.pageState = await page.evaluate(() => {
|
|
397
|
+
return {
|
|
398
|
+
readyState: document.readyState,
|
|
399
|
+
hasModals: document.querySelector('[role="dialog"], .modal, .popup') !== null,
|
|
400
|
+
hasOverlays: document.querySelector('.overlay, .backdrop') !== null,
|
|
401
|
+
activeElement: document.activeElement ? {
|
|
402
|
+
tagName: document.activeElement.tagName,
|
|
403
|
+
id: document.activeElement.id,
|
|
404
|
+
className: document.activeElement.className
|
|
405
|
+
} : null
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return context;
|
|
410
|
+
} catch (error) {
|
|
411
|
+
return { error: error.message };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Format detailed error message for AI agent
|
|
417
|
+
*/
|
|
418
|
+
function formatDetailedError(action, errorDetails) {
|
|
419
|
+
const parts = [
|
|
420
|
+
`❌ Action "${action.type}" failed after ${errorDetails.attempts.length} attempts`,
|
|
421
|
+
``,
|
|
422
|
+
`📍 Selector: ${errorDetails.selector}`,
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
if (errorDetails.context) {
|
|
426
|
+
parts.push(``, `📄 Page Context:`);
|
|
427
|
+
parts.push(` URL: ${errorDetails.context.url}`);
|
|
428
|
+
parts.push(` Title: ${errorDetails.context.title}`);
|
|
429
|
+
|
|
430
|
+
if (errorDetails.context.elementExists) {
|
|
431
|
+
parts.push(``, `🔍 Element Found But:`);
|
|
432
|
+
const info = errorDetails.context.elementInfo;
|
|
433
|
+
|
|
434
|
+
if (!info.visible) {
|
|
435
|
+
parts.push(` ⚠️ Element is NOT VISIBLE`);
|
|
436
|
+
parts.push(` - Display: ${info.styles.display}`);
|
|
437
|
+
parts.push(` - Visibility: ${info.styles.visibility}`);
|
|
438
|
+
parts.push(` - Opacity: ${info.styles.opacity}`);
|
|
439
|
+
parts.push(` - Size: ${info.position.width}x${info.position.height}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (info.disabled) {
|
|
443
|
+
parts.push(` ⚠️ Element is DISABLED`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (info.readonly && action.type === 'type') {
|
|
447
|
+
parts.push(` ⚠️ Element is READONLY`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (info.styles.pointerEvents === 'none') {
|
|
451
|
+
parts.push(` ⚠️ Element has pointer-events: none`);
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
parts.push(``, `❌ Element NOT FOUND in DOM`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (errorDetails.context.pageState) {
|
|
458
|
+
const state = errorDetails.context.pageState;
|
|
459
|
+
if (state.hasModals) {
|
|
460
|
+
parts.push(``, `⚠️ Page has open modal/dialog`);
|
|
461
|
+
}
|
|
462
|
+
if (state.hasOverlays) {
|
|
463
|
+
parts.push(`⚠️ Page has overlay/backdrop`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
parts.push(``, `🔄 Retry History:`);
|
|
469
|
+
errorDetails.attempts.forEach(attempt => {
|
|
470
|
+
parts.push(` Attempt ${attempt.number}: ${attempt.error || 'Unknown error'}`);
|
|
471
|
+
if (attempt.selector !== errorDetails.selector) {
|
|
472
|
+
parts.push(` (tried selector: ${attempt.selector})`);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
parts.push(``, `💡 Suggestions:`);
|
|
477
|
+
if (!errorDetails.context?.elementExists) {
|
|
478
|
+
parts.push(` - Check if page has fully loaded`);
|
|
479
|
+
parts.push(` - Verify the selector is correct`);
|
|
480
|
+
parts.push(` - Element might be dynamically added - add wait condition`);
|
|
481
|
+
} else if (!errorDetails.context?.elementVisible) {
|
|
482
|
+
parts.push(` - Element exists but is hidden - check CSS/JS conditions`);
|
|
483
|
+
parts.push(` - Wait for element to become visible`);
|
|
484
|
+
parts.push(` - Check if element is covered by modal/overlay`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return parts.join('\n');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Execute single action
|
|
492
|
+
*/
|
|
493
|
+
async function executeAction(action, page, timeout) {
|
|
494
|
+
const result = { output: null };
|
|
495
|
+
|
|
496
|
+
switch (action.type) {
|
|
497
|
+
case 'click':
|
|
498
|
+
await executeClick(action, page, timeout);
|
|
499
|
+
break;
|
|
500
|
+
|
|
501
|
+
case 'type':
|
|
502
|
+
await executeType(action, page, timeout);
|
|
503
|
+
break;
|
|
504
|
+
|
|
505
|
+
case 'select':
|
|
506
|
+
await executeSelect(action, page, timeout);
|
|
507
|
+
break;
|
|
508
|
+
|
|
509
|
+
case 'scroll':
|
|
510
|
+
await executeScroll(action, page);
|
|
511
|
+
break;
|
|
512
|
+
|
|
513
|
+
case 'hover':
|
|
514
|
+
await executeHover(action, page);
|
|
515
|
+
break;
|
|
516
|
+
|
|
517
|
+
case 'keypress':
|
|
518
|
+
await executeKeypress(action, page);
|
|
519
|
+
break;
|
|
520
|
+
|
|
521
|
+
case 'wait':
|
|
522
|
+
await executeWait(action, page);
|
|
523
|
+
break;
|
|
524
|
+
|
|
525
|
+
case 'upload':
|
|
526
|
+
await executeUpload(action, page, timeout);
|
|
527
|
+
break;
|
|
528
|
+
|
|
529
|
+
case 'drag':
|
|
530
|
+
await executeDrag(action, page);
|
|
531
|
+
break;
|
|
532
|
+
|
|
533
|
+
case 'navigate':
|
|
534
|
+
await executeNavigate(action, page, timeout);
|
|
535
|
+
break;
|
|
536
|
+
|
|
537
|
+
case 'extract':
|
|
538
|
+
result.output = await executeExtract(action, page);
|
|
539
|
+
break;
|
|
540
|
+
|
|
541
|
+
default:
|
|
542
|
+
throw new Error(`Unknown action type: ${action.type}`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return result;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Action executors
|
|
550
|
+
*/
|
|
551
|
+
|
|
552
|
+
async function executeClick(action, page, timeout) {
|
|
553
|
+
const selector = action.selector.value || action.selector.primary || action.selector;
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
await page.waitForSelector(selector, { timeout, visible: true });
|
|
557
|
+
await page.click(selector);
|
|
558
|
+
|
|
559
|
+
// Smart waiting after click
|
|
560
|
+
if (action.data.requiresWait !== false) {
|
|
561
|
+
await smartWaitAfterClick(page, action, timeout);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Additional wait if specified
|
|
565
|
+
if (action.data.waitAfter) {
|
|
566
|
+
await new Promise(resolve => setTimeout(resolve, action.data.waitAfter));
|
|
567
|
+
}
|
|
568
|
+
} catch (error) {
|
|
569
|
+
throw new Error(`Failed to click "${selector}": ${error.message}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Smart waiting after click - waits for animations and network requests
|
|
575
|
+
*/
|
|
576
|
+
async function smartWaitAfterClick(page, action, timeout) {
|
|
577
|
+
const startTime = Date.now();
|
|
578
|
+
const maxWaitTime = timeout || 30000;
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
// Initial wait 500ms to let page respond
|
|
582
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
583
|
+
|
|
584
|
+
// Check if there's any activity (animations, network, DOM changes)
|
|
585
|
+
const hasActivity = await checkPageActivity(page);
|
|
586
|
+
|
|
587
|
+
if (!hasActivity) {
|
|
588
|
+
// No activity detected - we're done, fast exit
|
|
589
|
+
debugLog('[Smart Wait] No activity detected, skipping extended wait');
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Activity detected - wait minimum 2 seconds
|
|
594
|
+
debugLog('[Smart Wait] Activity detected, waiting for completion');
|
|
595
|
+
const remainingMinWait = 2000 - 500; // Already waited 500ms
|
|
596
|
+
if (remainingMinWait > 0) {
|
|
597
|
+
await new Promise(resolve => setTimeout(resolve, remainingMinWait));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Wait for animations to complete
|
|
601
|
+
await page.evaluate(() => {
|
|
602
|
+
return new Promise((resolve) => {
|
|
603
|
+
const checkAnimations = () => {
|
|
604
|
+
// Check for CSS animations/transitions
|
|
605
|
+
const elements = document.querySelectorAll('*');
|
|
606
|
+
let hasAnimations = false;
|
|
607
|
+
|
|
608
|
+
for (const el of elements) {
|
|
609
|
+
const computedStyle = window.getComputedStyle(el);
|
|
610
|
+
const animations = computedStyle.getPropertyValue('animation-name');
|
|
611
|
+
const transitions = computedStyle.getPropertyValue('transition-property');
|
|
612
|
+
|
|
613
|
+
if ((animations && animations !== 'none') ||
|
|
614
|
+
(transitions && transitions !== 'none' && transitions !== 'all')) {
|
|
615
|
+
hasAnimations = true;
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (!hasAnimations) {
|
|
621
|
+
resolve();
|
|
622
|
+
} else {
|
|
623
|
+
setTimeout(checkAnimations, 100);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// Start checking immediately
|
|
628
|
+
checkAnimations();
|
|
629
|
+
// Timeout after 3 seconds
|
|
630
|
+
setTimeout(resolve, 3000);
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// Wait for network to be idle (no pending requests for 500ms)
|
|
635
|
+
await Promise.race([
|
|
636
|
+
page.waitForNetworkIdle({ idleTime: 500, timeout: 5000 }),
|
|
637
|
+
new Promise(resolve => setTimeout(resolve, 5000)) // Max 5 seconds for network
|
|
638
|
+
]);
|
|
639
|
+
|
|
640
|
+
// Wait for any DOM changes to settle
|
|
641
|
+
await page.evaluate(() => {
|
|
642
|
+
return new Promise((resolve) => {
|
|
643
|
+
let timeoutId;
|
|
644
|
+
const observer = new MutationObserver(() => {
|
|
645
|
+
clearTimeout(timeoutId);
|
|
646
|
+
timeoutId = setTimeout(() => {
|
|
647
|
+
observer.disconnect();
|
|
648
|
+
resolve();
|
|
649
|
+
}, 300); // 300ms of no DOM changes
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
observer.observe(document.body, {
|
|
653
|
+
childList: true,
|
|
654
|
+
subtree: true,
|
|
655
|
+
attributes: true,
|
|
656
|
+
attributeFilter: ['class', 'style', 'hidden', 'disabled']
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// Start the timeout
|
|
660
|
+
timeoutId = setTimeout(() => {
|
|
661
|
+
observer.disconnect();
|
|
662
|
+
resolve();
|
|
663
|
+
}, 300);
|
|
664
|
+
|
|
665
|
+
// Max wait 3 seconds
|
|
666
|
+
setTimeout(() => {
|
|
667
|
+
observer.disconnect();
|
|
668
|
+
resolve();
|
|
669
|
+
}, 3000);
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
} catch (error) {
|
|
674
|
+
// If smart wait fails, just log and continue
|
|
675
|
+
debugLog('[Smart Wait] Error during smart wait:', error.message);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Ensure we don't exceed max wait time
|
|
679
|
+
const elapsed = Date.now() - startTime;
|
|
680
|
+
if (elapsed > maxWaitTime) {
|
|
681
|
+
debugLog(`[Smart Wait] Exceeded max wait time (${maxWaitTime}ms)`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Check if page has any ongoing activity (animations, network, DOM changes)
|
|
687
|
+
* Returns true if activity detected, false otherwise
|
|
688
|
+
*/
|
|
689
|
+
async function checkPageActivity(page) {
|
|
690
|
+
try {
|
|
691
|
+
const activity = await page.evaluate(() => {
|
|
692
|
+
// Check for animations
|
|
693
|
+
const elements = document.querySelectorAll('*');
|
|
694
|
+
for (const el of elements) {
|
|
695
|
+
const computedStyle = window.getComputedStyle(el);
|
|
696
|
+
const animations = computedStyle.getPropertyValue('animation-name');
|
|
697
|
+
const transitions = computedStyle.getPropertyValue('transition-property');
|
|
698
|
+
|
|
699
|
+
if ((animations && animations !== 'none') ||
|
|
700
|
+
(transitions && transitions !== 'none' && transitions !== 'all')) {
|
|
701
|
+
return { hasActivity: true, reason: 'animations' };
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Check for recent DOM changes (using performance API)
|
|
706
|
+
if (window.performance && window.performance.getEntriesByType) {
|
|
707
|
+
const entries = window.performance.getEntriesByType('measure');
|
|
708
|
+
if (entries.length > 0) {
|
|
709
|
+
const recentEntries = entries.filter(e =>
|
|
710
|
+
performance.now() - e.startTime < 500
|
|
711
|
+
);
|
|
712
|
+
if (recentEntries.length > 0) {
|
|
713
|
+
return { hasActivity: true, reason: 'performance_measures' };
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return { hasActivity: false, reason: 'none' };
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
if (activity.hasActivity) {
|
|
722
|
+
debugLog(`[Smart Wait] Activity detected: ${activity.reason}`);
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Check for pending network requests using CDP
|
|
727
|
+
try {
|
|
728
|
+
const client = await page.target().createCDPSession();
|
|
729
|
+
await client.send('Network.enable');
|
|
730
|
+
|
|
731
|
+
// Give a moment for network requests to start
|
|
732
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
733
|
+
|
|
734
|
+
const hasNetworkActivity = await new Promise((resolve) => {
|
|
735
|
+
let requestCount = 0;
|
|
736
|
+
|
|
737
|
+
const requestListener = () => {
|
|
738
|
+
requestCount++;
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
client.on('Network.requestWillBeSent', requestListener);
|
|
742
|
+
|
|
743
|
+
setTimeout(() => {
|
|
744
|
+
client.off('Network.requestWillBeSent', requestListener);
|
|
745
|
+
client.detach().catch(() => {});
|
|
746
|
+
resolve(requestCount > 0);
|
|
747
|
+
}, 200);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
if (hasNetworkActivity) {
|
|
751
|
+
debugLog('[Smart Wait] Network activity detected');
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
} catch (netError) {
|
|
755
|
+
// Network check failed, assume no activity
|
|
756
|
+
debugLog('[Smart Wait] Network check failed, assuming no activity');
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return false;
|
|
760
|
+
} catch (error) {
|
|
761
|
+
// If check fails, assume there's activity to be safe
|
|
762
|
+
debugLog('[Smart Wait] Activity check failed, assuming activity exists:', error.message);
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function executeType(action, page, timeout) {
|
|
768
|
+
const selector = action.selector.value || action.selector.primary || action.selector;
|
|
769
|
+
|
|
770
|
+
try {
|
|
771
|
+
await page.waitForSelector(selector, { timeout, visible: true });
|
|
772
|
+
|
|
773
|
+
// Check if element is editable
|
|
774
|
+
const isEditable = await page.evaluate((sel) => {
|
|
775
|
+
const el = document.querySelector(sel);
|
|
776
|
+
if (!el) return false;
|
|
777
|
+
|
|
778
|
+
const isInput = el.tagName === 'INPUT' || el.tagName === 'TEXTAREA';
|
|
779
|
+
const isContentEditable = el.isContentEditable;
|
|
780
|
+
|
|
781
|
+
return isInput || isContentEditable;
|
|
782
|
+
}, selector);
|
|
783
|
+
|
|
784
|
+
if (!isEditable) {
|
|
785
|
+
throw new Error(`Element "${selector}" is not editable (not an input, textarea, or contenteditable)`);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Clear field if specified
|
|
789
|
+
if (action.data.clearFirst !== false) {
|
|
790
|
+
await page.click(selector, { clickCount: 3 });
|
|
791
|
+
await page.keyboard.press('Backspace');
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Type text with optional delay
|
|
795
|
+
await page.type(selector, action.data.text, {
|
|
796
|
+
delay: action.data.delay || 0
|
|
797
|
+
});
|
|
798
|
+
} catch (error) {
|
|
799
|
+
throw new Error(`Failed to type into "${selector}": ${error.message}`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function executeSelect(action, page, timeout) {
|
|
804
|
+
const selector = action.selector.value || action.selector.primary || action.selector;
|
|
805
|
+
|
|
806
|
+
try {
|
|
807
|
+
if (action.data.selectType === 'custom') {
|
|
808
|
+
// Custom select (multi-step)
|
|
809
|
+
for (const step of action.data.steps) {
|
|
810
|
+
if (step.action === 'click') {
|
|
811
|
+
await page.waitForSelector(step.selector, { timeout });
|
|
812
|
+
await page.click(step.selector);
|
|
813
|
+
} else if (step.action === 'wait') {
|
|
814
|
+
await new Promise(resolve => setTimeout(resolve, step.duration));
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
} else {
|
|
818
|
+
// Native select
|
|
819
|
+
await page.waitForSelector(selector, { timeout, visible: true });
|
|
820
|
+
|
|
821
|
+
// Verify it's a select element
|
|
822
|
+
const isSelect = await page.evaluate((sel) => {
|
|
823
|
+
const el = document.querySelector(sel);
|
|
824
|
+
return el && el.tagName === 'SELECT';
|
|
825
|
+
}, selector);
|
|
826
|
+
|
|
827
|
+
if (!isSelect) {
|
|
828
|
+
throw new Error(`Element "${selector}" is not a <select> element`);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
await page.select(selector, action.data.value);
|
|
832
|
+
}
|
|
833
|
+
} catch (error) {
|
|
834
|
+
throw new Error(`Failed to select option in "${selector}": ${error.message}`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function executeScroll(action, page) {
|
|
839
|
+
const selector = action.selector.value || action.selector.primary || action.selector;
|
|
840
|
+
await page.evaluate((selector, behavior) => {
|
|
841
|
+
const element = document.querySelector(selector);
|
|
842
|
+
if (element) {
|
|
843
|
+
element.scrollIntoView({ behavior: behavior || 'auto', block: 'center' });
|
|
844
|
+
}
|
|
845
|
+
}, selector, action.data.behavior);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function executeHover(action, page) {
|
|
849
|
+
const selector = action.selector.value || action.selector.primary || action.selector;
|
|
850
|
+
await page.hover(selector);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async function executeKeypress(action, page) {
|
|
854
|
+
const key = action.data.key;
|
|
855
|
+
const modifiers = action.data.modifiers || [];
|
|
856
|
+
|
|
857
|
+
// Press modifiers
|
|
858
|
+
for (const mod of modifiers) {
|
|
859
|
+
await page.keyboard.down(mod);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Press key
|
|
863
|
+
await page.keyboard.press(key);
|
|
864
|
+
|
|
865
|
+
// Release modifiers
|
|
866
|
+
for (const mod of modifiers.reverse()) {
|
|
867
|
+
await page.keyboard.up(mod);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async function executeWait(action, page) {
|
|
872
|
+
if (action.data.waitType === 'selector') {
|
|
873
|
+
await page.waitForSelector(action.data.selector, {
|
|
874
|
+
timeout: action.data.duration
|
|
875
|
+
});
|
|
876
|
+
} else {
|
|
877
|
+
await new Promise(resolve => setTimeout(resolve, action.data.duration));
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
async function executeUpload(action, page, timeout) {
|
|
882
|
+
const selector = action.selector.value || action.selector.primary || action.selector;
|
|
883
|
+
const fileInput = await page.waitForSelector(selector, { timeout });
|
|
884
|
+
await fileInput.uploadFile(action.data.filePath);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async function executeDrag(action, page) {
|
|
888
|
+
const { fromSelector, toSelector, fromX, fromY, toX, toY } = action.data;
|
|
889
|
+
|
|
890
|
+
if (fromSelector && toSelector) {
|
|
891
|
+
// Drag from element to element
|
|
892
|
+
const from = await page.$(fromSelector);
|
|
893
|
+
const to = await page.$(toSelector);
|
|
894
|
+
|
|
895
|
+
const fromBox = await from.boundingBox();
|
|
896
|
+
const toBox = await to.boundingBox();
|
|
897
|
+
|
|
898
|
+
await page.mouse.move(fromBox.x + fromBox.width / 2, fromBox.y + fromBox.height / 2);
|
|
899
|
+
await page.mouse.down();
|
|
900
|
+
await page.mouse.move(toBox.x + toBox.width / 2, toBox.y + toBox.height / 2);
|
|
901
|
+
await page.mouse.up();
|
|
902
|
+
} else {
|
|
903
|
+
// Drag by coordinates
|
|
904
|
+
await page.mouse.move(fromX, fromY);
|
|
905
|
+
await page.mouse.down();
|
|
906
|
+
await page.mouse.move(toX, toY);
|
|
907
|
+
await page.mouse.up();
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async function executeNavigate(action, page, timeout) {
|
|
912
|
+
await page.goto(action.data.url, {
|
|
913
|
+
waitUntil: action.data.waitUntil || 'networkidle2',
|
|
914
|
+
timeout
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async function executeExtract(action, page) {
|
|
919
|
+
const { selector, attribute, multiple } = action.data;
|
|
920
|
+
|
|
921
|
+
if (multiple) {
|
|
922
|
+
return await page.$$eval(selector, (elements, attr) => {
|
|
923
|
+
return elements.map(el => attr ? el.getAttribute(attr) : el.textContent.trim());
|
|
924
|
+
}, attribute);
|
|
925
|
+
} else {
|
|
926
|
+
return await page.$eval(selector, (el, attr) => {
|
|
927
|
+
return attr ? el.getAttribute(attr) : el.textContent.trim();
|
|
928
|
+
}, attribute);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Substitute parameters in action
|
|
934
|
+
* Replaces {{paramName}} with actual values
|
|
935
|
+
*/
|
|
936
|
+
function substituteParameters(action, params) {
|
|
937
|
+
const resolved = JSON.parse(JSON.stringify(action));
|
|
938
|
+
|
|
939
|
+
// Substitute in action data
|
|
940
|
+
if (resolved.data) {
|
|
941
|
+
for (const [key, value] of Object.entries(resolved.data)) {
|
|
942
|
+
if (typeof value === 'string') {
|
|
943
|
+
resolved.data[key] = substituteString(value, params);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return resolved;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Substitute {{param}} in string
|
|
953
|
+
*/
|
|
954
|
+
function substituteString(str, params) {
|
|
955
|
+
return str.replace(/\{\{(\w+)\}\}/g, (match, paramName) => {
|
|
956
|
+
if (params[paramName] !== undefined) {
|
|
957
|
+
return params[paramName];
|
|
958
|
+
}
|
|
959
|
+
return match; // Keep original if param not found
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Element finder utils code (to be injected into page)
|
|
965
|
+
* Will be loaded from utils/element-finder-utils.js
|
|
966
|
+
*/
|
|
967
|
+
const elementFinderUtilsCode = `
|
|
968
|
+
// This will be populated from element-finder-utils.js browser-side code
|
|
969
|
+
// For now, placeholder
|
|
970
|
+
`;
|