cdp-skill 1.0.16 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +4 -4
  2. package/SKILL.md +276 -170
  3. package/package.json +9 -8
  4. package/{src → scripts}/aria/index.js +1 -1
  5. package/scripts/aria/role-query.js +295 -0
  6. package/{src → scripts}/aria.js +11 -5
  7. package/{src → scripts}/capture/console-capture.js +11 -9
  8. package/{src → scripts}/capture/screenshot-capture.js +8 -9
  9. package/{src → scripts}/cdp/connection.js +30 -6
  10. package/{src → scripts}/cdp-skill.js +7 -6
  11. package/{src → scripts}/diff.js +7 -6
  12. package/{src → scripts}/dom/LazyResolver.js +23 -12
  13. package/{src → scripts}/dom/actionability.js +39 -22
  14. package/{src → scripts}/dom/click-executor.js +90 -53
  15. package/{src → scripts}/dom/element-locator.js +4 -4
  16. package/{src → scripts}/dom/fill-executor.js +8 -4
  17. package/{src → scripts}/dom/input-emulator.js +47 -9
  18. package/{src → scripts}/dom/react-filler.js +11 -3
  19. package/{src → scripts}/dom/wait-executor.js +10 -2
  20. package/{src → scripts}/page/dialog-handler.js +7 -3
  21. package/{src → scripts}/page/dom-stability.js +17 -10
  22. package/{src → scripts}/page/page-controller.js +41 -34
  23. package/{src → scripts}/runner/context-helpers.js +7 -0
  24. package/{src → scripts}/runner/execute-browser.js +3 -118
  25. package/{src → scripts}/runner/execute-dynamic.js +46 -11
  26. package/{src → scripts}/runner/execute-form.js +6 -4
  27. package/{src → scripts}/runner/execute-input.js +127 -100
  28. package/{src → scripts}/runner/execute-interaction.js +31 -46
  29. package/{src → scripts}/runner/execute-navigation.js +14 -12
  30. package/{src → scripts}/runner/step-executors.js +28 -9
  31. package/{src → scripts}/runner/step-registry.js +57 -8
  32. package/{src → scripts}/runner/step-validator.js +13 -3
  33. package/{src → scripts}/tests/ExecuteInput.test.js +58 -188
  34. package/src/aria/role-query.js +0 -1229
  35. package/src/aria/snapshot.js +0 -459
  36. /package/{src → scripts}/aria/output-processor.js +0 -0
  37. /package/{src → scripts}/capture/debug-capture.js +0 -0
  38. /package/{src → scripts}/capture/error-aggregator.js +0 -0
  39. /package/{src → scripts}/capture/eval-serializer.js +0 -0
  40. /package/{src → scripts}/capture/index.js +0 -0
  41. /package/{src → scripts}/capture/network-capture.js +0 -0
  42. /package/{src → scripts}/capture/pdf-capture.js +0 -0
  43. /package/{src → scripts}/cdp/browser.js +0 -0
  44. /package/{src → scripts}/cdp/discovery.js +0 -0
  45. /package/{src → scripts}/cdp/index.js +0 -0
  46. /package/{src → scripts}/cdp/target-and-session.js +0 -0
  47. /package/{src → scripts}/constants.js +0 -0
  48. /package/{src → scripts}/dom/element-handle.js +0 -0
  49. /package/{src → scripts}/dom/element-validator.js +0 -0
  50. /package/{src → scripts}/dom/index.js +0 -0
  51. /package/{src → scripts}/dom/keyboard-executor.js +0 -0
  52. /package/{src → scripts}/dom/quad-helpers.js +0 -0
  53. /package/{src → scripts}/index.js +0 -0
  54. /package/{src → scripts}/page/cookie-manager.js +0 -0
  55. /package/{src → scripts}/page/index.js +0 -0
  56. /package/{src → scripts}/page/wait-utilities.js +0 -0
  57. /package/{src → scripts}/page/web-storage-manager.js +0 -0
  58. /package/{src → scripts}/runner/execute-query.js +0 -0
  59. /package/{src → scripts}/runner/index.js +0 -0
  60. /package/{src → scripts}/tests/Actionability.test.js +0 -0
  61. /package/{src → scripts}/tests/Aria.test.js +0 -0
  62. /package/{src → scripts}/tests/BrowserClient.test.js +0 -0
  63. /package/{src → scripts}/tests/CDPConnection.test.js +0 -0
  64. /package/{src → scripts}/tests/ChromeDiscovery.test.js +0 -0
  65. /package/{src → scripts}/tests/ClickExecutor.test.js +0 -0
  66. /package/{src → scripts}/tests/ConsoleCapture.test.js +0 -0
  67. /package/{src → scripts}/tests/ContextHelpers.test.js +0 -0
  68. /package/{src → scripts}/tests/CookieManager.test.js +0 -0
  69. /package/{src → scripts}/tests/DebugCapture.test.js +0 -0
  70. /package/{src → scripts}/tests/ElementHandle.test.js +0 -0
  71. /package/{src → scripts}/tests/ElementLocator.test.js +0 -0
  72. /package/{src → scripts}/tests/ErrorAggregator.test.js +0 -0
  73. /package/{src → scripts}/tests/EvalSerializer.test.js +0 -0
  74. /package/{src → scripts}/tests/ExecuteBrowser.test.js +0 -0
  75. /package/{src → scripts}/tests/ExecuteDynamic.test.js +0 -0
  76. /package/{src → scripts}/tests/ExecuteForm.test.js +0 -0
  77. /package/{src → scripts}/tests/ExecuteInteraction.test.js +0 -0
  78. /package/{src → scripts}/tests/ExecuteQuery.test.js +0 -0
  79. /package/{src → scripts}/tests/FillExecutor.test.js +0 -0
  80. /package/{src → scripts}/tests/InputEmulator.test.js +0 -0
  81. /package/{src → scripts}/tests/KeyboardExecutor.test.js +0 -0
  82. /package/{src → scripts}/tests/LazyResolver.test.js +0 -0
  83. /package/{src → scripts}/tests/NetworkErrorCapture.test.js +0 -0
  84. /package/{src → scripts}/tests/PageController.test.js +0 -0
  85. /package/{src → scripts}/tests/PdfCapture.test.js +0 -0
  86. /package/{src → scripts}/tests/ScreenshotCapture.test.js +0 -0
  87. /package/{src → scripts}/tests/SessionRegistry.test.js +0 -0
  88. /package/{src → scripts}/tests/StepValidator.test.js +0 -0
  89. /package/{src → scripts}/tests/TargetManager.test.js +0 -0
  90. /package/{src → scripts}/tests/TestRunner.test.js +0 -0
  91. /package/{src → scripts}/tests/WaitStrategy.test.js +0 -0
  92. /package/{src → scripts}/tests/WaitUtilities.test.js +0 -0
  93. /package/{src → scripts}/tests/WebStorageManager.test.js +0 -0
  94. /package/{src → scripts}/tests/integration.test.js +0 -0
  95. /package/{src → scripts}/types.js +0 -0
  96. /package/{src → scripts}/utils/backoff.js +0 -0
  97. /package/{src → scripts}/utils/cdp-helpers.js +0 -0
  98. /package/{src → scripts}/utils/devices.js +0 -0
  99. /package/{src → scripts}/utils/errors.js +0 -0
  100. /package/{src → scripts}/utils/index.js +0 -0
  101. /package/{src → scripts}/utils/temp.js +0 -0
  102. /package/{src → scripts}/utils/validators.js +0 -0
  103. /package/{src → scripts}/utils.js +0 -0
