testdriverai 7.3.24 → 7.3.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [7.3.26](https://github.com/testdriverai/testdriverai/compare/v7.3.25...v7.3.26) (2026-02-20)
2
+
3
+
4
+
5
+ ## [7.3.25](https://github.com/testdriverai/testdriverai/compare/v7.3.24...v7.3.25) (2026-02-20)
6
+
7
+
8
+
1
9
  ## [7.3.24](https://github.com/testdriverai/testdriverai/compare/v7.3.23...v7.3.24) (2026-02-20)
2
10
 
3
11
 
@@ -122,6 +122,11 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
122
122
  );
123
123
  }
124
124
  }, timeout);
125
+ // Don't let pending message timeouts prevent Node process from exiting
126
+ // (unref is not available in browser/non-Node environments)
127
+ if (timeoutId.unref) {
128
+ timeoutId.unref();
129
+ }
125
130
 
126
131
  // Track timeout so close() can clear it
127
132
  this.pendingTimeouts.set(requestId, timeoutId);
@@ -309,6 +314,11 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
309
314
  this.reconnecting = false;
310
315
  }
311
316
  }, delay);
317
+ // Don't let the reconnect timer prevent Node process from exiting
318
+ // (unref is not available in browser/non-Node environments)
319
+ if (this.reconnectTimer.unref) {
320
+ this.reconnectTimer.unref();
321
+ }
312
322
  }
313
323
 
