testdriverai 7.2.56 â 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/.env.example +2 -0
- package/.github/workflows/acceptance-windows-scheduled.yaml +29 -28
- package/.github/workflows/acceptance.yaml +54 -52
- package/.github/workflows/testdriver.yml +157 -156
- package/.github/workflows/windows-self-hosted.yaml +60 -46
- package/docs/docs.json +1 -0
- package/docs/v7/captcha.mdx +160 -0
- package/examples/captcha-api.test.mjs +50 -0
- package/lib/captcha/solver.js +296 -0
- package/lib/core/Dashcam.js +135 -95
- package/lib/vitest/hooks.mjs +175 -126
- package/lib/vitest/setup-aws.mjs +69 -46
- package/package.json +1 -1
- package/sdk.d.ts +67 -20
- package/sdk.js +733 -402
- package/test/captcha-solver.test.mjs +70 -0
- package/test/chrome-remote-debugging.test.mjs +66 -0
- package/vitest.config.mjs +10 -6
package/lib/vitest/hooks.mjs
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Vitest Hooks for TestDriver
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Provides lifecycle management for TestDriver in Vitest tests.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* @example
|
|
7
7
|
* import { TestDriver } from 'testdriverai/vitest/hooks';
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* test('my test', async (context) => {
|
|
10
10
|
* const testdriver = TestDriver(context, { headless: true });
|
|
11
|
-
*
|
|
11
|
+
*
|
|
12
12
|
* await testdriver.provision.chrome({ url: 'https://example.com' });
|
|
13
13
|
* await testdriver.find('button').click();
|
|
14
14
|
* });
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import chalk from
|
|
18
|
-
import { createRequire } from
|
|
19
|
-
import path from
|
|
20
|
-
import { vi } from
|
|
21
|
-
import TestDriverSDK from
|
|
17
|
+
import chalk from "chalk";
|
|
18
|
+
import { createRequire } from "module";
|
|
19
|
+
import path from "path";
|
|
20
|
+
import { vi } from "vitest";
|
|
21
|
+
import TestDriverSDK from "../../sdk.js";
|
|
22
22
|
|
|
23
23
|
// Use createRequire to import CommonJS modules
|
|
24
24
|
const require = createRequire(import.meta.url);
|
|
@@ -34,21 +34,21 @@ const MINIMUM_VITEST_VERSION = 4;
|
|
|
34
34
|
*/
|
|
35
35
|
function checkVitestVersion() {
|
|
36
36
|
try {
|
|
37
|
-
const vitestPkg = require(
|
|
37
|
+
const vitestPkg = require("vitest/package.json");
|
|
38
38
|
const version = vitestPkg.version;
|
|
39
|
-
const major = parseInt(version.split(
|
|
40
|
-
|
|
39
|
+
const major = parseInt(version.split(".")[0], 10);
|
|
40
|
+
|
|
41
41
|
if (major < MINIMUM_VITEST_VERSION) {
|
|
42
42
|
throw new Error(
|
|
43
43
|
`TestDriver requires Vitest >= ${MINIMUM_VITEST_VERSION}.0.0, but found ${version}. ` +
|
|
44
|
-
|
|
44
|
+
`Please upgrade Vitest: npm install vitest@latest`,
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
} catch (err) {
|
|
48
|
-
if (err.code ===
|
|
48
|
+
if (err.code === "MODULE_NOT_FOUND") {
|
|
49
49
|
throw new Error(
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
"TestDriver requires Vitest to be installed. " +
|
|
51
|
+
"Please install it: npm install vitest@latest",
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
throw err;
|
|
@@ -66,14 +66,19 @@ checkVitestVersion();
|
|
|
66
66
|
* @param {string} taskId - Unique task identifier for this test
|
|
67
67
|
*/
|
|
68
68
|
function setupConsoleSpy(client, taskId) {
|
|
69
|
-
|
|
70
69
|
// Debug logging for console spy setup
|
|
71
|
-
const debugConsoleSpy = process.env.TD_DEBUG_CONSOLE_SPY ===
|
|
70
|
+
const debugConsoleSpy = process.env.TD_DEBUG_CONSOLE_SPY === "true";
|
|
72
71
|
if (debugConsoleSpy) {
|
|
73
72
|
process.stdout.write(`[DEBUG setupConsoleSpy] taskId: ${taskId}\n`);
|
|
74
|
-
process.stdout.write(
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
process.stdout.write(
|
|
74
|
+
`[DEBUG setupConsoleSpy] client.sandbox exists: ${!!client.sandbox}\n`,
|
|
75
|
+
);
|
|
76
|
+
process.stdout.write(
|
|
77
|
+
`[DEBUG setupConsoleSpy] client.sandbox?.instanceSocketConnected: ${client.sandbox?.instanceSocketConnected}\n`,
|
|
78
|
+
);
|
|
79
|
+
process.stdout.write(
|
|
80
|
+
`[DEBUG setupConsoleSpy] client.sandbox?.send: ${typeof client.sandbox?.send}\n`,
|
|
81
|
+
);
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
// Track forwarding stats
|
|
@@ -84,9 +89,7 @@ function setupConsoleSpy(client, taskId) {
|
|
|
84
89
|
const forwardToSandbox = (args) => {
|
|
85
90
|
const message = args
|
|
86
91
|
.map((arg) =>
|
|
87
|
-
typeof arg === "object"
|
|
88
|
-
? JSON.stringify(arg, null, 2)
|
|
89
|
-
: String(arg),
|
|
92
|
+
typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg),
|
|
90
93
|
)
|
|
91
94
|
.join(" ");
|
|
92
95
|
|
|
@@ -99,17 +102,23 @@ function setupConsoleSpy(client, taskId) {
|
|
|
99
102
|
});
|
|
100
103
|
forwardedCount++;
|
|
101
104
|
if (debugConsoleSpy && forwardedCount <= 3) {
|
|
102
|
-
process.stdout.write(
|
|
105
|
+
process.stdout.write(
|
|
106
|
+
`[DEBUG forwardToSandbox] Forwarded message #${forwardedCount}: "${message.substring(0, 50)}..."\n`,
|
|
107
|
+
);
|
|
103
108
|
}
|
|
104
109
|
} catch (err) {
|
|
105
110
|
if (debugConsoleSpy) {
|
|
106
|
-
process.stdout.write(
|
|
111
|
+
process.stdout.write(
|
|
112
|
+
`[DEBUG forwardToSandbox] Error sending: ${err.message}\n`,
|
|
113
|
+
);
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
116
|
} else {
|
|
110
117
|
skippedCount++;
|
|
111
118
|
if (debugConsoleSpy && skippedCount <= 3) {
|
|
112
|
-
process.stdout.write(
|
|
119
|
+
process.stdout.write(
|
|
120
|
+
`[DEBUG forwardToSandbox] SKIPPED (sandbox not connected): "${message.substring(0, 50)}..."\n`,
|
|
121
|
+
);
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
};
|
|
@@ -121,29 +130,28 @@ function setupConsoleSpy(client, taskId) {
|
|
|
121
130
|
const originalInfo = console.info.bind(console);
|
|
122
131
|
|
|
123
132
|
// Create spies for each console method
|
|
124
|
-
const logSpy = vi.spyOn(console,
|
|
125
|
-
originalLog(...args);
|
|
133
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation((...args) => {
|
|
134
|
+
originalLog(...args); // Call original (Vitest will capture this)
|
|
126
135
|
forwardToSandbox(args);
|
|
127
136
|
});
|
|
128
137
|
|
|
129
|
-
const errorSpy = vi.spyOn(console,
|
|
138
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => {
|
|
130
139
|
originalError(...args);
|
|
131
140
|
forwardToSandbox(args);
|
|
132
141
|
});
|
|
133
142
|
|
|
134
|
-
const warnSpy = vi.spyOn(console,
|
|
143
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation((...args) => {
|
|
135
144
|
originalWarn(...args);
|
|
136
145
|
forwardToSandbox(args);
|
|
137
146
|
});
|
|
138
147
|
|
|
139
|
-
const infoSpy = vi.spyOn(console,
|
|
148
|
+
const infoSpy = vi.spyOn(console, "info").mockImplementation((...args) => {
|
|
140
149
|
originalInfo(...args);
|
|
141
150
|
forwardToSandbox(args);
|
|
142
151
|
});
|
|
143
152
|
|
|
144
153
|
// Store spies on client for cleanup
|
|
145
154
|
client._consoleSpies = { logSpy, errorSpy, warnSpy, infoSpy };
|
|
146
|
-
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
/**
|
|
@@ -167,26 +175,28 @@ const lifecycleHandlers = new WeakMap();
|
|
|
167
175
|
|
|
168
176
|
/**
|
|
169
177
|
* Create a TestDriver client in a Vitest test with automatic lifecycle management
|
|
170
|
-
*
|
|
178
|
+
*
|
|
171
179
|
* @param {import('vitest').TestContext} context - Vitest test context (from async (context) => {})
|
|
172
180
|
* @param {import('../../sdk.js').TestDriverOptions} [options] - TestDriver options (passed directly to TestDriver constructor)
|
|
173
181
|
* @returns {import('../../sdk.js').default} TestDriver client instance
|
|
174
|
-
*
|
|
182
|
+
*
|
|
175
183
|
* @example
|
|
176
184
|
* test('my test', async (context) => {
|
|
177
185
|
* const testdriver = TestDriver(context, { headless: true });
|
|
178
|
-
*
|
|
186
|
+
*
|
|
179
187
|
* // provision.chrome() automatically calls ready() and starts dashcam
|
|
180
188
|
* await testdriver.provision.chrome({ url: 'https://example.com' });
|
|
181
|
-
*
|
|
189
|
+
*
|
|
182
190
|
* await testdriver.find('Login button').click();
|
|
183
191
|
* });
|
|
184
192
|
*/
|
|
185
193
|
export function TestDriver(context, options = {}) {
|
|
186
194
|
if (!context || !context.task) {
|
|
187
|
-
throw new Error(
|
|
195
|
+
throw new Error(
|
|
196
|
+
'TestDriver() requires Vitest context. Pass the context parameter from your test function: test("name", async (context) => { ... })',
|
|
197
|
+
);
|
|
188
198
|
}
|
|
189
|
-
|
|
199
|
+
|
|
190
200
|
// Return existing instance if already created for this test AND it's still connected
|
|
191
201
|
// On retry, the previous instance will be disconnected, so we need to create a new one
|
|
192
202
|
if (testDriverInstances.has(context.task)) {
|
|
@@ -198,106 +208,123 @@ export function TestDriver(context, options = {}) {
|
|
|
198
208
|
testDriverInstances.delete(context.task);
|
|
199
209
|
lifecycleHandlers.delete(context.task);
|
|
200
210
|
}
|
|
201
|
-
|
|
211
|
+
|
|
202
212
|
// Get global plugin options if available
|
|
203
|
-
const pluginOptions =
|
|
204
|
-
|
|
213
|
+
const pluginOptions =
|
|
214
|
+
globalThis.__testdriverPlugin?.state?.testDriverOptions || {};
|
|
215
|
+
|
|
205
216
|
// Merge options: plugin global options < test-specific options
|
|
206
217
|
const mergedOptions = { ...pluginOptions, ...options };
|
|
207
|
-
|
|
218
|
+
|
|
208
219
|
// Support TD_OS environment variable for specifying target OS (linux, mac, windows)
|
|
209
220
|
// Priority: test options > plugin options > environment variable > default (linux)
|
|
210
221
|
if (!mergedOptions.os && process.env.TD_OS) {
|
|
211
222
|
mergedOptions.os = process.env.TD_OS;
|
|
212
|
-
console.log(
|
|
223
|
+
console.log(
|
|
224
|
+
`[testdriver] Set mergedOptions.os = ${mergedOptions.os} from TD_OS environment variable`,
|
|
225
|
+
);
|
|
213
226
|
}
|
|
214
|
-
|
|
227
|
+
|
|
215
228
|
// Use IP from context if set by setup-aws.mjs (or other setup files)
|
|
216
229
|
// Priority: test options > context.ip (from setup hooks)
|
|
217
230
|
if (!mergedOptions.ip && context.ip) {
|
|
218
231
|
mergedOptions.ip = context.ip;
|
|
219
|
-
console.log(
|
|
232
|
+
console.log(
|
|
233
|
+
`[testdriver] Set mergedOptions.ip = ${mergedOptions.ip} from context.ip`,
|
|
234
|
+
);
|
|
220
235
|
}
|
|
221
|
-
|
|
236
|
+
|
|
222
237
|
// Extract TestDriver-specific options
|
|
223
238
|
const apiKey = mergedOptions.apiKey || process.env.TD_API_KEY;
|
|
224
|
-
|
|
239
|
+
|
|
225
240
|
// Build config for TestDriverSDK constructor
|
|
226
241
|
const config = { ...mergedOptions };
|
|
227
242
|
delete config.apiKey;
|
|
228
|
-
|
|
243
|
+
|
|
229
244
|
// Use TD_API_ROOT from environment if not provided in config
|
|
230
245
|
if (!config.apiRoot && process.env.TD_API_ROOT) {
|
|
231
246
|
config.apiRoot = process.env.TD_API_ROOT;
|
|
232
247
|
}
|
|
233
|
-
|
|
248
|
+
|
|
234
249
|
const testdriver = new TestDriverSDK(apiKey, config);
|
|
235
250
|
testdriver.__vitestContext = context.task;
|
|
236
251
|
testDriverInstances.set(context.task, testdriver);
|
|
237
|
-
|
|
252
|
+
|
|
238
253
|
// Set platform metadata early so the reporter can show the correct OS from the start
|
|
239
254
|
if (!context.task.meta) {
|
|
240
255
|
context.task.meta = {};
|
|
241
256
|
}
|
|
242
|
-
const platform = mergedOptions.os ||
|
|
243
|
-
const absolutePath =
|
|
257
|
+
const platform = mergedOptions.os || "linux";
|
|
258
|
+
const absolutePath =
|
|
259
|
+
context.task.file?.filepath || context.task.file?.name || "unknown";
|
|
244
260
|
const projectRoot = process.cwd();
|
|
245
|
-
const testFile =
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
261
|
+
const testFile =
|
|
262
|
+
absolutePath !== "unknown"
|
|
263
|
+
? path.relative(projectRoot, absolutePath)
|
|
264
|
+
: absolutePath;
|
|
265
|
+
|
|
249
266
|
context.task.meta.platform = platform;
|
|
250
267
|
context.task.meta.testFile = testFile;
|
|
251
268
|
context.task.meta.testOrder = 0;
|
|
252
|
-
|
|
269
|
+
|
|
253
270
|
// Pass test file name to SDK for debugger display
|
|
254
271
|
testdriver.testFile = testFile;
|
|
255
|
-
|
|
256
|
-
const debugConsoleSpy = process.env.TD_DEBUG_CONSOLE_SPY ===
|
|
257
|
-
|
|
272
|
+
|
|
273
|
+
const debugConsoleSpy = process.env.TD_DEBUG_CONSOLE_SPY === "true";
|
|
274
|
+
|
|
258
275
|
testdriver.__connectionPromise = (async () => {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
276
|
+
if (debugConsoleSpy) {
|
|
277
|
+
console.log(
|
|
278
|
+
"[DEBUG] Before auth - sandbox.instanceSocketConnected:",
|
|
279
|
+
testdriver.sandbox?.instanceSocketConnected,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
await testdriver.auth();
|
|
284
|
+
await testdriver.connect();
|
|
285
|
+
|
|
286
|
+
// Clear the connection promise now that we're connected
|
|
287
|
+
// This prevents deadlock when exec() is called below (exec() lazy-awaits __connectionPromise)
|
|
288
|
+
testdriver.__connectionPromise = null;
|
|
289
|
+
|
|
290
|
+
if (debugConsoleSpy) {
|
|
291
|
+
console.log(
|
|
292
|
+
"[DEBUG] After connect - sandbox.instanceSocketConnected:",
|
|
293
|
+
testdriver.sandbox?.instanceSocketConnected,
|
|
294
|
+
);
|
|
295
|
+
console.log(
|
|
296
|
+
"[DEBUG] After connect - sandbox.send:",
|
|
297
|
+
typeof testdriver.sandbox?.send,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Set up console spy using vi.spyOn (test-isolated)
|
|
302
|
+
setupConsoleSpy(testdriver, context.task.id);
|
|
303
|
+
|
|
304
|
+
// Create the log file on the remote machine
|
|
305
|
+
const shell = testdriver.os === "windows" ? "pwsh" : "sh";
|
|
306
|
+
const logPath =
|
|
307
|
+
testdriver.os === "windows"
|
|
281
308
|
? "C:\\Users\\testdriver\\Documents\\testdriver.log"
|
|
282
309
|
: "/tmp/testdriver.log";
|
|
283
|
-
|
|
284
|
-
|
|
310
|
+
|
|
311
|
+
const createLogCmd =
|
|
312
|
+
testdriver.os === "windows"
|
|
285
313
|
? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
|
|
286
314
|
: `touch ${logPath}`;
|
|
287
|
-
|
|
288
|
-
await testdriver.exec(shell, createLogCmd, 10000, true);
|
|
289
|
-
|
|
290
|
-
// Only set up dashcam if enabled (default: true)
|
|
291
|
-
if (testdriver.dashcamEnabled) {
|
|
292
|
-
// Add testdriver log to dashcam tracking
|
|
293
|
-
await testdriver.dashcam.addFileLog(logPath, "TestDriver Log");
|
|
294
|
-
|
|
295
|
-
// Start dashcam recording (always, regardless of provision method)
|
|
296
|
-
await testdriver.dashcam.start();
|
|
297
|
-
}
|
|
298
315
|
|
|
299
|
-
|
|
300
|
-
|
|
316
|
+
await testdriver.exec(shell, createLogCmd, 10000, true);
|
|
317
|
+
|
|
318
|
+
// Only set up dashcam if enabled (default: true)
|
|
319
|
+
if (testdriver.dashcamEnabled) {
|
|
320
|
+
// Add testdriver log to dashcam tracking
|
|
321
|
+
await testdriver.dashcam.addFileLog(logPath, "TestDriver Log");
|
|
322
|
+
|
|
323
|
+
// Start dashcam recording (always, regardless of provision method)
|
|
324
|
+
await testdriver.dashcam.start();
|
|
325
|
+
}
|
|
326
|
+
})();
|
|
327
|
+
|
|
301
328
|
// Register cleanup handler with dashcam.stop()
|
|
302
329
|
// We always register a new cleanup handler because on retry we need to clean up the new instance
|
|
303
330
|
const cleanup = async () => {
|
|
@@ -307,50 +334,71 @@ export function TestDriver(context, options = {}) {
|
|
|
307
334
|
if (!currentInstance) {
|
|
308
335
|
return; // Already cleaned up
|
|
309
336
|
}
|
|
310
|
-
|
|
337
|
+
|
|
311
338
|
try {
|
|
312
339
|
// Ensure meta object exists
|
|
313
340
|
if (!context.task.meta) {
|
|
314
341
|
context.task.meta = {};
|
|
315
342
|
}
|
|
316
|
-
|
|
343
|
+
|
|
317
344
|
// Always set test metadata, even if dashcam never started or fails to stop
|
|
318
345
|
// This ensures the reporter can record test results even for early failures
|
|
319
|
-
const platform = currentInstance.os ||
|
|
320
|
-
const absolutePath =
|
|
346
|
+
const platform = currentInstance.os || "linux";
|
|
347
|
+
const absolutePath =
|
|
348
|
+
context.task.file?.filepath || context.task.file?.name || "unknown";
|
|
321
349
|
const projectRoot = process.cwd();
|
|
322
|
-
const testFile =
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
350
|
+
const testFile =
|
|
351
|
+
absolutePath !== "unknown"
|
|
352
|
+
? path.relative(projectRoot, absolutePath)
|
|
353
|
+
: absolutePath;
|
|
354
|
+
|
|
326
355
|
// Set basic metadata that's always available
|
|
327
356
|
context.task.meta.platform = platform;
|
|
328
357
|
context.task.meta.testFile = testFile;
|
|
329
358
|
context.task.meta.testOrder = 0;
|
|
330
359
|
context.task.meta.sessionId = currentInstance.getSessionId?.() || null;
|
|
331
|
-
|
|
360
|
+
|
|
332
361
|
// Stop dashcam if it was started - with timeout to prevent hanging
|
|
333
362
|
if (currentInstance._dashcam && currentInstance._dashcam.recording) {
|
|
334
363
|
try {
|
|
335
364
|
const dashcamUrl = await currentInstance.dashcam.stop();
|
|
336
|
-
console.log('');
|
|
337
|
-
console.log('đĨ' + chalk.yellow(` Dashcam URL`) + `: ${dashcamUrl}`);
|
|
338
|
-
console.log('');
|
|
339
|
-
|
|
340
365
|
// Add dashcam URL to metadata
|
|
341
366
|
context.task.meta.dashcamUrl = dashcamUrl || null;
|
|
342
|
-
|
|
367
|
+
|
|
343
368
|
// Also register in memory if plugin is available (for cross-process scenarios)
|
|
344
369
|
if (globalThis.__testdriverPlugin?.registerDashcamUrl) {
|
|
345
|
-
globalThis.__testdriverPlugin.registerDashcamUrl(
|
|
370
|
+
globalThis.__testdriverPlugin.registerDashcamUrl(
|
|
371
|
+
context.task.id,
|
|
372
|
+
dashcamUrl,
|
|
373
|
+
platform,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const debugMode =
|
|
378
|
+
process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
|
|
379
|
+
|
|
380
|
+
if (debugMode) {
|
|
381
|
+
console.log("");
|
|
382
|
+
console.log(
|
|
383
|
+
"đĨ" + chalk.yellow(` Dashcam URL`) + `: ${dashcamUrl}`,
|
|
384
|
+
);
|
|
385
|
+
console.log("");
|
|
346
386
|
}
|
|
347
387
|
} catch (error) {
|
|
348
388
|
// Log more detailed error information for debugging
|
|
349
|
-
console.error(
|
|
350
|
-
|
|
389
|
+
console.error(
|
|
390
|
+
"â Failed to stop dashcam:",
|
|
391
|
+
error.name || error.constructor?.name || "Error",
|
|
392
|
+
);
|
|
393
|
+
if (error.message) console.error(" Message:", error.message);
|
|
351
394
|
// NotFoundError during cleanup is expected if sandbox already terminated
|
|
352
|
-
if (
|
|
353
|
-
|
|
395
|
+
if (
|
|
396
|
+
error.name === "NotFoundError" ||
|
|
397
|
+
error.responseData?.error === "NotFoundError"
|
|
398
|
+
) {
|
|
399
|
+
console.log(
|
|
400
|
+
" âšī¸ Sandbox session already terminated - dashcam stop skipped",
|
|
401
|
+
);
|
|
354
402
|
}
|
|
355
403
|
// Mark as not recording to prevent retries
|
|
356
404
|
if (currentInstance._dashcam) {
|
|
@@ -363,34 +411,35 @@ export function TestDriver(context, options = {}) {
|
|
|
363
411
|
// No dashcam recording, set URL to null explicitly
|
|
364
412
|
context.task.meta.dashcamUrl = null;
|
|
365
413
|
}
|
|
366
|
-
|
|
414
|
+
|
|
367
415
|
// Clean up console spies
|
|
368
416
|
cleanupConsoleSpy(currentInstance);
|
|
369
|
-
|
|
417
|
+
|
|
370
418
|
// Wait for connection to finish if it was initiated
|
|
371
419
|
if (currentInstance.__connectionPromise) {
|
|
372
420
|
await currentInstance.__connectionPromise.catch(() => {}); // Ignore connection errors during cleanup
|
|
373
421
|
}
|
|
374
|
-
|
|
422
|
+
|
|
375
423
|
// Disconnect with timeout
|
|
376
424
|
await Promise.race([
|
|
377
425
|
currentInstance.disconnect(),
|
|
378
|
-
new Promise((resolve) => setTimeout(resolve, 5000)) // 5s timeout for disconnect
|
|
426
|
+
new Promise((resolve) => setTimeout(resolve, 5000)), // 5s timeout for disconnect
|
|
379
427
|
]);
|
|
380
|
-
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.error("Error disconnecting client:", error);
|
|
430
|
+
} finally {
|
|
381
431
|
// Terminate AWS instance if one was spawned for this test
|
|
382
432
|
// This must happen AFTER dashcam.stop() to ensure recording is saved
|
|
433
|
+
// AND it must happen even if disconnect() fails
|
|
383
434
|
if (globalThis.__testdriverAWS?.terminateInstance) {
|
|
384
435
|
await globalThis.__testdriverAWS.terminateInstance(context.task.id);
|
|
385
436
|
}
|
|
386
|
-
} catch (error) {
|
|
387
|
-
console.error('Error disconnecting client:', error);
|
|
388
437
|
}
|
|
389
438
|
};
|
|
390
439
|
lifecycleHandlers.set(context.task, cleanup);
|
|
391
|
-
|
|
440
|
+
|
|
392
441
|
// Vitest will call this automatically after the test (each retry attempt)
|
|
393
442
|
context.onTestFinished?.(cleanup);
|
|
394
|
-
|
|
443
|
+
|
|
395
444
|
return testdriver;
|
|
396
445
|
}
|