@@ -140,8 +140,7 @@ export function createPageController(cdpClient, options = {}) {
140
140
  }
141
141
  }
142
142
 
143
- function onRequestStarted({ requestId, frameId }) {
144
- if (frameId && frameId !== mainFrameId) return;
143
+ function onRequestStarted({ requestId }) {
145
144
  pendingRequests.add(requestId);
146
145
  networkRequestCount++;
147
146
  lastNetworkActivity = Date.now();
@@ -424,7 +423,9 @@ export function createPageController(cdpClient, options = {}) {
424
423
  addListener('Runtime.executionContextCreated', ({ context }) => {
425
424
  if (context.auxData && context.auxData.frameId) {
426
425
  frameExecutionContexts.set(context.auxData.frameId, context.id);
427
- if (context.auxData.frameId === mainFrameId) {
426
+ // Update current context when the active frame gets a new context
427
+ // (handles both main frame and iframes after switchToFrame)
428
+ if (context.auxData.frameId === currentFrameId) {
428
429
  currentExecutionContextId = context.id;
429
430
  }
430
431
  }
@@ -437,6 +438,14 @@ export function createPageController(cdpClient, options = {}) {
437
438
  break;
438
439
  }
439
440
  }
441
+ if (executionContextId === currentExecutionContextId) {
442
+ currentExecutionContextId = null;
443
+ }
444
+ });
445
+
446
+ addListener('Runtime.executionContextsCleared', () => {
447
+ frameExecutionContexts.clear();
448
+ currentExecutionContextId = null;
440
449
  });
441
450
 
442
451
  addListener('Page.lifecycleEvent', onLifecycleEvent);
@@ -552,9 +561,8 @@ export function createPageController(cdpClient, options = {}) {
552
561
 
553
562
  await waitPromise;
554
563
 
555
- if (abortReason) {
556
- throw navigationAbortedError(abortReason, url);
557
- }
564
+ // Note: if abort happened during waitPromise, abortWaiters already rejected it.
565
+ // We don't re-check abortReason here to avoid rejecting a successfully completed navigation.
558
566
 
559
567
  return {
560
568
  frameId: response.frameId,
@@ -646,10 +654,7 @@ export function createPageController(cdpClient, options = {}) {
646
654
  */
647
655
  async function getUrl() {
648
656
  try {
649
- const result = await cdpClient.send('Runtime.evaluate', {
650
- expression: 'window.location.href',
651
- returnByValue: true
652
- });
657
+ const result = await evaluateInFrame('window.location.href');
653
658
  return result.result.value;
654
659
  } catch (error) {
655
660
  throw connectionError(error.message, 'Runtime.evaluate (getUrl)');
@@ -662,10 +667,7 @@ export function createPageController(cdpClient, options = {}) {
662
667
  */
663
668
  async function getTitle() {
664
669
  try {
665
- const result = await cdpClient.send('Runtime.evaluate', {
666
- expression: 'document.title',
667
- returnByValue: true
668
- });
670
+ const result = await evaluateInFrame('document.title');
669
671
  return result.result.value;
670
672
  } catch (error) {
671
673
  throw connectionError(error.message, 'Runtime.evaluate (getTitle)');
@@ -689,6 +691,7 @@ export function createPageController(cdpClient, options = {}) {
689
691
  pendingRequests.clear();
690
692
  crashWaiters.clear();
691
693
  abortWaiters.clear();
694
+ networkIdleWaiters.clear();
692
695
  }
693
696
 
694
697
  /**
@@ -754,11 +757,11 @@ export function createPageController(cdpClient, options = {}) {
754
757
  async function setGeolocation(options) {
755
758
  const { latitude, longitude, accuracy = 1 } = options;
756
759
 
757
- if (latitude < -90 || latitude > 90) {
758
- throw new Error('Latitude must be between -90 and 90');
760
+ if (typeof latitude !== 'number' || Number.isNaN(latitude) || latitude < -90 || latitude > 90) {
761
+ throw new Error('Latitude must be a number between -90 and 90');
759
762
  }
760
- if (longitude < -180 || longitude > 180) {
761
- throw new Error('Longitude must be between -180 and 180');
763
+ if (typeof longitude !== 'number' || Number.isNaN(longitude) || longitude < -180 || longitude > 180) {
764
+ throw new Error('Longitude must be a number between -180 and 180');
762
765
  }
763
766
 
764
767
  await cdpClient.send('Emulation.setGeolocationOverride', {
@@ -811,7 +814,7 @@ export function createPageController(cdpClient, options = {}) {
811
814
 
812
815
  // Discover cross-origin iframes via DOM query
813
816
  try {
814
- const domResult = await cdpClient.send('Runtime.evaluate', {
817
+ const evalParams = {
815
818
  expression: `
816
819
  (function() {
817
820
  const iframes = document.querySelectorAll('iframe');
@@ -826,7 +829,9 @@ export function createPageController(cdpClient, options = {}) {
826
829
  })()
827
830
  `,
828
831
  returnByValue: true
829
- });
832
+ };
833
+ if (currentExecutionContextId) evalParams.contextId = currentExecutionContextId;
834
+ const domResult = await cdpClient.send('Runtime.evaluate', evalParams);
830
835
 
831
836
  const domIframes = domResult.result?.value;
832
837
  if (Array.isArray(domIframes)) {
@@ -894,7 +899,7 @@ export function createPageController(cdpClient, options = {}) {
894
899
  targetFrame = allFrames.find(f => f.frame.name === params);
895
900
 
896
901
  if (!targetFrame) {
897
- const result = await cdpClient.send('Runtime.evaluate', {
902
+ const findParams = {
898
903
  expression: `
899
904
  (function() {
900
905
  const iframe = document.querySelector(${JSON.stringify(params)});
@@ -903,10 +908,12 @@ export function createPageController(cdpClient, options = {}) {
903
908
  })()
904
909
  `,
905
910
  returnByValue: true
906
- });
911
+ };
912
+ if (currentExecutionContextId) findParams.contextId = currentExecutionContextId;
913
+ const result = await cdpClient.send('Runtime.evaluate', findParams);
907
914
 
908
- if (result.result.value === 'found') {
909
- const srcResult = await cdpClient.send('Runtime.evaluate', {
915
+ if (result.result?.value === 'found') {
916
+ const srcParams = {
910
917
  expression: `
911
918
  (function() {
912
919
  const iframe = document.querySelector(${JSON.stringify(params)});
@@ -918,15 +925,18 @@ export function createPageController(cdpClient, options = {}) {
918
925
  })()
919
926
  `,
920
927
  returnByValue: true
921
- });
928
+ };
929
+ if (currentExecutionContextId) srcParams.contextId = currentExecutionContextId;
930
+ const srcResult = await cdpClient.send('Runtime.evaluate', srcParams);
922
931
 
923
- if (srcResult.result.value) {
932
+ if (srcResult.result?.value) {
924
933
  const { src, name } = srcResult.result.value;
925
934
  targetFrame = allFrames.find(f =>
926
- (src && f.frame.url === src) ||
927
- (src && f.frame.url.endsWith(src)) ||
928
- (name && f.frame.name === name) ||
929
- f.frame.parentId
935
+ f.frame.parentId && (
936
+ (src && f.frame.url === src) ||
937
+ (src && f.frame.url.endsWith(src)) ||
938
+ (name && f.frame.name === name)
939
+ )
930
940
  );
931
941
 
932
942
  if (!targetFrame) {
@@ -1108,10 +1118,7 @@ export function createPageController(cdpClient, options = {}) {
1108
1118
  */
1109
1119
  async function getViewport() {
1110
1120
  try {
1111
- const result = await cdpClient.send('Runtime.evaluate', {
1112
- expression: '({ width: window.innerWidth, height: window.innerHeight })',
1113
- returnByValue: true
1114
- });
1121
+ const result = await evaluateInFrame('({ width: window.innerWidth, height: window.innerHeight })');
1115
1122
  return result.result.value;
1116
1123
  } catch (error) {
1117
1124
  throw connectionError(error.message, 'Runtime.evaluate (getViewport)');
@@ -69,6 +69,13 @@ export function buildActionContext(action, params, context) {
69
69
  case 'press': {
70
70
  return `Pressed ${params || 'key'}`;
71
71
  }
72
+ case 'upload': {
73
+ if (typeof params === 'string') return `Uploaded ${params}`;
74
+ if (Array.isArray(params)) return `Uploaded ${params.length} file(s)`;
75
+ if (params?.selector) return `Uploaded to ${params.selector}`;
76
+ if (params?.ref) return `Uploaded to [ref=${params.ref}]`;
77
+ return 'Uploaded file(s)';
78
+ }
72
79
  default:
73
80
  return '';
74
81
  }
@@ -120,14 +120,14 @@ export async function executeEval(pageController, params) {
120
120
  }
121
121
 
122
122
  // Process serialized result if serialization was used
123
- if (serialize && result.result.value && typeof result.result.value === 'object') {
123
+ if (serialize && result.result?.value && typeof result.result.value === 'object') {
124
124
  const evalSerializer = createEvalSerializer();
125
125
  return evalSerializer.processResult(result.result.value);
126
126
  }
127
127
 
128
128
  return {
129
- value: result.result.value,
130
- type: result.result.type
129
+ value: result.result?.value,
130
+ type: result.result?.type
131
131
  };
132
132
  }
133
133
 
@@ -402,121 +402,6 @@ export function formatCommandConsole(consoleCapture, messageCountBefore) {
402
402
  };
403
403
  }
404
404
 
405
- /**
406
- * Run an array of test steps
407
- * @param {Object} deps - Dependencies
408
- * @param {Array<Object>} steps - Array of step definitions
409
- * @param {Object} [options] - Execution options
410
- * @param {boolean} [options.stopOnError=true] - Stop on first error
411
- * @param {number} [options.stepTimeout=30000] - Timeout per step
412
- * @returns {Promise<{status: string, steps: Array, errors: Array}>}
413
- */
414
- export async function runSteps(deps, steps, options = {}) {
415
- const validation = validateSteps(steps);
416
- if (!validation.valid) {
417
- throw stepValidationError(validation.errors);
418
- }
419
-
420
- const stopOnError = options.stopOnError !== false;
421
- const result = {
422
- status: 'ok',
423
- steps: [],
424
- errors: []
425
- };
426
-
427
- // Capture console message count before command starts
428
- const consoleCountBefore = deps.consoleCapture ? deps.consoleCapture.getMessages().length : 0;
429
-
430
- // Feature 8.1: Capture BEFORE state at command start (for diff baseline)
431
- let beforeUrl, beforeViewport, beforeSnapshot;
432
- const contextCapture = deps.pageController ? createContextCapture(deps.pageController.session) : null;
433
-
434
- if (deps.ariaSnapshot && contextCapture) {
435
- try {
436
- beforeUrl = await getCurrentUrl(deps.pageController.session);
437
- // Capture viewport-only snapshot for command-level diff
438
- // Use preserveRefs to avoid clobbering refs from snapshotSearch
439
- // Use internal to avoid incrementing snapshot ID (this is for diff, not agent-facing)
440
- beforeViewport = await deps.ariaSnapshot.generate({ mode: 'ai', viewportOnly: true, preserveRefs: true, internal: true });
441
- } catch {
442
- // Ignore initial snapshot errors - will just skip diff comparison
443
- }
444
- }
445
-
446
- for (const step of steps) {
447
- const stepResult = await executeStep(deps, step, options);
448
- result.steps.push(stepResult);
449
-
450
- if (stepResult.status === 'error') {
451
- result.status = 'error';
452
- result.errors.push({
453
- step: result.steps.length,
454
- action: stepResult.action,
455
- error: stepResult.error
456
- });
457
-
458
- if (stopOnError) {
459
- break;
460
- }
461
- }
462
- // 'skipped' (optional) steps don't fail the run
463
- }
464
-
465
- // Wait for async console messages after steps complete
466
- if (deps.consoleCapture) {
467
- await sleep(250);
468
- const consoleSummary = formatCommandConsole(deps.consoleCapture, consoleCountBefore);
469
- if (consoleSummary) {
470
- result.console = consoleSummary;
471
- }
472
- }
473
-
474
- // Feature 8.1: Capture AFTER state and compute command-level diff
475
- if (deps.ariaSnapshot && contextCapture && beforeViewport) {
476
- try {
477
- const afterUrl = await getCurrentUrl(deps.pageController.session);
478
- const afterContext = await contextCapture.captureContext();
479
-
480
- // Capture both viewport and full page snapshots
481
- // Use preserveRefs to avoid clobbering refs from snapshotSearch
482
- // Use internal to avoid incrementing snapshot ID (this is for diff, not agent-facing)
483
- const afterViewport = await deps.ariaSnapshot.generate({ mode: 'ai', viewportOnly: true, preserveRefs: true, internal: true });
484
- const afterFull = await deps.ariaSnapshot.generate({ mode: 'ai', viewportOnly: false, preserveRefs: true, internal: true });
485
-
486
- const navigated = contextCapture.isNavigation(beforeUrl, afterUrl);
487
-
488
- // Save full page snapshot to file (use tabAlias for filename)
489
- const fullSnapshotPath = await resolveTempPath(`${options.tabAlias || 'command'}.after.yaml`, '.yaml');
490
- await fs.writeFile(fullSnapshotPath, afterFull.yaml || '', 'utf8');
491
-
492
- // Add command-level results
493
- result.navigated = navigated;
494
- result.fullSnapshot = fullSnapshotPath;
495
- result.context = afterContext;
496
-
497
- // Always include viewport snapshot inline
498
- result.viewportSnapshot = afterViewport.yaml;
499
- result.truncated = afterViewport.truncated || false;
500
-
501
- // For same-page interactions, compute viewport diff
502
- if (!navigated && beforeViewport?.yaml) {
503
- const differ = createSnapshotDiffer();
504
- const viewportDiff = differ.computeDiff(beforeViewport.yaml, afterViewport.yaml);
505
-
506
- // Report changes if any significant changes found
507
- if (differ.hasSignificantChanges(viewportDiff)) {
508
- const actionContext = buildCommandContext(steps);
509
- result.changes = differ.formatDiff(viewportDiff, { actionContext });
510
- }
511
- }
512
- } catch (e) {
513
- result.viewportSnapshotError = e.message;
514
- }
515
- }
516
-
517
- return result;
518
- }
519
-
520
405
  /**
521
406
  * Execute a validate step - query validation state of an element
522
407
  */
@@ -65,8 +65,20 @@ export async function executePageFunction(pageController, params) {
65
65
  const arg = useRefs ? 'window.__ariaRefs' : '';
66
66
  const serializerFn = getSerializationWrapper();
67
67
 
68
+ // Detect async functions to await their return value
69
+ const isAsync = /^async[\s(]/.test(fn.trim()) ||
70
+ (fn.trim().startsWith('(') && /^async[\s(]/.test(fn.trim().slice(1).trim()));
71
+
68
72
  // Wrap the agent function so its return value is serialized
69
- const wrapped = `(function() {
73
+ // Use async wrapper when the function is async so we can await its result
74
+ const wrapped = isAsync
75
+ ? `(async function() {
76
+ const __fn = ${fn};
77
+ const __serialize = ${serializerFn};
78
+ const __result = await __fn(${arg});
79
+ return __serialize(__result);
80
+ })()`
81
+ : `(function() {
70
82
  const __fn = ${fn};
71
83
  const __serialize = ${serializerFn};
72
84
  const __result = __fn(${arg});
@@ -75,7 +87,7 @@ export async function executePageFunction(pageController, params) {
75
87
 
76
88
  const evalPromise = pageController.evaluateInFrame(wrapped, {
77
89
  returnByValue: true,
78
- awaitPromise: false
90
+ awaitPromise: isAsync
79
91
  });
80
92
 
81
93
  let result;
@@ -84,8 +96,14 @@ export async function executePageFunction(pageController, params) {
84
96
  const tp = new Promise((_, reject) => {
85
97
  tid = setTimeout(() => reject(new Error(`pageFunction timed out after ${timeout}ms`)), timeout);
86
98
  });
87
- result = await Promise.race([evalPromise, tp]);
88
- clearTimeout(tid);
99
+ try {
100
+ result = await Promise.race([evalPromise, tp]);
101
+ } catch (err) {
102
+ evalPromise.catch(() => {});
103
+ throw err;
104
+ } finally {
105
+ clearTimeout(tid);
106
+ }
89
107
  } else {
90
108
  result = await evalPromise;
91
109
  }
@@ -96,7 +114,7 @@ export async function executePageFunction(pageController, params) {
96
114
  throw new Error(`pageFunction error: ${errorText}\nSource: ${fn.substring(0, 200)}`);
97
115
  }
98
116
 
99
- return processSerializedResult(result.result.value);
117
+ return processSerializedResult(result.result?.value);
100
118
  }
101
119
 
102
120
  // ---------------------------------------------------------------------------
@@ -122,7 +140,18 @@ export async function executePoll(pageController, params) {
122
140
  }
123
141
 
124
142
  const serializerFn = getSerializationWrapper();
125
- const expression = `(function() {
143
+
144
+ // Detect async predicates to properly await their return value
145
+ const isAsync = /^async[\s(]/.test(fn.trim()) ||
146
+ (fn.trim().startsWith('(') && /^async[\s(]/.test(fn.trim().slice(1).trim()));
147
+
148
+ const expression = isAsync
149
+ ? `(async function() {
150
+ const __fn = ${fn};
151
+ const __serialize = ${serializerFn};
152
+ return __serialize(await __fn());
153
+ })()`
154
+ : `(function() {
126
155
  const __fn = ${fn};
127
156
  const __serialize = ${serializerFn};
128
157
  return __serialize(__fn());
@@ -134,7 +163,7 @@ export async function executePoll(pageController, params) {
134
163
  while (true) {
135
164
  const result = await pageController.evaluateInFrame(expression, {
136
165
  returnByValue: true,
137
- awaitPromise: false
166
+ awaitPromise: isAsync
138
167
  });
139
168
 
140
169
  if (result.exceptionDetails) {
@@ -143,11 +172,11 @@ export async function executePoll(pageController, params) {
143
172
  throw new Error(`poll error: ${errorText}\nSource: ${fn.substring(0, 200)}`);
144
173
  }
145
174
 
146
- const processed = processSerializedResult(result.result.value);
175
+ const processed = processSerializedResult(result.result?.value);
147
176
  lastValue = processed;
148
177
 
149
178
  // Check truthiness from the raw value (before serialization wrapping)
150
- const rawVal = result.result.value;
179
+ const rawVal = result.result?.value;
151
180
  const isTruthy = rawVal !== null && rawVal !== undefined &&
152
181
  rawVal !== false && rawVal !== 0 && rawVal !== '' &&
153
182
  !(typeof rawVal === 'object' && rawVal !== null && rawVal.type === 'null') &&
@@ -336,8 +365,14 @@ export async function executePipeline(pageController, params) {
336
365
  const tp = new Promise((_, reject) => {
337
366
  tid = setTimeout(() => reject(new Error(`pipeline timed out after ${timeout}ms`)), timeout);
338
367
  });
339
- result = await Promise.race([evalPromise, tp]);
340
- clearTimeout(tid);
368
+ try {
369
+ result = await Promise.race([evalPromise, tp]);
370
+ } catch (err) {
371
+ evalPromise.catch(() => {});
372
+ throw err;
373
+ } finally {
374
+ clearTimeout(tid);
375
+ }
341
376
  } else {
342
377
  result = await evalPromise;
343
378
  }
@@ -105,8 +105,10 @@ export async function executeExtract(deps, params) {
105
105
  }
106
106
  }
107
107
 
108
- // Get data rows
109
- const dataRows = tableEl.querySelectorAll('tbody tr, tr');
108
+ // Get data rows - prefer tbody rows, fall back to all rows
109
+ const dataRows = tableEl.querySelector('tbody')
110
+ ? tableEl.querySelectorAll('tbody tr')
111
+ : tableEl.querySelectorAll('tr');
110
112
  let count = 0;
111
113
  for (const row of dataRows) {
112
114
  // Skip header row
@@ -222,7 +224,7 @@ export async function executeAssert(pageController, elementLocator, params) {
222
224
 
223
225
  // URL assertion
224
226
  if (params.url) {
225
- const currentUrl = await pageController.getUrl();
227
+ const currentUrl = (await pageController.getUrl()) || '';
226
228
  const urlAssertion = { type: 'url', actual: currentUrl };
227
229
 
228
230
  if (params.url.contains) {
@@ -264,7 +266,7 @@ export async function executeAssert(pageController, elementLocator, params) {
264
266
  })()
265
267
  `);
266
268
 
267
- const actualText = textResult.result.value;
269
+ const actualText = textResult.result?.value ?? null;
268
270
  textAssertion.found = actualText !== null;
269
271
 
270
272
  if (actualText === null) {