314
324
  /**
@@ -12,8 +12,8 @@ describe("Parse Test", () => {
12
12
  const testdriver = TestDriver(context, { ...getDefaults(context) });
13
13
  await testdriver.provision.chrome({ url: "https://www.airbnb.com" });
14
14
 
15
+ // The SDK automatically outputs elements as a formatted table
15
16
  const result = await testdriver.parse();
16
17
  console.log(`Found ${result.elements?.length || 0} elements`);
17
- console.log(JSON.stringify(result, null, 2));
18
18
  });
19
19
  });
@@ -173,6 +173,33 @@ function cleanupConsoleSpy(client) {
173
173
  const testDriverInstances = new WeakMap();
174
174
  const lifecycleHandlers = new WeakMap();
175
175
 
176
+ // Set to track all active TestDriver instances for signal-based cleanup
177
+ const activeInstances = new Set();
178
+
179
+ // Register signal handlers once to clean up all active instances on forced exit
180
+ let signalHandlersRegistered = false;
181
+ function registerSignalHandlers() {
182
+ if (signalHandlersRegistered) return;
183
+ signalHandlersRegistered = true;
184
+
185
+ const cleanup = async () => {
186
+ const instances = Array.from(activeInstances);
187
+ activeInstances.clear();
188
+ await Promise.race([
189
+ Promise.all(instances.map((inst) => inst.disconnect().catch(() => {}))),
190
+ new Promise((resolve) => setTimeout(resolve, 5000)), // 5s max for cleanup
191
+ ]);
192
+ };
193
+
194
+ process.on("SIGINT", () => {
195
+ cleanup().finally(() => process.exit(130));
196
+ });
197
+
198
+ process.on("SIGTERM", () => {
199
+ cleanup().finally(() => process.exit(143));
200
+ });
201
+ }
202
+
176
203
  /**
177
204
  * Create a TestDriver client in a Vitest test with automatic lifecycle management
178
205
  *
@@ -253,6 +280,8 @@ export function TestDriver(context, options = {}) {
253
280
  testdriver.__vitestContext = context.task;
254
281
  testdriver._debugOnFailure = mergedOptions.debugOnFailure || false;
255
282
  testDriverInstances.set(context.task, testdriver);
283
+ activeInstances.add(testdriver);
284
+ registerSignalHandlers();
256
285
 
257
286
  // Set platform metadata early so the reporter can show the correct OS from the start
258
287
  if (!context.task.meta) {
@@ -388,6 +417,9 @@ export function TestDriver(context, options = {}) {
388
417
  // Clean up console spies
389
418
  cleanupConsoleSpy(currentInstance);
390
419
 
420
+ // Remove from active instances tracking (even in debug mode we clean up tracking)
421
+ activeInstances.delete(currentInstance);
422
+
391
423
  // DO NOT disconnect or terminate - keep sandbox alive for debugging
392
424
  return;
393
425
  }
@@ -514,6 +546,8 @@ export function TestDriver(context, options = {}) {
514
546
  } catch (error) {
515
547
  console.error("Error disconnecting client:", error);
516
548
  } finally {
549
+ // Remove from active instances tracking
550
+ activeInstances.delete(currentInstance);
517
551
  // Terminate AWS instance if one was spawned for this test
518
552
  // This must happen AFTER dashcam.stop() to ensure recording is saved
519
553
  // AND it must happen even if disconnect() fails
@@ -115,16 +115,16 @@ function cleanupAllInstances() {
115
115
  process.on("exit", cleanupAllInstances);
116
116
  process.on("SIGINT", () => {
117
117
  cleanupAllInstances();
118
- // Don't call process.exit here - let the signal handler do its job
118
+ process.exit(130); // Restore default SIGINT exit behavior (128 + signal 2)
119
119
  });
120
120
  process.on("SIGTERM", () => {
121
121
  cleanupAllInstances();
122
- // Don't call process.exit here - let the signal handler do its job
122
+ process.exit(143); // Restore default SIGTERM exit behavior (128 + signal 15)
123
123
  });
124
124
  process.on("uncaughtException", (error) => {
125
125
  console.error("[TestDriver] Uncaught exception:", error);
126
126
  cleanupAllInstances();
127
- // Don't call process.exit here - let Node.js handle the exception
127
+ process.exit(1); // Exit after uncaught exception cleanup
128
128
  });
129
129
 
130
130
  beforeEach(async (context) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.3.24",
3
+ "version": "7.3.26",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "sdk.js",
6
6
  "types": "sdk.d.ts",
@@ -1022,6 +1022,130 @@ class SDKLogFormatter {
1022
1022
 
1023
1023
  return parts.join(" ");
1024
1024
  }
1025
+
1026
+ /**
1027
+ * Format parse() elements as a formatted table for console output 📋
1028
+ * @param {Array} elements - Array of parsed elements from parse()
1029
+ * @param {Object} options - Formatting options
1030
+ * @param {number} options.maxContentLength - Max length for content column (default: 30)
1031
+ * @param {number} options.maxRows - Max number of rows to display (default: 50)
1032
+ * @returns {string} Formatted table string
1033
+ */
1034
+ formatParseElements(elements, options = {}) {
1035
+ if (!elements || elements.length === 0) {
1036
+ return chalk.dim(" No elements found");
1037
+ }
1038
+
1039
+ const maxContentLength = options.maxContentLength || 30;
1040
+ const maxRows = options.maxRows || 50;
1041
+
1042
+ // Column widths
1043
+ const idxWidth = 5;
1044
+ const typeWidth = 10;
1045
+ const contentWidth = maxContentLength + 2;
1046
+ const interactWidth = 14;
1047
+ const posWidth = 18;
1048
+
1049
+ const lines = [];
1050
+
1051
+ // Header
1052
+ const headerLine = [
1053
+ chalk.bold.cyan(this._padRight("Idx", idxWidth)),
1054
+ chalk.bold.cyan(this._padRight("Type", typeWidth)),
1055
+ chalk.bold.cyan(this._padRight("Content", contentWidth)),
1056
+ chalk.bold.cyan(this._padRight("Interactive", interactWidth)),
1057
+ chalk.bold.cyan("Position"),
1058
+ ].join(chalk.dim(" │ "));
1059
+
1060
+ lines.push(" " + headerLine);
1061
+
1062
+ // Separator line
1063
+ const separatorLine = [
1064
+ chalk.dim("─".repeat(idxWidth)),
1065
+ chalk.dim("─".repeat(typeWidth)),
1066
+ chalk.dim("─".repeat(contentWidth)),
1067
+ chalk.dim("─".repeat(interactWidth)),
1068
+ chalk.dim("─".repeat(posWidth)),
1069
+ ].join(chalk.dim("─┼─"));
1070
+
1071
+ lines.push(" " + separatorLine);
1072
+
1073
+ // Data rows
1074
+ const displayElements = elements.slice(0, maxRows);
1075
+ for (const el of displayElements) {
1076
+ const idx = String(el.index ?? "?");
1077
+ const type = el.type || "unknown";
1078
+ const content = this._truncate(el.content || "", maxContentLength);
1079
+
1080
+ // Format interactivity with color
1081
+ // Note: interactivity can be boolean (true/false) or string ("clickable", "non-interactive")
1082
+ let interactivity = el.interactivity || "-";
1083
+ let interactivityDisplay;
1084
+ if (interactivity === "clickable" || interactivity === true) {
1085
+ interactivityDisplay = chalk.green("✓ clickable");
1086
+ } else if (interactivity === false || interactivity === "non-interactive") {
1087
+ interactivityDisplay = chalk.dim("-");
1088
+ } else {
1089
+ interactivityDisplay = chalk.dim(String(interactivity));
1090
+ }
1091
+
1092
+ // Format position from bbox
1093
+ let position = "-";
1094
+ if (el.bbox) {
1095
+ position = `(${el.bbox.x0}, ${el.bbox.y0})`;
1096
+ }
1097
+
1098
+ // For interactivity column, we need to pad based on visible text, not chalk codes
1099
+ // "✓ clickable" = 11 chars, "-" = 1 char, so we pad manually after getting visible length
1100
+ const interactPadded = this._padRight(interactivityDisplay, interactWidth);
1101
+
1102
+ const dataLine = [
1103
+ chalk.yellow(this._padRight(idx, idxWidth)),
1104
+ chalk.white(this._padRight(type, typeWidth)),
1105
+ chalk.gray(this._padRight(content, contentWidth)),
1106
+ interactPadded,
1107
+ chalk.dim(position),
1108
+ ].join(chalk.dim(" │ "));
1109
+
1110
+ lines.push(" " + dataLine);
1111
+ }
1112
+
1113
+ // Show truncation message if needed
1114
+ if (elements.length > maxRows) {
1115
+ lines.push(chalk.dim(` ... and ${elements.length - maxRows} more elements (showing first ${maxRows})`));
1116
+ }
1117
+
1118
+ return lines.join("\n");
1119
+ }
1120
+
1121
+ /**
1122
+ * Truncate a string to a maximum length with ellipsis
1123
+ * @private
1124
+ * @param {string} str - String to truncate
1125
+ * @param {number} maxLength - Maximum length
1126
+ * @returns {string} Truncated string
1127
+ */
1128
+ _truncate(str, maxLength) {
1129
+ if (!str) return "";
1130
+ // Remove newlines and extra whitespace
1131
+ const cleaned = str.replace(/\s+/g, " ").trim();
1132
+ if (cleaned.length <= maxLength) return cleaned;
1133
+ return cleaned.substring(0, maxLength - 1) + "…";
1134
+ }
1135
+
1136
+ /**
1137
+ * Pad a string to the right to a fixed width
1138
+ * @private
1139
+ * @param {string} str - String to pad
1140
+ * @param {number} width - Target width
1141
+ * @returns {string} Padded string
1142
+ */
1143
+ _padRight(str, width) {
1144
+ // Handle chalk strings by getting visible length
1145
+ const visibleStr = String(str).replace(/\x1b\[[0-9;]*m/g, "");
1146
+ const padding = Math.max(0, width - visibleStr.length);
1147
+ return str + " ".repeat(padding);
1148
+ }
1025
1149
  }
1026
1150
 
1027
1151
  // Export singleton instance
package/sdk.js CHANGED
@@ -3678,6 +3678,12 @@ CAPTCHA_SOLVER_EOF`,
3678
3678
  `✅ Parse complete: ${response.elements?.length || 0} elements detected`,
3679
3679
  );
3680
3680
 
3681
+ // Output elements as a formatted table
3682
+ if (response.elements && response.elements.length > 0) {
3683
+ const tableOutput = formatter.formatParseElements(response.elements);
3684
+ this.emitter.emit(events.log.log, tableOutput);
3685
+ }
3686
+
3681
3687
  return response;
3682
3688
  }
3683
3689