testdriverai 7.2.55 → 7.2.57

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/sdk.js CHANGED
@@ -19,13 +19,15 @@ function getCallerFilePath() {
19
19
  // Look for the first file that's not sdk.js, hooks.mjs, or node internals
20
20
  for (const callSite of stack) {
21
21
  const fileName = callSite.getFileName();
22
- if (fileName &&
23
- !fileName.includes('sdk.js') &&
24
- !fileName.includes('hooks.mjs') &&
25
- !fileName.includes('hooks.js') &&
26
- !fileName.includes('node_modules') &&
27
- !fileName.includes('node:internal') &&
28
- fileName !== 'evalmachine.<anonymous>') {
22
+ if (
23
+ fileName &&
24
+ !fileName.includes("sdk.js") &&
25
+ !fileName.includes("hooks.mjs") &&
26
+ !fileName.includes("hooks.js") &&
27
+ !fileName.includes("node_modules") &&
28
+ !fileName.includes("node:internal") &&
29
+ fileName !== "evalmachine.<anonymous>"
30
+ ) {
29
31
  return fileName;
30
32
  }
31
33
  }
@@ -50,12 +52,12 @@ function getCallerFileHash() {
50
52
  try {
51
53
  // Handle file:// URLs by converting to file system path
52
54
  let fsPath = filePath;
53
- if (filePath.startsWith('file://')) {
54
- fsPath = filePath.replace('file://', '');
55
+ if (filePath.startsWith("file://")) {
56
+ fsPath = filePath.replace("file://", "");
55
57
  }
56
-
57
- const fileContent = fs.readFileSync(fsPath, 'utf-8');
58
- const hash = crypto.createHash('sha256').update(fileContent).digest('hex');
58
+
59
+ const fileContent = fs.readFileSync(fsPath, "utf-8");
60
+ const hash = crypto.createHash("sha256").update(fileContent).digest("hex");
59
61
  // Return first 16 chars of hash for brevity
60
62
  return hash.substring(0, 16);
61
63
  } catch (error) {
@@ -298,7 +300,7 @@ class AIError extends Error {
298
300
  this.message += `\nTries: ${this.tries}/${this.maxTries}`;
299
301
  this.message += `\nDuration: ${this.duration}ms`;
300
302
  this.message += `\nTimestamp: ${this.timestamp}`;
301
-
303
+
302
304
  if (this.cause) {
303
305
  this.message += `\nUnderlying error: ${this.cause.message}`;
304
306
  }
@@ -349,17 +351,21 @@ class Element {
349
351
  // Include response metadata if available
350
352
  if (this._response) {
351
353
  result.cache = {
352
- hit: this._response.cacheHit || this._response.cache_hit || this._response.cached || false,
354
+ hit:
355
+ this._response.cacheHit ||
356
+ this._response.cache_hit ||
357
+ this._response.cached ||
358
+ false,
353
359
  strategy: this._response.cacheStrategy,
354
360
  createdAt: this._response.cacheCreatedAt,
355
361
  diffPercent: this._response.cacheDiffPercent,
356
362
  imageUrl: this._response.cachedImageUrl,
357
363
  };
358
-
364
+
359
365
  result.similarity = this._response.similarity;
360
366
  result.confidence = this._response.confidence;
361
367
  result.selector = this._response.selector;
362
-
368
+
363
369
  // Include AI response text if available
364
370
  if (this._response.response?.content?.[0]?.text) {
365
371
  result.aiResponse = this._response.response.content[0].text;
@@ -378,7 +384,7 @@ class Element {
378
384
  */
379
385
  async find(newDescription, options) {
380
386
  // Handle timeout/polling option
381
- const timeout = typeof options === 'object' ? options?.timeout : null;
387
+ const timeout = typeof options === "object" ? options?.timeout : null;
382
388
  if (timeout && timeout > 0) {
383
389
  return this._findWithTimeout(newDescription, options, timeout);
384
390
  }
@@ -414,11 +420,11 @@ class Element {
414
420
  let cacheKey = null;
415
421
  let cacheThreshold = null;
416
422
  let zoom = false; // Default to disabled, enable with zoom: true
417
-
418
- if (typeof options === 'number') {
423
+
424
+ if (typeof options === "number") {
419
425
  // Legacy: options is just a number threshold
420
426
  cacheThreshold = options;
421
- } else if (typeof options === 'object' && options !== null) {
427
+ } else if (typeof options === "object" && options !== null) {
422
428
  // New: options is an object with cacheKey and/or cacheThreshold
423
429
  cacheKey = options.cacheKey || null;
424
430
  cacheThreshold = options.cacheThreshold ?? null;
@@ -428,11 +434,15 @@ class Element {
428
434
 
429
435
  // Use default cacheKey from SDK constructor if not provided in find() options
430
436
  // BUT only if cache is not explicitly disabled via cache: false option
431
- if (!cacheKey && this.sdk.options?.cacheKey && !this.sdk._cacheExplicitlyDisabled) {
437
+ if (
438
+ !cacheKey &&
439
+ this.sdk.options?.cacheKey &&
440
+ !this.sdk._cacheExplicitlyDisabled
441
+ ) {
432
442
  cacheKey = this.sdk.options.cacheKey;
433
443
  }
434
444
 
435
- // Determine threshold:
445
+ // Determine threshold:
436
446
  // - If cache is explicitly disabled, don't use cache even with cacheKey
437
447
  // - If cacheKey is provided, enable cache with threshold
438
448
  // - If no cacheKey, disable cache
@@ -458,9 +468,11 @@ class Element {
458
468
  // Debug log threshold
459
469
  if (debugMode) {
460
470
  const { events } = require("./agent/events.js");
461
- const autoGenMsg = (this.sdk._autoGeneratedCacheKey && cacheKey === this.sdk.options.cacheKey)
462
- ? ' (auto-generated from file hash)'
463
- : '';
471
+ const autoGenMsg =
472
+ this.sdk._autoGeneratedCacheKey &&
473
+ cacheKey === this.sdk.options.cacheKey
474
+ ? " (auto-generated from file hash)"
475
+ : "";
464
476
  this.sdk.emitter.emit(
465
477
  events.log.debug,
466
478
  `🔍 find() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"}${cacheKey ? `, cacheKey: ${cacheKey}${autoGenMsg}` : ""})`,
@@ -492,7 +504,7 @@ class Element {
492
504
  this._response = this._sanitizeResponse(response);
493
505
  this._found = false;
494
506
  findError = "Element not found";
495
-
507
+
496
508
  // Log not found
497
509
  const duration = Date.now() - startTime;
498
510
  const { events } = require("./agent/events.js");
@@ -508,7 +520,7 @@ class Element {
508
520
  this._found = false;
509
521
  findError = error.message;
510
522
  response = error.response;
511
-
523
+
512
524
  // Log not found with error
513
525
  const duration = Date.now() - startTime;
514
526
  const { events } = require("./agent/events.js");
@@ -517,27 +529,33 @@ class Element {
517
529
  error: error.message,
518
530
  });
519
531
  this.sdk.emitter.emit(events.log.log, notFoundMessage);
520
-
532
+
521
533
  console.error("Error during find():", error);
522
534
  }
523
535
 
524
536
  // Track find interaction once at the end (fire-and-forget, don't block)
525
537
  const sessionId = this.sdk.getSessionId();
526
538
  if (sessionId && this.sdk.sandbox?.send) {
527
- await this.sdk.sandbox.send({
528
- type: "trackInteraction",
529
- interactionType: "find",
530
- session: sessionId,
531
- prompt: description,
532
- timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
533
- success: this._found,
534
- error: findError,
535
- cacheHit: response?.cacheHit || response?.cache_hit || response?.cached || false,
536
- selector: response?.selector,
537
- selectorUsed: !!response?.selector,
538
- }).catch(err => {
539
- console.warn("Failed to track find interaction:", err.message);
540
- });
539
+ await this.sdk.sandbox
540
+ .send({
541
+ type: "trackInteraction",
542
+ interactionType: "find",
543
+ session: sessionId,
544
+ prompt: description,
545
+ timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
546
+ success: this._found,
547
+ error: findError,
548
+ cacheHit:
549
+ response?.cacheHit ||
550
+ response?.cache_hit ||
551
+ response?.cached ||
552
+ false,
553
+ selector: response?.selector,
554
+ selectorUsed: !!response?.selector,
555
+ })
556
+ .catch((err) => {
557
+ console.warn("Failed to track find interaction:", err.message);
558
+ });
541
559
  }
542
560
 
543
561
  return this;
@@ -555,46 +573,58 @@ class Element {
555
573
  const POLL_INTERVAL = 5000; // 5 seconds between attempts
556
574
  const startTime = Date.now();
557
575
  const description = newDescription || this.description;
558
-
576
+
559
577
  // Log that we're starting a polling find
560
578
  const { events } = require("./agent/events.js");
561
- this.sdk.emitter.emit(events.log.log, `🔄 Polling for "${description}" (timeout: ${timeout}ms)`);
562
-
579
+ this.sdk.emitter.emit(
580
+ events.log.log,
581
+ `🔄 Polling for "${description}" (timeout: ${timeout}ms)`,
582
+ );
583
+
563
584
  // Create options without timeout to avoid infinite recursion
564
- const findOptions = typeof options === 'object' ? { ...options } : {};
585
+ const findOptions = typeof options === "object" ? { ...options } : {};
565
586
  delete findOptions.timeout;
566
-
587
+
567
588
  let attempts = 0;
568
589
  while (Date.now() - startTime < timeout) {
569
590
  attempts++;
570
-
591
+
571
592
  // Call the regular find (without timeout option)
572
593
  await this.find(newDescription, findOptions);
573
-
594
+
574
595
  if (this._found) {
575
- this.sdk.emitter.emit(events.log.log, `✅ Found "${description}" after ${attempts} attempt(s)`);
596
+ this.sdk.emitter.emit(
597
+ events.log.log,
598
+ `✅ Found "${description}" after ${attempts} attempt(s)`,
599
+ );
576
600
  return this;
577
601
  }
578
-
602
+
579
603
  const elapsed = Date.now() - startTime;
580
604
  const remaining = timeout - elapsed;
581
-
605
+
582
606
  if (remaining > POLL_INTERVAL) {
583
- this.sdk.emitter.emit(events.log.log, `⏳ Element not found, retrying in 5s... (${Math.round(remaining / 1000)}s remaining)`);
584
- await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
607
+ this.sdk.emitter.emit(
608
+ events.log.log,
609
+ `⏳ Element not found, retrying in 5s... (${Math.round(remaining / 1000)}s remaining)`,
610
+ );
611
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
585
612
  } else if (remaining > 0) {
586
613
  // Less than 5s remaining, wait the remaining time and try once more
587
- await new Promise(resolve => setTimeout(resolve, remaining));
614
+ await new Promise((resolve) => setTimeout(resolve, remaining));
588
615
  }
589
616
  }
590
-
617
+
591
618
  // Final attempt after timeout
592
619
  await this.find(newDescription, findOptions);
593
-
620
+
594
621
  if (!this._found) {
595
- this.sdk.emitter.emit(events.log.log, `❌ Element "${description}" not found after ${timeout}ms (${attempts} attempts)`);
622
+ this.sdk.emitter.emit(
623
+ events.log.log,
624
+ `❌ Element "${description}" not found after ${timeout}ms (${attempts} attempts)`,
625
+ );
596
626
  }
597
-
627
+
598
628
  return this;
599
629
  }
600
630
 
@@ -881,13 +911,22 @@ class Element {
881
911
  edgeDetectedImageUrl: this._response?.edgeSavedImagePath || null,
882
912
  cacheHit: this._response?.cacheHit,
883
913
  selectorUsed: !!this._response?.selector,
884
- selector: this._response?.selector
914
+ selector: this._response?.selector,
885
915
  };
886
916
 
887
917
  if (action === "hover") {
888
- await this.commands.hover(this.coordinates.x, this.coordinates.y, elementData);
918
+ await this.commands.hover(
919
+ this.coordinates.x,
920
+ this.coordinates.y,
921
+ elementData,
922
+ );
889
923
  } else {
890
- await this.commands.click(this.coordinates.x, this.coordinates.y, action, elementData);
924
+ await this.commands.click(
925
+ this.coordinates.x,
926
+ this.coordinates.y,
927
+ action,
928
+ elementData,
929
+ );
891
930
  }
892
931
  }
893
932
 
@@ -921,10 +960,14 @@ class Element {
921
960
  edgeDetectedImageUrl: this._response?.edgeSavedImagePath || null,
922
961
  cacheHit: this._response?.cacheHit,
923
962
  selectorUsed: !!this._response?.selector,
924
- selector: this._response?.selector
963
+ selector: this._response?.selector,
925
964
  };
926
965
 
927
- await this.commands.hover(this.coordinates.x, this.coordinates.y, elementData);
966
+ await this.commands.hover(
967
+ this.coordinates.x,
968
+ this.coordinates.y,
969
+ elementData,
970
+ );
928
971
  }
929
972
 
930
973
  /**
@@ -1119,54 +1162,69 @@ class Element {
1119
1162
  /**
1120
1163
  * Creates a chainable promise that allows method chaining on find() results
1121
1164
  * This enables syntax like: await testdriver.find("button").click()
1122
- *
1165
+ *
1123
1166
  * @param {Promise<Element>} promise - The promise that resolves to an Element
1124
1167
  * @returns {Promise<Element> & ChainableElement} A promise with chainable element methods
1125
1168
  */
1126
1169
  function createChainablePromise(promise) {
1127
1170
  // Define the chainable methods that should be available
1128
- const chainableMethods = ['click', 'hover', 'doubleClick', 'rightClick', 'mouseDown', 'mouseUp'];
1129
-
1171
+ const chainableMethods = [
1172
+ "click",
1173
+ "hover",
1174
+ "doubleClick",
1175
+ "rightClick",
1176
+ "mouseDown",
1177
+ "mouseUp",
1178
+ ];
1179
+
1130
1180
  // Create a new promise that wraps the original
1131
- const chainablePromise = promise.then(element => element);
1132
-
1181
+ const chainablePromise = promise.then((element) => element);
1182
+
1133
1183
  // Add chainable methods to the promise
1134
1184
  for (const method of chainableMethods) {
1135
- chainablePromise[method] = function(...args) {
1185
+ chainablePromise[method] = function (...args) {
1136
1186
  // Return a promise that waits for the element, then calls the method
1137
- return promise.then(element => element[method](...args));
1187
+ return promise.then((element) => element[method](...args));
1138
1188
  };
1139
1189
  }
1140
-
1190
+
1141
1191
  // Add getters for element properties (these return promises)
1142
- Object.defineProperty(chainablePromise, 'x', {
1143
- get() { return promise.then(el => el.x); }
1192
+ Object.defineProperty(chainablePromise, "x", {
1193
+ get() {
1194
+ return promise.then((el) => el.x);
1195
+ },
1144
1196
  });
1145
- Object.defineProperty(chainablePromise, 'y', {
1146
- get() { return promise.then(el => el.y); }
1197
+ Object.defineProperty(chainablePromise, "y", {
1198
+ get() {
1199
+ return promise.then((el) => el.y);
1200
+ },
1147
1201
  });
1148
- Object.defineProperty(chainablePromise, 'centerX', {
1149
- get() { return promise.then(el => el.centerX); }
1202
+ Object.defineProperty(chainablePromise, "centerX", {
1203
+ get() {
1204
+ return promise.then((el) => el.centerX);
1205
+ },
1150
1206
  });
1151
- Object.defineProperty(chainablePromise, 'centerY', {
1152
- get() { return promise.then(el => el.centerY); }
1207
+ Object.defineProperty(chainablePromise, "centerY", {
1208
+ get() {
1209
+ return promise.then((el) => el.centerY);
1210
+ },
1153
1211
  });
1154
-
1212
+
1155
1213
  // Add found() method
1156
- chainablePromise.found = function() {
1157
- return promise.then(el => el.found());
1214
+ chainablePromise.found = function () {
1215
+ return promise.then((el) => el.found());
1158
1216
  };
1159
-
1217
+
1160
1218
  // Add getCoordinates() method
1161
- chainablePromise.getCoordinates = function() {
1162
- return promise.then(el => el.getCoordinates());
1219
+ chainablePromise.getCoordinates = function () {
1220
+ return promise.then((el) => el.getCoordinates());
1163
1221
  };
1164
-
1222
+
1165
1223
  // Add getResponse() method
1166
- chainablePromise.getResponse = function() {
1167
- return promise.then(el => el.getResponse());
1224
+ chainablePromise.getResponse = function () {
1225
+ return promise.then((el) => el.getResponse());
1168
1226
  };
1169
-
1227
+
1170
1228
  return chainablePromise;
1171
1229
  }
1172
1230
 
@@ -1256,7 +1314,8 @@ class TestDriverSDK {
1256
1314
  this.sandboxInstance = options.sandboxInstance || null;
1257
1315
 
1258
1316
  // Store reconnect preference from options
1259
- this.reconnect = options.reconnect !== undefined ? options.reconnect : false;
1317
+ this.reconnect =
1318
+ options.reconnect !== undefined ? options.reconnect : false;
1260
1319
 
1261
1320
  // Store dashcam preference (default: true)
1262
1321
  this.dashcamEnabled = options.dashcam !== false;
@@ -1281,7 +1340,7 @@ class TestDriverSDK {
1281
1340
  } else {
1282
1341
  // Cache enabled by default when cacheKey is provided
1283
1342
  this.cacheThresholds = {
1284
- find: options.cacheThreshold?.find ?? 0.01, // Default: 1% threshold
1343
+ find: options.cacheThreshold?.find ?? 0.01, // Default: 1% threshold
1285
1344
  findAll: options.cacheThreshold?.findAll ?? 0.01,
1286
1345
  };
1287
1346
  }
@@ -1293,14 +1352,16 @@ class TestDriverSDK {
1293
1352
  // The `redraw` option takes precedence and matches the per-command API
1294
1353
  if (options.redraw !== undefined) {
1295
1354
  // New unified API: redraw object (matches per-command options)
1296
- this.redrawOptions = typeof options.redraw === 'object'
1297
- ? options.redraw
1298
- : { enabled: options.redraw }; // Support redraw: false as shorthand
1355
+ this.redrawOptions =
1356
+ typeof options.redraw === "object"
1357
+ ? options.redraw
1358
+ : { enabled: options.redraw }; // Support redraw: false as shorthand
1299
1359
  } else if (options.redrawThreshold !== undefined) {
1300
1360
  // Legacy API: redrawThreshold number or object
1301
- this.redrawOptions = typeof options.redrawThreshold === 'object'
1302
- ? options.redrawThreshold
1303
- : { diffThreshold: options.redrawThreshold };
1361
+ this.redrawOptions =
1362
+ typeof options.redrawThreshold === "object"
1363
+ ? options.redrawThreshold
1364
+ : { diffThreshold: options.redrawThreshold };
1304
1365
  } else {
1305
1366
  // Default: enabled (as of v7.2)
1306
1367
  this.redrawOptions = { enabled: true };
@@ -1354,7 +1415,7 @@ class TestDriverSDK {
1354
1415
  await this.__connectionPromise;
1355
1416
  }
1356
1417
  if (!this.connected) {
1357
- throw new Error('Not connected to sandbox. Call connect() first.');
1418
+ throw new Error("Not connected to sandbox. Call connect() first.");
1358
1419
  }
1359
1420
  }
1360
1421
 
@@ -1411,18 +1472,18 @@ class TestDriverSDK {
1411
1472
  * @private
1412
1473
  */
1413
1474
  async _getDashcamChromeExtensionPath() {
1414
- if (this.os !== 'windows') {
1415
- return '/usr/lib/node_modules/dashcam-chrome/build';
1475
+ if (this.os !== "windows") {
1476
+ return "/usr/lib/node_modules/dashcam-chrome/build";
1416
1477
  }
1417
1478
 
1418
1479
  // dashcam-chrome is preinstalled on Windows at C:\Program Files\nodejs\node_modules\dashcam-chrome\build
1419
1480
  // Use the actual long path - we'll handle quoting in the chrome launch
1420
- return 'C:\\PROGRA~1\\nodejs\\node_modules\\dashcam-chrome\\build';
1481
+ return "C:\\PROGRA~1\\nodejs\\node_modules\\dashcam-chrome\\build";
1421
1482
  }
1422
1483
 
1423
1484
  _createProvisionAPI() {
1424
1485
  const self = this;
1425
-
1486
+
1426
1487
  const provisionMethods = {
1427
1488
  /**
1428
1489
  * Launch Chrome browser
@@ -1434,7 +1495,7 @@ class TestDriverSDK {
1434
1495
  */
1435
1496
  chrome: async (options = {}) => {
1436
1497
  const {
1437
- url = 'http://testdriver-sandbox.vercel.app/',
1498
+ url = "http://testdriver-sandbox.vercel.app/",
1438
1499
  maximized = true,
1439
1500
  guest = false,
1440
1501
  } = options;
@@ -1442,69 +1503,84 @@ class TestDriverSDK {
1442
1503
  // If dashcam is available, add web logs for all websites
1443
1504
  // Note: File log and dashcam.start() are handled by the connection promise in hooks.mjs
1444
1505
  if (this._dashcam) {
1445
- await this._dashcam.addWebLog('**', 'Web Logs');
1506
+ await this._dashcam.addWebLog("**", "Web Logs");
1446
1507
  }
1447
1508
 
1448
1509
  // Set up Chrome profile with preferences
1449
- const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1450
- const userDataDir = this.os === 'windows'
1451
- ? 'C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome'
1452
- : '/tmp/testdriver-chrome-profile';
1453
-
1510
+ const shell = this.os === "windows" ? "pwsh" : "sh";
1511
+ const userDataDir =
1512
+ this.os === "windows"
1513
+ ? "C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome"
1514
+ : "/tmp/testdriver-chrome-profile";
1515
+
1454
1516
  // Create user data directory and Default profile directory
1455
- const defaultProfileDir = this.os === 'windows'
1456
- ? `${userDataDir}\\Default`
1457
- : `${userDataDir}/Default`;
1458
-
1459
- const createDirCmd = this.os === 'windows'
1460
- ? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
1461
- : `mkdir -p "${defaultProfileDir}"`;
1462
-
1517
+ const defaultProfileDir =
1518
+ this.os === "windows"
1519
+ ? `${userDataDir}\\Default`
1520
+ : `${userDataDir}/Default`;
1521
+
1522
+ const createDirCmd =
1523
+ this.os === "windows"
1524
+ ? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
1525
+ : `mkdir -p "${defaultProfileDir}"`;
1526
+
1463
1527
  await this.exec(shell, createDirCmd, 60000, true);
1464
-
1528
+
1465
1529
  // Write Chrome preferences
1466
1530
  const chromePrefs = {
1467
1531
  credentials_enable_service: false,
1468
1532
  profile: {
1469
1533
  password_manager_enabled: false,
1470
- default_content_setting_values: {}
1534
+ default_content_setting_values: {},
1471
1535
  },
1472
1536
  signin: {
1473
- allowed: false
1537
+ allowed: false,
1474
1538
  },
1475
1539
  sync: {
1476
1540
  requested: false,
1477
1541
  first_setup_complete: true,
1478
- sync_all_os_types: false
1542
+ sync_all_os_types: false,
1479
1543
  },
1480
1544
  autofill: {
1481
- enabled: false
1545
+ enabled: false,
1482
1546
  },
1483
1547
  local_state: {
1484
1548
  browser: {
1485
- has_seen_welcome_page: true
1486
- }
1487
- }
1549
+ has_seen_welcome_page: true,
1550
+ },
1551
+ },
1488
1552
  };
1489
-
1490
- const prefsPath = this.os === 'windows'
1491
- ? `${defaultProfileDir}\\Preferences`
1492
- : `${defaultProfileDir}/Preferences`;
1493
-
1553
+
1554
+ const prefsPath =
1555
+ this.os === "windows"
1556
+ ? `${defaultProfileDir}\\Preferences`
1557
+ : `${defaultProfileDir}/Preferences`;
1558
+
1494
1559
  const prefsJson = JSON.stringify(chromePrefs, null, 2);
1495
- const writePrefCmd = this.os === 'windows'
1496
- // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
1497
- ? `[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
1498
- : `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
1499
-
1560
+ const writePrefCmd =
1561
+ this.os === "windows"
1562
+ ? // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
1563
+ `[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
1564
+ : `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
1565
+
1500
1566
  await this.exec(shell, writePrefCmd, 60000, true);
1501
1567
 
1502
1568
  // Build Chrome launch command
1503
1569
  const chromeArgs = [];
1504
- if (maximized) chromeArgs.push('--start-maximized');
1505
- if (guest) chromeArgs.push('--guest');
1506
- chromeArgs.push('--disable-fre', '--no-default-browser-check', '--no-first-run', '--no-experiments', '--disable-infobars', `--user-data-dir=${userDataDir}`);
1507
-
1570
+ if (maximized) chromeArgs.push("--start-maximized");
1571
+ if (guest) chromeArgs.push("--guest");
1572
+ chromeArgs.push(
1573
+ "--disable-fre",
1574
+ "--no-default-browser-check",
1575
+ "--no-first-run",
1576
+ "--no-experiments",
1577
+ "--disable-infobars",
1578
+ `--user-data-dir=${userDataDir}`,
1579
+ );
1580
+
1581
+ // Add remote debugging port for captcha solving support
1582
+ chromeArgs.push("--remote-debugging-port=9222");
1583
+
1508
1584
  // Add dashcam-chrome extension
1509
1585
  const dashcamChromePath = await this._getDashcamChromeExtensionPath();
1510
1586
  if (dashcamChromePath) {
@@ -1512,44 +1588,47 @@ class TestDriverSDK {
1512
1588
  }
1513
1589
 
1514
1590
  // Launch Chrome
1515
-
1516
- if (this.os === 'windows') {
1517
- const argsString = chromeArgs.map(arg => `"${arg}"`).join(', ');
1591
+
1592
+ if (this.os === "windows") {
1593
+ const argsString = chromeArgs.map((arg) => `"${arg}"`).join(", ");
1518
1594
  await this.exec(
1519
1595
  shell,
1520
1596
  `Start-Process "C:\\ChromeForTesting\\chrome-win64\\chrome.exe" -ArgumentList ${argsString}, "${url}"`,
1521
- 30000
1597
+ 30000,
1522
1598
  );
1523
1599
  } else {
1524
- const argsString = chromeArgs.join(' ');
1600
+ const argsString = chromeArgs.join(" ");
1525
1601
  await this.exec(
1526
1602
  shell,
1527
1603
  `chrome-for-testing ${argsString} "${url}" >/dev/null 2>&1 &`,
1528
- 30000
1604
+ 30000,
1529
1605
  );
1530
1606
  }
1531
1607
 
1532
1608
  // Wait for Chrome to be ready
1533
- await this.focusApplication('Google Chrome');
1609
+ await this.focusApplication("Google Chrome");
1534
1610
 
1535
1611
  // Wait for URL to load
1536
1612
  try {
1537
1613
  const urlObj = new URL(url);
1538
1614
  const domain = urlObj.hostname;
1539
-
1615
+
1540
1616
  for (let attempt = 0; attempt < 30; attempt++) {
1541
1617
  const result = await this.find(`${domain}`);
1542
1618
 
1543
1619
  if (result.found()) {
1544
1620
  break;
1545
1621
  } else {
1546
- await new Promise(resolve => setTimeout(resolve, 1000));
1622
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1547
1623
  }
1548
1624
  }
1549
-
1550
- await this.focusApplication('Google Chrome');
1625
+
1626
+ await this.focusApplication("Google Chrome");
1551
1627
  } catch (e) {
1552
- console.warn(`[provision.chrome] ⚠️ Could not parse URL "${url}":`, e.message);
1628
+ console.warn(
1629
+ `[provision.chrome] ⚠️ Could not parse URL "${url}":`,
1630
+ e.message,
1631
+ );
1553
1632
  }
1554
1633
  },
1555
1634
 
@@ -1566,7 +1645,7 @@ class TestDriverSDK {
1566
1645
  * await testdriver.provision.chromeExtension({
1567
1646
  * extensionPath: '/tmp/extension'
1568
1647
  * });
1569
- *
1648
+ *
1570
1649
  * @example
1571
1650
  * // Load extension by Chrome Web Store ID
1572
1651
  * await testdriver.provision.chromeExtension({
@@ -1581,55 +1660,62 @@ class TestDriverSDK {
1581
1660
  } = options;
1582
1661
 
1583
1662
  if (!providedExtensionPath && !extensionId) {
1584
- throw new Error('[provision.chromeExtension] Either extensionPath or extensionId is required');
1663
+ throw new Error(
1664
+ "[provision.chromeExtension] Either extensionPath or extensionId is required",
1665
+ );
1585
1666
  }
1586
1667
 
1587
1668
  let extensionPath = providedExtensionPath;
1588
- const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1669
+ const shell = this.os === "windows" ? "pwsh" : "sh";
1589
1670
 
1590
1671
  // If extensionId is provided, download and extract the extension from Chrome Web Store
1591
1672
  if (extensionId && !extensionPath) {
1592
- console.log(`[provision.chromeExtension] Downloading extension ${extensionId} from Chrome Web Store...`);
1593
-
1594
- const extensionDir = this.os === 'windows'
1595
- ? `C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Extensions\\${extensionId}`
1596
- : `/tmp/testdriver-extensions/${extensionId}`;
1597
-
1673
+ console.log(
1674
+ `[provision.chromeExtension] Downloading extension ${extensionId} from Chrome Web Store...`,
1675
+ );
1676
+
1677
+ const extensionDir =
1678
+ this.os === "windows"
1679
+ ? `C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Extensions\\${extensionId}`
1680
+ : `/tmp/testdriver-extensions/${extensionId}`;
1681
+
1598
1682
  // Create extension directory
1599
- const mkdirCmd = this.os === 'windows'
1600
- ? `New-Item -ItemType Directory -Path "${extensionDir}" -Force | Out-Null`
1601
- : `mkdir -p "${extensionDir}"`;
1683
+ const mkdirCmd =
1684
+ this.os === "windows"
1685
+ ? `New-Item -ItemType Directory -Path "${extensionDir}" -Force | Out-Null`
1686
+ : `mkdir -p "${extensionDir}"`;
1602
1687
  await this.exec(shell, mkdirCmd, 60000, true);
1603
-
1688
+
1604
1689
  // Download CRX from Chrome Web Store
1605
1690
  // The CRX download URL format for Chrome Web Store
1606
1691
  const crxUrl = `https://clients2.google.com/service/update2/crx?response=redirect&prodversion=131.0.0.0&acceptformat=crx2,crx3&x=id%3D${extensionId}%26installsource%3Dondemand%26uc`;
1607
- const crxPath = this.os === 'windows'
1608
- ? `${extensionDir}\\extension.crx`
1609
- : `${extensionDir}/extension.crx`;
1610
-
1611
- if (this.os === 'windows') {
1692
+ const crxPath =
1693
+ this.os === "windows"
1694
+ ? `${extensionDir}\\extension.crx`
1695
+ : `${extensionDir}/extension.crx`;
1696
+
1697
+ if (this.os === "windows") {
1612
1698
  await this.exec(
1613
- 'pwsh',
1699
+ "pwsh",
1614
1700
  `Invoke-WebRequest -Uri "${crxUrl}" -OutFile "${crxPath}"`,
1615
1701
  60000,
1616
- true
1702
+ true,
1617
1703
  );
1618
1704
  } else {
1619
1705
  await this.exec(
1620
- 'sh',
1706
+ "sh",
1621
1707
  `curl -L -o "${crxPath}" "${crxUrl}"`,
1622
1708
  60000,
1623
- true
1709
+ true,
1624
1710
  );
1625
1711
  }
1626
-
1712
+
1627
1713
  // Extract the CRX file (CRX is a ZIP with a header)
1628
1714
  // Skip the CRX header and extract as ZIP
1629
- if (this.os === 'windows') {
1715
+ if (this.os === "windows") {
1630
1716
  // PowerShell: Read CRX, skip header, extract ZIP
1631
1717
  await this.exec(
1632
- 'pwsh',
1718
+ "pwsh",
1633
1719
  `
1634
1720
  $crxBytes = [System.IO.File]::ReadAllBytes("${crxPath}")
1635
1721
  # CRX3 header: 4 bytes magic + 4 bytes version + 4 bytes header length + header
@@ -1647,13 +1733,13 @@ $zipPath = "${extensionDir}\\extension.zip"
1647
1733
  Expand-Archive -Path $zipPath -DestinationPath "${extensionDir}\\unpacked" -Force
1648
1734
  `,
1649
1735
  30000,
1650
- true
1736
+ true,
1651
1737
  );
1652
1738
  extensionPath = `${extensionDir}\\unpacked`;
1653
1739
  } else {
1654
1740
  // Linux: Use unzip with offset or python to extract
1655
1741
  await this.exec(
1656
- 'sh',
1742
+ "sh",
1657
1743
  `
1658
1744
  cd "${extensionDir}"
1659
1745
  # Extract CRX (skip header and unzip)
@@ -1686,120 +1772,140 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1686
1772
  "
1687
1773
  `,
1688
1774
  30000,
1689
- true
1775
+ true,
1690
1776
  );
1691
1777
  extensionPath = `${extensionDir}/unpacked`;
1692
1778
  }
1693
-
1694
- console.log(`[provision.chromeExtension] Extension ${extensionId} extracted to ${extensionPath}`);
1779
+
1780
+ console.log(
1781
+ `[provision.chromeExtension] Extension ${extensionId} extracted to ${extensionPath}`,
1782
+ );
1695
1783
  }
1696
1784
 
1697
1785
  // If dashcam is available, add web logs for all websites
1698
1786
  // Note: File log and dashcam.start() are handled by the connection promise in hooks.mjs
1699
1787
  if (this._dashcam) {
1700
- await this._dashcam.addWebLog('**', 'Web Logs');
1788
+ await this._dashcam.addWebLog("**", "Web Logs");
1701
1789
  }
1702
1790
 
1703
1791
  // Set up Chrome profile with preferences
1704
- const userDataDir = this.os === 'windows'
1705
- ? 'C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome'
1706
- : '/tmp/testdriver-chrome-profile';
1707
-
1792
+ const userDataDir =
1793
+ this.os === "windows"
1794
+ ? "C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome"
1795
+ : "/tmp/testdriver-chrome-profile";
1796
+
1708
1797
  // Create user data directory and Default profile directory
1709
- const defaultProfileDir = this.os === 'windows'
1710
- ? `${userDataDir}\\Default`
1711
- : `${userDataDir}/Default`;
1712
-
1713
- const createDirCmd = this.os === 'windows'
1714
- ? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
1715
- : `mkdir -p "${defaultProfileDir}"`;
1716
-
1798
+ const defaultProfileDir =
1799
+ this.os === "windows"
1800
+ ? `${userDataDir}\\Default`
1801
+ : `${userDataDir}/Default`;
1802
+
1803
+ const createDirCmd =
1804
+ this.os === "windows"
1805
+ ? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
1806
+ : `mkdir -p "${defaultProfileDir}"`;
1807
+
1717
1808
  await this.exec(shell, createDirCmd, 60000, true);
1718
-
1809
+
1719
1810
  // Write Chrome preferences
1720
1811
  const chromePrefs = {
1721
1812
  credentials_enable_service: false,
1722
1813
  profile: {
1723
1814
  password_manager_enabled: false,
1724
- default_content_setting_values: {}
1815
+ default_content_setting_values: {},
1725
1816
  },
1726
1817
  signin: {
1727
- allowed: false
1818
+ allowed: false,
1728
1819
  },
1729
1820
  sync: {
1730
1821
  requested: false,
1731
1822
  first_setup_complete: true,
1732
- sync_all_os_types: false
1823
+ sync_all_os_types: false,
1733
1824
  },
1734
1825
  autofill: {
1735
- enabled: false
1826
+ enabled: false,
1736
1827
  },
1737
1828
  local_state: {
1738
1829
  browser: {
1739
- has_seen_welcome_page: true
1740
- }
1741
- }
1830
+ has_seen_welcome_page: true,
1831
+ },
1832
+ },
1742
1833
  };
1743
-
1744
- const prefsPath = this.os === 'windows'
1745
- ? `${defaultProfileDir}\\Preferences`
1746
- : `${defaultProfileDir}/Preferences`;
1747
-
1834
+
1835
+ const prefsPath =
1836
+ this.os === "windows"
1837
+ ? `${defaultProfileDir}\\Preferences`
1838
+ : `${defaultProfileDir}/Preferences`;
1839
+
1748
1840
  const prefsJson = JSON.stringify(chromePrefs, null, 2);
1749
- const writePrefCmd = this.os === 'windows'
1750
- // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
1751
- ? `[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
1752
- : `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
1753
-
1841
+ const writePrefCmd =
1842
+ this.os === "windows"
1843
+ ? // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
1844
+ `[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
1845
+ : `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
1846
+
1754
1847
  await this.exec(shell, writePrefCmd, 60000, true);
1755
1848
 
1756
1849
  // Build Chrome launch command
1757
1850
  const chromeArgs = [];
1758
- if (maximized) chromeArgs.push('--start-maximized');
1759
- chromeArgs.push('--disable-fre', '--no-default-browser-check', '--no-first-run', '--no-experiments', '--disable-infobars', '--disable-features=ChromeLabs', `--user-data-dir=${userDataDir}`);
1760
-
1851
+ if (maximized) chromeArgs.push("--start-maximized");
1852
+ chromeArgs.push(
1853
+ "--disable-fre",
1854
+ "--no-default-browser-check",
1855
+ "--no-first-run",
1856
+ "--no-experiments",
1857
+ "--disable-infobars",
1858
+ "--disable-features=ChromeLabs",
1859
+ `--user-data-dir=${userDataDir}`,
1860
+ );
1861
+
1862
+ // Add remote debugging port for captcha solving support
1863
+ chromeArgs.push("--remote-debugging-port=9222");
1864
+
1761
1865
  // Add user extension and dashcam-chrome extension
1762
1866
  const dashcamChromePath = await this._getDashcamChromeExtensionPath();
1763
1867
  if (dashcamChromePath) {
1764
1868
  // Load both user extension and dashcam-chrome for web log capture
1765
- chromeArgs.push(`--load-extension=${extensionPath},${dashcamChromePath}`);
1869
+ chromeArgs.push(
1870
+ `--load-extension=${extensionPath},${dashcamChromePath}`,
1871
+ );
1766
1872
  } else {
1767
1873
  // If dashcam-chrome unavailable, just load user extension
1768
1874
  chromeArgs.push(`--load-extension=${extensionPath}`);
1769
1875
  }
1770
1876
 
1771
1877
  // Launch Chrome (opens to New Tab by default)
1772
- if (this.os === 'windows') {
1773
- const argsString = chromeArgs.map(arg => `"${arg}"`).join(', ');
1878
+ if (this.os === "windows") {
1879
+ const argsString = chromeArgs.map((arg) => `"${arg}"`).join(", ");
1774
1880
  await this.exec(
1775
1881
  shell,
1776
1882
  `Start-Process "C:\\ChromeForTesting\\chrome-win64\\chrome.exe" -ArgumentList ${argsString}`,
1777
- 30000
1883
+ 30000,
1778
1884
  );
1779
1885
  } else {
1780
- const argsString = chromeArgs.join(' ');
1886
+ const argsString = chromeArgs.join(" ");
1781
1887
  await this.exec(
1782
1888
  shell,
1783
1889
  `chrome-for-testing ${argsString} >/dev/null 2>&1 &`,
1784
- 30000
1890
+ 30000,
1785
1891
  );
1786
1892
  }
1787
1893
 
1788
1894
  // Wait for Chrome to be ready
1789
- await this.focusApplication('Google Chrome');
1895
+ await this.focusApplication("Google Chrome");
1790
1896
 
1791
1897
  // Wait for New Tab to appear
1792
1898
  for (let attempt = 0; attempt < 30; attempt++) {
1793
- const result = await this.find('New Tab');
1899
+ const result = await this.find("New Tab");
1794
1900
 
1795
1901
  if (result.found()) {
1796
1902
  break;
1797
1903
  } else {
1798
- await new Promise(resolve => setTimeout(resolve, 1000));
1904
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1799
1905
  }
1800
1906
  }
1801
-
1802
- await this.focusApplication('Google Chrome');
1907
+
1908
+ await this.focusApplication("Google Chrome");
1803
1909
  },
1804
1910
 
1805
1911
  /**
@@ -1810,17 +1916,14 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1810
1916
  * @returns {Promise<void>}
1811
1917
  */
1812
1918
  vscode: async (options = {}) => {
1813
- const {
1814
- workspace = null,
1815
- extensions = [],
1816
- } = options;
1919
+ const { workspace = null, extensions = [] } = options;
1817
1920
 
1818
- const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1921
+ const shell = this.os === "windows" ? "pwsh" : "sh";
1819
1922
 
1820
1923
  // If dashcam is available, add web logs for all websites
1821
1924
  // Note: File log and dashcam.start() are handled by the connection promise in hooks.mjs
1822
1925
  if (this._dashcam) {
1823
- await this._dashcam.addWebLog('**', 'Web Logs');
1926
+ await this._dashcam.addWebLog("**", "Web Logs");
1824
1927
  }
1825
1928
 
1826
1929
  // Install extensions if provided
@@ -1830,33 +1933,35 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1830
1933
  shell,
1831
1934
  `code --install-extension ${extension} --force`,
1832
1935
  120000,
1833
- true
1936
+ true,
1937
+ );
1938
+ console.log(
1939
+ `[provision.vscode] ✅ Extension installed: ${extension}`,
1834
1940
  );
1835
- console.log(`[provision.vscode] ✅ Extension installed: ${extension}`);
1836
1941
  }
1837
1942
 
1838
1943
  // Launch VS Code
1839
- const workspaceArg = workspace ? `"${workspace}"` : '';
1840
-
1841
- if (this.os === 'windows') {
1944
+ const workspaceArg = workspace ? `"${workspace}"` : "";
1945
+
1946
+ if (this.os === "windows") {
1842
1947
  await this.exec(
1843
1948
  shell,
1844
1949
  `Start-Process code -ArgumentList ${workspaceArg}`,
1845
- 30000
1950
+ 30000,
1846
1951
  );
1847
1952
  } else {
1848
1953
  await this.exec(
1849
1954
  shell,
1850
1955
  `code ${workspaceArg} >/dev/null 2>&1 &`,
1851
- 30000
1956
+ 30000,
1852
1957
  );
1853
1958
  }
1854
1959
 
1855
1960
  // Wait for VS Code to start up
1856
- await new Promise(resolve => setTimeout(resolve, 3000));
1961
+ await new Promise((resolve) => setTimeout(resolve, 3000));
1857
1962
 
1858
1963
  // Wait for VS Code to be ready
1859
- await this.focusApplication('Visual Studio Code');
1964
+ await this.focusApplication("Visual Studio Code");
1860
1965
  },
1861
1966
 
1862
1967
  /**
@@ -1873,7 +1978,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1873
1978
  * url: 'https://example.com/app.deb',
1874
1979
  * appName: 'MyApp'
1875
1980
  * });
1876
- *
1981
+ *
1877
1982
  * @example
1878
1983
  * // Download and run custom commands
1879
1984
  * const filePath = await testdriver.provision.installer({
@@ -1883,39 +1988,33 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1883
1988
  * await testdriver.exec('sh', `chmod +x "${filePath}" && "${filePath}" &`, 10000);
1884
1989
  */
1885
1990
  installer: async (options = {}) => {
1886
- const {
1887
- url,
1888
- filename,
1889
- appName,
1890
- launch = true,
1891
- } = options;
1991
+ const { url, filename, appName, launch = true } = options;
1892
1992
 
1893
1993
  if (!url) {
1894
- throw new Error('[provision.installer] url is required');
1994
+ throw new Error("[provision.installer] url is required");
1895
1995
  }
1896
1996
 
1897
- const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1997
+ const shell = this.os === "windows" ? "pwsh" : "sh";
1898
1998
 
1899
1999
  // If dashcam is available, add web logs for all websites
1900
2000
  // Note: File log and dashcam.start() are handled by the connection promise in hooks.mjs
1901
2001
  if (this._dashcam) {
1902
- await this._dashcam.addWebLog('**', 'Web Logs');
2002
+ await this._dashcam.addWebLog("**", "Web Logs");
1903
2003
  }
1904
2004
 
1905
2005
  // Determine download directory
1906
- const downloadDir = this.os === 'windows'
1907
- ? 'C:\\Users\\testdriver\\Downloads'
1908
- : '/tmp';
2006
+ const downloadDir =
2007
+ this.os === "windows" ? "C:\\Users\\testdriver\\Downloads" : "/tmp";
1909
2008
 
1910
2009
  console.log(`[provision.installer] Downloading ${url}...`);
1911
2010
 
1912
2011
  let actualFilePath;
1913
2012
 
1914
2013
  // Download the file and get the actual filename (handles redirects)
1915
- if (this.os === 'windows') {
2014
+ if (this.os === "windows") {
1916
2015
  // Simple approach: download first, then get the actual filename from the response
1917
2016
  const tempFile = `${downloadDir}\\installer_temp_${Date.now()}`;
1918
-
2017
+
1919
2018
  const downloadScript = `
1920
2019
  $ProgressPreference = 'SilentlyContinue'
1921
2020
  $response = Invoke-WebRequest -Uri "${url}" -OutFile "${tempFile}" -PassThru -UseBasicParsing
@@ -1942,12 +2041,12 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1942
2041
  Move-Item -Path "${tempFile}" -Destination $finalPath -Force
1943
2042
  Write-Output $finalPath
1944
2043
  `;
1945
-
2044
+
1946
2045
  const result = await this.exec(shell, downloadScript, 300000, true);
1947
2046
  actualFilePath = result ? result.trim() : null;
1948
-
2047
+
1949
2048
  if (!actualFilePath) {
1950
- throw new Error('[provision.installer] Failed to download file');
2049
+ throw new Error("[provision.installer] Failed to download file");
1951
2050
  }
1952
2051
  } else {
1953
2052
  // Use curl with options to get the final filename
@@ -1956,21 +2055,21 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1956
2055
  cd "${downloadDir}"
1957
2056
  curl -L -J -O -w "%{filename_effective}" "${url}" 2>/dev/null || echo "${tempMarker}"
1958
2057
  `;
1959
-
2058
+
1960
2059
  const result = await this.exec(shell, downloadScript, 300000, true);
1961
2060
  const downloadedFile = result ? result.trim() : null;
1962
-
2061
+
1963
2062
  if (downloadedFile && downloadedFile !== tempMarker) {
1964
2063
  actualFilePath = `${downloadDir}/${downloadedFile}`;
1965
2064
  } else {
1966
2065
  // Fallback: use curl without -J and specify output file
1967
- const fallbackFilename = filename || 'installer';
2066
+ const fallbackFilename = filename || "installer";
1968
2067
  actualFilePath = `${downloadDir}/${fallbackFilename}`;
1969
2068
  await this.exec(
1970
2069
  shell,
1971
2070
  `curl -L -o "${actualFilePath}" "${url}"`,
1972
2071
  300000,
1973
- true
2072
+ true,
1974
2073
  );
1975
2074
  }
1976
2075
  }
@@ -1978,30 +2077,30 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1978
2077
  console.log(`[provision.installer] ✅ Downloaded to ${actualFilePath}`);
1979
2078
 
1980
2079
  // Auto-detect install command based on file extension (use actualFilePath for extension detection)
1981
- const actualFilename = actualFilePath.split(/[/\\]/).pop() || '';
1982
- const ext = actualFilename.split('.').pop()?.toLowerCase();
2080
+ const actualFilename = actualFilePath.split(/[/\\]/).pop() || "";
2081
+ const ext = actualFilename.split(".").pop()?.toLowerCase();
1983
2082
  let installCommand = null;
1984
-
1985
- if (this.os === 'windows') {
1986
- if (ext === 'msi') {
2083
+
2084
+ if (this.os === "windows") {
2085
+ if (ext === "msi") {
1987
2086
  installCommand = `Start-Process msiexec -ArgumentList '/i', '"${actualFilePath}"', '/quiet', '/norestart' -Wait`;
1988
- } else if (ext === 'exe') {
2087
+ } else if (ext === "exe") {
1989
2088
  installCommand = `Start-Process "${actualFilePath}" -ArgumentList '/S' -Wait`;
1990
2089
  }
1991
- } else if (this.os === 'linux') {
1992
- if (ext === 'deb') {
2090
+ } else if (this.os === "linux") {
2091
+ if (ext === "deb") {
1993
2092
  installCommand = `sudo dpkg -i "${actualFilePath}" && sudo apt-get install -f -y`;
1994
- } else if (ext === 'rpm') {
2093
+ } else if (ext === "rpm") {
1995
2094
  installCommand = `sudo rpm -i "${actualFilePath}"`;
1996
- } else if (ext === 'appimage') {
2095
+ } else if (ext === "appimage") {
1997
2096
  installCommand = `chmod +x "${actualFilePath}"`;
1998
- } else if (ext === 'sh') {
2097
+ } else if (ext === "sh") {
1999
2098
  installCommand = `chmod +x "${actualFilePath}" && "${actualFilePath}"`;
2000
2099
  }
2001
- } else if (this.os === 'darwin') {
2002
- if (ext === 'dmg') {
2100
+ } else if (this.os === "darwin") {
2101
+ if (ext === "dmg") {
2003
2102
  installCommand = `hdiutil attach "${actualFilePath}" -mountpoint /Volumes/installer && cp -R /Volumes/installer/*.app /Applications/ && hdiutil detach /Volumes/installer`;
2004
- } else if (ext === 'pkg') {
2103
+ } else if (ext === "pkg") {
2005
2104
  installCommand = `sudo installer -pkg "${actualFilePath}" -target /`;
2006
2105
  }
2007
2106
  }
@@ -2014,7 +2113,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2014
2113
 
2015
2114
  // Launch and focus the app if appName is provided and launch is true
2016
2115
  if (appName && launch) {
2017
- await new Promise(resolve => setTimeout(resolve, 2000));
2116
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2018
2117
  await this.focusApplication(appName);
2019
2118
  }
2020
2119
 
@@ -2030,36 +2129,36 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2030
2129
  */
2031
2130
  electron: async (options = {}) => {
2032
2131
  const { appPath, args = [] } = options;
2033
-
2132
+
2034
2133
  if (!appPath) {
2035
- throw new Error('provision.electron requires appPath option');
2134
+ throw new Error("provision.electron requires appPath option");
2036
2135
  }
2037
2136
 
2038
- const shell = this.os === 'windows' ? 'pwsh' : 'sh';
2137
+ const shell = this.os === "windows" ? "pwsh" : "sh";
2039
2138
 
2040
2139
  // If dashcam is available, add web logs for all websites
2041
2140
  // Note: File log and dashcam.start() are handled by the connection promise in hooks.mjs
2042
2141
  if (this._dashcam) {
2043
- await this._dashcam.addWebLog('**', 'Web Logs');
2142
+ await this._dashcam.addWebLog("**", "Web Logs");
2044
2143
  }
2045
2144
 
2046
- const argsString = args.join(' ');
2047
-
2048
- if (this.os === 'windows') {
2145
+ const argsString = args.join(" ");
2146
+
2147
+ if (this.os === "windows") {
2049
2148
  await this.exec(
2050
2149
  shell,
2051
2150
  `Start-Process electron -ArgumentList "${appPath}", ${argsString}`,
2052
- 30000
2151
+ 30000,
2053
2152
  );
2054
2153
  } else {
2055
2154
  await this.exec(
2056
2155
  shell,
2057
2156
  `electron "${appPath}" ${argsString} >/dev/null 2>&1 &`,
2058
- 30000
2157
+ 30000,
2059
2158
  );
2060
2159
  }
2061
2160
 
2062
- await this.focusApplication('Electron');
2161
+ await this.focusApplication("Electron");
2063
2162
  },
2064
2163
  };
2065
2164
 
@@ -2067,19 +2166,204 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2067
2166
  return new Proxy(provisionMethods, {
2068
2167
  get(target, prop) {
2069
2168
  const method = target[prop];
2070
- if (typeof method === 'function') {
2169
+ if (typeof method === "function") {
2071
2170
  return async (...args) => {
2072
2171
  // Skip provisioning if reconnecting to existing sandbox
2073
2172
  if (self.reconnect) {
2074
- console.log(`[provision.${prop}] Skipping provisioning (reconnect mode)`);
2173
+ console.log(
2174
+ `[provision.${prop}] Skipping provisioning (reconnect mode)`,
2175
+ );
2075
2176
  return;
2076
2177
  }
2077
2178
  return method(...args);
2078
2179
  };
2079
2180
  }
2080
2181
  return method;
2081
- }
2182
+ },
2183
+ });
2184
+ }
2185
+
2186
+ /**
2187
+ * Solve a captcha on the current page using 2captcha service
2188
+ * Requires Chrome to be launched with remote debugging (--remote-debugging-port=9222)
2189
+ *
2190
+ * @param {Object} options - Captcha solving options
2191
+ * @param {string} options.apiKey - 2captcha API key (required)
2192
+ * @param {string} [options.sitekey] - Captcha sitekey (auto-detected if not provided)
2193
+ * @param {string} [options.type='recaptcha_v3'] - Captcha type: 'recaptcha_v2', 'recaptcha_v3', 'hcaptcha', 'turnstile'
2194
+ * @param {string} [options.action='verify'] - Action parameter for reCAPTCHA v3
2195
+ * @param {boolean} [options.autoSubmit=true] - Automatically click submit button after solving
2196
+ * @param {number} [options.pollInterval=5000] - Polling interval in ms for 2captcha
2197
+ * @param {number} [options.timeout=120000] - Timeout in ms for solving
2198
+ * @returns {Promise<{success: boolean, message: string, token?: string}>}
2199
+ *
2200
+ * @example
2201
+ * // Auto-detect and solve captcha
2202
+ * await testdriver.captcha({
2203
+ * apiKey: 'your-2captcha-api-key'
2204
+ * });
2205
+ *
2206
+ * @example
2207
+ * // Solve with known sitekey
2208
+ * await testdriver.captcha({
2209
+ * apiKey: 'your-2captcha-api-key',
2210
+ * sitekey: '6LfB5_IbAAAAAMCtsjEHEHKqcB9iQocwwxTiihJu',
2211
+ * action: 'demo_action'
2212
+ * });
2213
+ */
2214
+ async captcha(options = {}) {
2215
+ const {
2216
+ apiKey,
2217
+ sitekey,
2218
+ type = "recaptcha_v3",
2219
+ action = "verify",
2220
+ autoSubmit = true,
2221
+ pollInterval = 5000,
2222
+ timeout = 120000,
2223
+ } = options;
2224
+
2225
+ if (!apiKey) {
2226
+ throw new Error(
2227
+ "[captcha] apiKey is required. Get your API key at https://2captcha.com",
2228
+ );
2229
+ }
2230
+
2231
+ const shell = this.os === "windows" ? "pwsh" : "sh";
2232
+ const isWindows = this.os === "windows";
2233
+
2234
+ // Paths for config and solver script
2235
+ const configPath = isWindows
2236
+ ? "C:\\Users\\testdriver\\AppData\\Local\\Temp\\td-captcha-config.json"
2237
+ : "/tmp/td-captcha-config.json";
2238
+ const solverPath = isWindows
2239
+ ? "C:\\Users\\testdriver\\AppData\\Local\\Temp\\td-captcha-solver.js"
2240
+ : "/tmp/td-captcha-solver.js";
2241
+
2242
+ // Ensure chrome-remote-interface is installed
2243
+ if (isWindows) {
2244
+ await this.exec(
2245
+ shell,
2246
+ "npm install -g chrome-remote-interface 2>$null; $true",
2247
+ 60000,
2248
+ true,
2249
+ );
2250
+ } else {
2251
+ await this.exec(
2252
+ shell,
2253
+ "sudo npm install -g chrome-remote-interface 2>/dev/null || npm install -g chrome-remote-interface",
2254
+ 60000,
2255
+ true,
2256
+ );
2257
+ }
2258
+
2259
+ // Build config JSON for the solver
2260
+ const config = JSON.stringify({
2261
+ apiKey,
2262
+ sitekey: sitekey || null,
2263
+ type,
2264
+ action,
2265
+ autoSubmit,
2266
+ pollInterval,
2267
+ timeout,
2082
2268
  });
2269
+
2270
+ // Write config file
2271
+ if (isWindows) {
2272
+ // Use PowerShell's Set-Content with escaped JSON
2273
+ const escapedConfig = config.replace(/'/g, "''");
2274
+ await this.exec(
2275
+ shell,
2276
+ `[System.IO.File]::WriteAllText('${configPath}', '${escapedConfig}')`,
2277
+ 5000,
2278
+ true,
2279
+ );
2280
+ } else {
2281
+ // Use heredoc for Linux
2282
+ await this.exec(
2283
+ shell,
2284
+ `cat > ${configPath} << 'CONFIGEOF'
2285
+ ${config}
2286
+ CONFIGEOF`,
2287
+ 5000,
2288
+ true,
2289
+ );
2290
+ }
2291
+
2292
+ // Load the solver script from file (avoids escaping issues with string concatenation)
2293
+ const solverScriptPath = path.join(
2294
+ __dirname,
2295
+ "lib",
2296
+ "captcha",
2297
+ "solver.js",
2298
+ );
2299
+ const solverScript = fs.readFileSync(solverScriptPath, "utf8");
2300
+
2301
+ // Write the solver script to sandbox
2302
+ if (isWindows) {
2303
+ // For Windows, write the script using base64 encoding to avoid escaping issues
2304
+ const base64Script = Buffer.from(solverScript).toString("base64");
2305
+ await this.exec(
2306
+ shell,
2307
+ `[System.IO.File]::WriteAllBytes('${solverPath}', [System.Convert]::FromBase64String('${base64Script}'))`,
2308
+ 10000,
2309
+ true,
2310
+ );
2311
+ } else {
2312
+ // Use heredoc for Linux
2313
+ await this.exec(
2314
+ shell,
2315
+ `cat > ${solverPath} << 'CAPTCHA_SOLVER_EOF'
2316
+ ${solverScript}
2317
+ CAPTCHA_SOLVER_EOF`,
2318
+ 10000,
2319
+ true,
2320
+ );
2321
+ }
2322
+
2323
+ // Run the solver (capture output even on failure)
2324
+ let result;
2325
+ try {
2326
+ if (isWindows) {
2327
+ // Set environment variable and run node on Windows
2328
+ result = await this.exec(
2329
+ shell,
2330
+ `$env:NODE_PATH = (npm root -g).Trim(); $env:TD_CAPTCHA_CONFIG_PATH='${configPath}'; node '${solverPath}' 2>&1 | Out-String; Write-Output "EXIT_CODE:$LASTEXITCODE"`,
2331
+ timeout + 30000,
2332
+ );
2333
+ } else {
2334
+ result = await this.exec(
2335
+ shell,
2336
+ `NODE_PATH=/usr/lib/node_modules node ${solverPath} 2>&1; echo "EXIT_CODE:$?"`,
2337
+ timeout + 30000,
2338
+ );
2339
+ }
2340
+ } catch (err) {
2341
+ // If exec throws, try to get output from the error
2342
+ result = err.message || err.toString();
2343
+ if (err.responseData && err.responseData.stdout) {
2344
+ result = err.responseData.stdout;
2345
+ }
2346
+ }
2347
+
2348
+ const tokenMatch = result.match(/TOKEN:\s*(\S+)/);
2349
+ const success = result.includes('"success":true');
2350
+ const hasError = result.includes("ERROR:");
2351
+
2352
+ if (hasError && !success) {
2353
+ const errorMatch = result.match(/ERROR:\s*(.+)/);
2354
+ throw new Error(
2355
+ `[captcha] ${errorMatch ? errorMatch[1] : "Unknown error"}\nOutput: ${result}`,
2356
+ );
2357
+ }
2358
+
2359
+ return {
2360
+ success,
2361
+ message: success
2362
+ ? "Captcha solved successfully"
2363
+ : "Captcha solving failed",
2364
+ token: tokenMatch ? tokenMatch[1] : null,
2365
+ output: result,
2366
+ };
2083
2367
  }
2084
2368
 
2085
2369
  /**
@@ -2117,8 +2401,16 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2117
2401
 
2118
2402
  // Clean up screenshots folder for this test file before running
2119
2403
  if (this.testFile) {
2120
- const testFileName = path.basename(this.testFile, path.extname(this.testFile));
2121
- const screenshotsDir = path.join(process.cwd(), ".testdriver", "screenshots", testFileName);
2404
+ const testFileName = path.basename(
2405
+ this.testFile,
2406
+ path.extname(this.testFile),
2407
+ );
2408
+ const screenshotsDir = path.join(
2409
+ process.cwd(),
2410
+ ".testdriver",
2411
+ "screenshots",
2412
+ testFileName,
2413
+ );
2122
2414
  if (fs.existsSync(screenshotsDir)) {
2123
2415
  fs.rmSync(screenshotsDir, { recursive: true, force: true });
2124
2416
  }
@@ -2149,23 +2441,24 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2149
2441
 
2150
2442
  // Handle reconnect option - use last sandbox file
2151
2443
  // Check both connectOptions and constructor options
2152
- const shouldReconnect = connectOptions.reconnect !== undefined
2153
- ? connectOptions.reconnect
2154
- : this.reconnect;
2155
-
2444
+ const shouldReconnect =
2445
+ connectOptions.reconnect !== undefined
2446
+ ? connectOptions.reconnect
2447
+ : this.reconnect;
2448
+
2156
2449
  // Skip reconnect if IP is supplied - directly connect to the provided IP
2157
2450
  const hasIp = Boolean(connectOptions.ip || this.ip);
2158
-
2451
+
2159
2452
  if (shouldReconnect && !hasIp) {
2160
2453
  const lastSandbox = this.agent.getLastSandboxId();
2161
2454
  if (!lastSandbox || !lastSandbox.sandboxId) {
2162
2455
  throw new Error(
2163
- "Cannot reconnect: No previous sandbox found. Run a test first to create a sandbox, or remove the reconnect option."
2456
+ "Cannot reconnect: No previous sandbox found. Run a test first to create a sandbox, or remove the reconnect option.",
2164
2457
  );
2165
2458
  }
2166
2459
  this.agent.sandboxId = lastSandbox.sandboxId;
2167
2460
  buildEnvOptions.new = false;
2168
-
2461
+
2169
2462
  // Use OS from last sandbox if not explicitly specified
2170
2463
  if (!connectOptions.os && lastSandbox.os) {
2171
2464
  this.agent.sandboxOs = lastSandbox.os;
@@ -2226,7 +2519,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2226
2519
  if (this.agent.sandboxOs) {
2227
2520
  this.os = this.agent.sandboxOs;
2228
2521
  }
2229
-
2522
+
2230
2523
  // Also ensure sandbox.os is set for consistency
2231
2524
  if (this.agent.sandbox && this.os) {
2232
2525
  this.agent.sandbox.os = this.os;
@@ -2276,7 +2569,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2276
2569
 
2277
2570
  // Always close the sandbox WebSocket connection to clean up resources
2278
2571
  // This ensures we don't leave orphaned connections even if connect() failed
2279
- if (this.sandbox && typeof this.sandbox.close === 'function') {
2572
+ if (this.sandbox && typeof this.sandbox.close === "function") {
2280
2573
  this.sandbox.close();
2281
2574
  }
2282
2575
 
@@ -2353,23 +2646,28 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2353
2646
  if (this._lastCommandName && !this._lastPromiseSettled) {
2354
2647
  console.warn(
2355
2648
  `⚠️ Warning: Previous ${this._lastCommandName}() may not have been awaited.\n` +
2356
- ` Add "await" before the call: await testdriver.${this._lastCommandName}(...)\n` +
2357
- ` Unawaited promises can cause race conditions and flaky tests.`
2649
+ ` Add "await" before the call: await testdriver.${this._lastCommandName}(...)\n` +
2650
+ ` Unawaited promises can cause race conditions and flaky tests.`,
2358
2651
  );
2359
2652
  }
2360
2653
 
2361
2654
  this._ensureConnected();
2362
2655
 
2363
2656
  // Track this promise for unawaited detection
2364
- this._lastCommandName = 'find';
2657
+ this._lastCommandName = "find";
2365
2658
  this._lastPromiseSettled = false;
2366
2659
 
2367
- const element = new Element(description, this, this.system, this.commands);
2660
+ const element = new Element(
2661
+ description,
2662
+ this,
2663
+ this.system,
2664
+ this.commands,
2665
+ );
2368
2666
  const result = await element.find(null, options);
2369
2667
  this._lastPromiseSettled = true;
2370
2668
  return result;
2371
2669
  })();
2372
-
2670
+
2373
2671
  // Create a chainable promise that allows direct method chaining
2374
2672
  // e.g., await testdriver.find("button").click()
2375
2673
  return createChainablePromise(findPromise);
@@ -2407,15 +2705,15 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2407
2705
  if (this._lastCommandName && !this._lastPromiseSettled) {
2408
2706
  console.warn(
2409
2707
  `⚠️ Warning: Previous ${this._lastCommandName}() may not have been awaited.\n` +
2410
- ` Add "await" before the call: await testdriver.${this._lastCommandName}(...)\n` +
2411
- ` Unawaited promises can cause race conditions and flaky tests.`
2708
+ ` Add "await" before the call: await testdriver.${this._lastCommandName}(...)\n` +
2709
+ ` Unawaited promises can cause race conditions and flaky tests.`,
2412
2710
  );
2413
2711
  }
2414
2712
 
2415
2713
  this._ensureConnected();
2416
2714
 
2417
2715
  // Track this promise for unawaited detection
2418
- this._lastCommandName = 'findAll';
2716
+ this._lastCommandName = "findAll";
2419
2717
  this._lastPromiseSettled = false;
2420
2718
 
2421
2719
  // Capture absolute timestamp at the very start of the command
@@ -2431,11 +2729,11 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2431
2729
  // Handle options - can be a number (cacheThreshold) or object with cacheKey/cacheThreshold
2432
2730
  let cacheKey = null;
2433
2731
  let cacheThreshold = null;
2434
-
2435
- if (typeof options === 'number') {
2732
+
2733
+ if (typeof options === "number") {
2436
2734
  // Legacy: options is just a number threshold
2437
2735
  cacheThreshold = options;
2438
- } else if (typeof options === 'object' && options !== null) {
2736
+ } else if (typeof options === "object" && options !== null) {
2439
2737
  // New: options is an object with cacheKey and/or cacheThreshold
2440
2738
  cacheKey = options.cacheKey || null;
2441
2739
  cacheThreshold = options.cacheThreshold ?? null;
@@ -2443,11 +2741,15 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2443
2741
 
2444
2742
  // Use default cacheKey from SDK constructor if not provided in findAll() options
2445
2743
  // BUT only if cache is not explicitly disabled via cache: false option
2446
- if (!cacheKey && this.options?.cacheKey && !this._cacheExplicitlyDisabled) {
2744
+ if (
2745
+ !cacheKey &&
2746
+ this.options?.cacheKey &&
2747
+ !this._cacheExplicitlyDisabled
2748
+ ) {
2447
2749
  cacheKey = this.options.cacheKey;
2448
2750
  }
2449
2751
 
2450
- // Determine threshold:
2752
+ // Determine threshold:
2451
2753
  // - If cache is explicitly disabled, don't use cache even with cacheKey
2452
2754
  // - If cacheKey is provided, enable cache with threshold
2453
2755
  // - If no cacheKey, disable cache
@@ -2468,11 +2770,13 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2468
2770
  }
2469
2771
 
2470
2772
  // Debug log threshold
2471
- const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
2773
+ const debugMode =
2774
+ process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
2472
2775
  if (debugMode) {
2473
- const autoGenMsg = (this._autoGeneratedCacheKey && cacheKey === this.options.cacheKey)
2474
- ? ' (auto-generated from file hash)'
2475
- : '';
2776
+ const autoGenMsg =
2777
+ this._autoGeneratedCacheKey && cacheKey === this.options.cacheKey
2778
+ ? " (auto-generated from file hash)"
2779
+ : "";
2476
2780
  this.emitter.emit(
2477
2781
  events.log.debug,
2478
2782
  `🔍 findAll() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"}${cacheKey ? `, cacheKey: ${cacheKey}${autoGenMsg}` : ""})`,
@@ -2536,20 +2840,22 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2536
2840
  // Track successful findAll interaction (fire-and-forget, don't block)
2537
2841
  const sessionId = this.getSessionId();
2538
2842
  if (sessionId && this.sandbox?.send) {
2539
- this.sandbox.send({
2540
- type: "trackInteraction",
2541
- interactionType: "findAll",
2542
- session: sessionId,
2543
- prompt: description,
2544
- timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
2545
- success: true,
2546
- input: { count: elements.length },
2547
- cacheHit: response.cached || false,
2548
- selector: response.selector,
2549
- selectorUsed: !!response.selector,
2550
- }).catch(err => {
2551
- console.warn("Failed to track findAll interaction:", err.message);
2552
- });
2843
+ this.sandbox
2844
+ .send({
2845
+ type: "trackInteraction",
2846
+ interactionType: "findAll",
2847
+ session: sessionId,
2848
+ prompt: description,
2849
+ timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
2850
+ success: true,
2851
+ input: { count: elements.length },
2852
+ cacheHit: response.cached || false,
2853
+ selector: response.selector,
2854
+ selectorUsed: !!response.selector,
2855
+ })
2856
+ .catch((err) => {
2857
+ console.warn("Failed to track findAll interaction:", err.message);
2858
+ });
2553
2859
  }
2554
2860
 
2555
2861
  // Log debug information when elements are found
@@ -2569,7 +2875,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2569
2875
  return elements;
2570
2876
  } else {
2571
2877
  const duration = Date.now() - startTime;
2572
-
2878
+
2573
2879
  // Single log at the end - no elements found
2574
2880
  const formattedMessage = formatter.formatFindAllSingleLine(
2575
2881
  description,
@@ -2584,21 +2890,23 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2584
2890
  // No elements found - track interaction (fire-and-forget, don't block)
2585
2891
  const sessionId = this.getSessionId();
2586
2892
  if (sessionId && this.sandbox?.send) {
2587
- this.sandbox.send({
2588
- type: "trackInteraction",
2589
- interactionType: "findAll",
2590
- session: sessionId,
2591
- prompt: description,
2592
- timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
2593
- success: false,
2594
- error: "No elements found",
2595
- input: { count: 0 },
2596
- cacheHit: response?.cached || false,
2597
- selector: response?.selector,
2598
- selectorUsed: !!response?.selector,
2599
- }).catch(err => {
2600
- console.warn("Failed to track findAll interaction:", err.message);
2601
- });
2893
+ this.sandbox
2894
+ .send({
2895
+ type: "trackInteraction",
2896
+ interactionType: "findAll",
2897
+ session: sessionId,
2898
+ prompt: description,
2899
+ timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
2900
+ success: false,
2901
+ error: "No elements found",
2902
+ input: { count: 0 },
2903
+ cacheHit: response?.cached || false,
2904
+ selector: response?.selector,
2905
+ selectorUsed: !!response?.selector,
2906
+ })
2907
+ .catch((err) => {
2908
+ console.warn("Failed to track findAll interaction:", err.message);
2909
+ });
2602
2910
  }
2603
2911
 
2604
2912
  // No elements found - return empty array
@@ -2607,7 +2915,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2607
2915
  }
2608
2916
  } catch (error) {
2609
2917
  const duration = Date.now() - startTime;
2610
-
2918
+
2611
2919
  // Single log at the end - error
2612
2920
  const formattedMessage = formatter.formatFindAllSingleLine(
2613
2921
  description,
@@ -2621,18 +2929,20 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2621
2929
  // Track findAll error interaction (fire-and-forget, don't block)
2622
2930
  const sessionId = this.getSessionId();
2623
2931
  if (sessionId && this.sandbox?.send) {
2624
- this.sandbox.send({
2625
- type: "trackInteraction",
2626
- interactionType: "findAll",
2627
- session: sessionId,
2628
- prompt: description,
2629
- timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
2630
- success: false,
2631
- error: error.message,
2632
- input: { count: 0 },
2633
- }).catch(err => {
2634
- console.warn("Failed to track findAll interaction:", err.message);
2635
- });
2932
+ this.sandbox
2933
+ .send({
2934
+ type: "trackInteraction",
2935
+ interactionType: "findAll",
2936
+ session: sessionId,
2937
+ prompt: description,
2938
+ timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
2939
+ success: false,
2940
+ error: error.message,
2941
+ input: { count: 0 },
2942
+ })
2943
+ .catch((err) => {
2944
+ console.warn("Failed to track findAll interaction:", err.message);
2945
+ });
2636
2946
  }
2637
2947
 
2638
2948
  this._lastPromiseSettled = true;
@@ -2690,20 +3000,20 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2690
3000
  "hover-text": "hoverText",
2691
3001
  "hover-image": "hoverImage",
2692
3002
  "match-image": "matchImage",
2693
- "type": "type",
3003
+ type: "type",
2694
3004
  "press-keys": "pressKeys",
2695
- "click": "click",
2696
- "hover": "hover",
2697
- "scroll": "scroll",
2698
- "wait": "wait",
3005
+ click: "click",
3006
+ hover: "hover",
3007
+ scroll: "scroll",
3008
+ wait: "wait",
2699
3009
  "wait-for-text": "waitForText",
2700
3010
  "wait-for-image": "waitForImage",
2701
3011
  "scroll-until-text": "scrollUntilText",
2702
3012
  "scroll-until-image": "scrollUntilImage",
2703
3013
  "focus-application": "focusApplication",
2704
- "extract": "extract",
2705
- "assert": "assert",
2706
- "exec": "exec",
3014
+ extract: "extract",
3015
+ assert: "assert",
3016
+ exec: "exec",
2707
3017
  };
2708
3018
 
2709
3019
  // Create SDK methods that lazy-await connection then forward to this.commands
@@ -2718,8 +3028,8 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2718
3028
  if (this._lastCommandName && !this._lastPromiseSettled) {
2719
3029
  console.warn(
2720
3030
  `⚠️ Warning: Previous ${this._lastCommandName}() may not have been awaited.\n` +
2721
- ` Add "await" before the call: await testdriver.${this._lastCommandName}(...)\n` +
2722
- ` Unawaited promises can cause race conditions and flaky tests.`
3031
+ ` Add "await" before the call: await testdriver.${this._lastCommandName}(...)\n` +
3032
+ ` Unawaited promises can cause race conditions and flaky tests.`,
2723
3033
  );
2724
3034
  }
2725
3035
 
@@ -2787,33 +3097,38 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2787
3097
  */
2788
3098
  async screenshot(filename) {
2789
3099
  this._ensureConnected();
2790
-
2791
- const finalFilename = filename
2792
- ? (filename.endsWith('.png') ? filename : `${filename}.png`)
3100
+
3101
+ const finalFilename = filename
3102
+ ? filename.endsWith(".png")
3103
+ ? filename
3104
+ : `${filename}.png`
2793
3105
  : `screenshot-${Date.now()}.png`;
2794
-
3106
+
2795
3107
  const base64Data = await this.system.captureScreenBase64(1, false, false);
2796
-
3108
+
2797
3109
  // Save to .testdriver/screenshots/<test-file-name> directory
2798
3110
  let screenshotsDir = path.join(process.cwd(), ".testdriver", "screenshots");
2799
3111
  if (this.testFile) {
2800
- const testFileName = path.basename(this.testFile, path.extname(this.testFile));
3112
+ const testFileName = path.basename(
3113
+ this.testFile,
3114
+ path.extname(this.testFile),
3115
+ );
2801
3116
  screenshotsDir = path.join(screenshotsDir, testFileName);
2802
3117
  }
2803
3118
  if (!fs.existsSync(screenshotsDir)) {
2804
3119
  fs.mkdirSync(screenshotsDir, { recursive: true });
2805
3120
  }
2806
-
3121
+
2807
3122
  const filePath = path.join(screenshotsDir, finalFilename);
2808
-
3123
+
2809
3124
  // Remove data:image/png;base64, prefix if present
2810
3125
  const cleanBase64 = base64Data.replace(/^data:image\/\w+;base64,/, "");
2811
3126
  const buffer = Buffer.from(cleanBase64, "base64");
2812
-
3127
+
2813
3128
  fs.writeFileSync(filePath, buffer);
2814
-
3129
+
2815
3130
  this.emitter.emit("log:info", `📸 Screenshot saved to: ${filePath}`);
2816
-
3131
+
2817
3132
  return filePath;
2818
3133
  }
2819
3134
 
@@ -2872,13 +3187,14 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2872
3187
  _setupLogging() {
2873
3188
  // Track the last fatal error message to throw on exit
2874
3189
  let lastFatalError = null;
2875
- const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
3190
+ const debugMode =
3191
+ process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
2876
3192
 
2877
3193
  // Set up markdown logger
2878
3194
  createMarkdownLogger(this.emitter);
2879
3195
 
2880
3196
  // Set up basic event logging
2881
- // Note: We only console.log here - the console spy in vitest/hooks.mjs
3197
+ // Note: We only console.log here - the console spy in vitest/hooks.mjs
2882
3198
  // handles forwarding to sandbox. This prevents duplicate output to server.
2883
3199
  this.emitter.on("log:**", (message) => {
2884
3200
  const event = this.emitter.event;
@@ -2900,7 +3216,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2900
3216
  if (this.loggingEnabled) {
2901
3217
  const event = this.emitter.event;
2902
3218
  console.error(event, ":", data);
2903
-
3219
+
2904
3220
  // Capture fatal errors
2905
3221
  if (event === events.error.fatal) {
2906
3222
  lastFatalError = data;
@@ -2919,9 +3235,9 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2919
3235
  this.emitter.on(events.exit, (exitCode) => {
2920
3236
  if (exitCode !== 0) {
2921
3237
  // Create an error with the fatal error message if available
2922
- const errorMessage = lastFatalError || 'TestDriver fatal error';
3238
+ const errorMessage = lastFatalError || "TestDriver fatal error";
2923
3239
  const error = new Error(errorMessage);
2924
- error.name = 'TestDriverFatalError';
3240
+ error.name = "TestDriverFatalError";
2925
3241
  error.exitCode = exitCode;
2926
3242
  throw error;
2927
3243
  }
@@ -2946,7 +3262,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2946
3262
  console.log(url);
2947
3263
  await this._openBrowser(url);
2948
3264
  }
2949
-
2950
3265
  }
2951
3266
  });
2952
3267
  }
@@ -3037,7 +3352,12 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
3037
3352
 
3038
3353
  // Auto-detect sandbox ID from the active sandbox if not provided
3039
3354
  // For E2B (Linux), the instance has sandboxId; for AWS (Windows), it has instanceId
3040
- const sandboxId = options.sandboxId || this.instance?.sandboxId || this.instance?.instanceId || this.agent?.sandboxId || null;
3355
+ const sandboxId =
3356
+ options.sandboxId ||
3357
+ this.instance?.sandboxId ||
3358
+ this.instance?.instanceId ||
3359
+ this.agent?.sandboxId ||
3360
+ null;
3041
3361
 
3042
3362
  // Get or create session ID using the agent's newSession method
3043
3363
  let sessionId = this.agent?.sessionInstance?.get() || null;
@@ -3208,7 +3528,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
3208
3528
  // Store original checkLimit and set custom one if provided
3209
3529
  const originalCheckLimit = this.agent.checkLimit;
3210
3530
  this.agent.checkLimit = tries;
3211
-
3531
+
3212
3532
  // Reset check count for this act() call
3213
3533
  const originalCheckCount = this.agent.checkCount;
3214
3534
  this.agent.checkCount = 0;
@@ -3218,17 +3538,25 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
3218
3538
 
3219
3539
  try {
3220
3540
  // Use the agent's exploratoryLoop method directly
3221
- const response = await this.agent.exploratoryLoop(task, false, true, false);
3222
-
3541
+ const response = await this.agent.exploratoryLoop(
3542
+ task,
3543
+ false,
3544
+ true,
3545
+ false,
3546
+ );
3547
+
3223
3548
  const duration = Date.now() - startTime;
3224
3549
  const triesUsed = this.agent.checkCount;
3225
-
3226
- this.emitter.emit(events.log.log, formatter.formatAIComplete(duration, true));
3227
-
3550
+
3551
+ this.emitter.emit(
3552
+ events.log.log,
3553
+ formatter.formatAIComplete(duration, true),
3554
+ );
3555
+
3228
3556
  // Restore original checkLimit
3229
3557
  this.agent.checkLimit = originalCheckLimit;
3230
3558
  this.agent.checkCount = originalCheckCount;
3231
-
3559
+
3232
3560
  return {
3233
3561
  success: true,
3234
3562
  task,
@@ -3240,13 +3568,16 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
3240
3568
  } catch (error) {
3241
3569
  const duration = Date.now() - startTime;
3242
3570
  const triesUsed = this.agent.checkCount;
3243
-
3244
- this.emitter.emit(events.log.log, formatter.formatAIComplete(duration, false, error.message));
3245
-
3571
+
3572
+ this.emitter.emit(
3573
+ events.log.log,
3574
+ formatter.formatAIComplete(duration, false, error.message),
3575
+ );
3576
+
3246
3577
  // Restore original checkLimit
3247
3578
  this.agent.checkLimit = originalCheckLimit;
3248
3579
  this.agent.checkCount = originalCheckCount;
3249
-
3580
+
3250
3581
  // Create an enhanced error with additional context using AIError class
3251
3582
  throw new AIError(`AI failed: ${error.message}`, {
3252
3583
  task,