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 +8 -0
- package/agent/lib/sandbox.js +10 -0
- package/examples/parse.test.mjs +1 -1
- package/lib/vitest/hooks.mjs +34 -0
- package/lib/vitest/setup-aws.mjs +3 -3
- package/package.json +1 -1
- package/sdk-log-formatter.js +124 -0
- package/sdk.js +6 -0
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
|
|
package/agent/lib/sandbox.js
CHANGED
|
@@ -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
|
/**
|
package/examples/parse.test.mjs
CHANGED
|
@@ -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
|
});
|
package/lib/vitest/hooks.mjs
CHANGED
|
@@ -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
|
package/lib/vitest/setup-aws.mjs
CHANGED
|
@@ -115,16 +115,16 @@ function cleanupAllInstances() {
|
|
|
115
115
|
process.on("exit", cleanupAllInstances);
|
|
116
116
|
process.on("SIGINT", () => {
|
|
117
117
|
cleanupAllInstances();
|
|
118
|
-
|
|
118
|
+
process.exit(130); // Restore default SIGINT exit behavior (128 + signal 2)
|
|
119
119
|
});
|
|
120
120
|
process.on("SIGTERM", () => {
|
|
121
121
|
cleanupAllInstances();
|
|
122
|
-
|
|
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
|
-
|
|
127
|
+
process.exit(1); // Exit after uncaught exception cleanup
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
beforeEach(async (context) => {
|
package/package.json
CHANGED
package/sdk-log-formatter.js
CHANGED
|
@@ -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
|
|