testdriverai 7.4.4 → 7.4.5
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/.github/copilot-instructions.md +1 -1
- package/.github/skills/testdriver:performing-actions/SKILL.md +3 -0
- package/.github/skills/testdriver:waiting-for-elements/SKILL.md +16 -0
- package/agent/lib/sandbox.js +11 -219
- package/ai/agents/testdriver.md +1 -1
- package/ai/skills/testdriver:performing-actions/SKILL.md +3 -0
- package/ai/skills/testdriver:testdriver/SKILL.md +1 -1
- package/ai/skills/testdriver:waiting-for-elements/SKILL.md +16 -0
- package/docs/guide/best-practices-polling.mdx +25 -5
- package/docs/v7/performing-actions.mdx +3 -0
- package/docs/v7/wait.mdx +52 -0
- package/docs/v7/waiting-for-elements.mdx +18 -0
- package/examples/chrome-extension.test.mjs +2 -0
- package/examples/no-provision.test.mjs +2 -0
- package/package.json +1 -1
- package/sdk.d.ts +14 -2
|
@@ -670,7 +670,7 @@ await testdriver.screenshot(1, false, true);
|
|
|
670
670
|
3. **⚠️ SHARE THE TEST REPORT URL** - After EVERY test run, find `TESTDRIVER_RUN_URL=https://console.testdriver.ai/runs/...` in the output and share it with the user. This is CRITICAL - users need to view the recording to understand what happened.
|
|
671
671
|
3. **Screenshots are automatic** - TestDriver captures screenshots before/after every command by default. Each screenshot filename includes the line number (e.g., `001-click-before-L42-submit-button.png`) making it easy to trace issues.
|
|
672
672
|
4. **⚠️ USE SCREENSHOT VIEWING FOR DEBUGGING** - When tests fail, use `list_local_screenshots` and `view_local_screenshot` MCP commands to see exactly what the UI looked like. The filenames tell you which line of code triggered each screenshot.
|
|
673
|
-
5.
|
|
673
|
+
5. **Use `wait()` for simple delays** - Use `await testdriver.wait(ms)` when you need a pause (e.g., after actions, for animations). For waiting for specific elements, prefer `find()` with a `timeout` option.
|
|
674
674
|
6. **Use MCP tools for development** - Build tests interactively with visual feedback
|
|
675
675
|
7. **Always check `sdk.d.ts`** for method signatures and types when debugging generated tests
|
|
676
676
|
8. **Look at test samples** in `node_modules/testdriverai/test` for working examples
|
|
@@ -29,6 +29,9 @@ await testdriver.find('dropdown menu').hover();
|
|
|
29
29
|
await testdriver.scroll('down', 500);
|
|
30
30
|
await testdriver.scrollUntilText('Footer content');
|
|
31
31
|
|
|
32
|
+
// Waiting
|
|
33
|
+
await testdriver.wait(2000); // Wait 2 seconds for animation/state change
|
|
34
|
+
|
|
32
35
|
// Extracting information from screen
|
|
33
36
|
const price = await testdriver.extract('the total price');
|
|
34
37
|
const orderNumber = await testdriver.extract('the order confirmation number');
|
|
@@ -70,3 +70,19 @@ const testdriver = TestDriver(context, {
|
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
72
|
```
|
|
73
|
+
|
|
74
|
+
## Simple Delays with `wait()`
|
|
75
|
+
|
|
76
|
+
For simple pauses — waiting for animations, transitions, or state changes after an action — use `wait()`:
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
// Wait for an animation to complete
|
|
80
|
+
await testdriver.find('menu toggle').click();
|
|
81
|
+
await testdriver.wait(2000);
|
|
82
|
+
|
|
83
|
+
// Wait for a page transition to settle
|
|
84
|
+
await testdriver.find('next page button').click();
|
|
85
|
+
await testdriver.wait(1000);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For waiting for specific **elements** to appear, prefer `find()` with a `timeout` option. Use `wait()` only for simple time-based pauses.
|
package/agent/lib/sandbox.js
CHANGED
|
@@ -38,16 +38,9 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
38
38
|
this.os = null; // Store OS value to send with every message
|
|
39
39
|
this.sessionInstance = sessionInstance; // Store session instance to include in messages
|
|
40
40
|
this.traceId = null; // Sentry trace ID for debugging
|
|
41
|
-
this.reconnectAttempts = 0;
|
|
42
|
-
this.maxReconnectAttempts = 10;
|
|
43
|
-
this.intentionalDisconnect = false;
|
|
44
41
|
this.apiRoot = null;
|
|
45
42
|
this.apiKey = null;
|
|
46
|
-
this.
|
|
47
|
-
this.reconnecting = false; // Prevent duplicate reconnection attempts
|
|
48
|
-
this.pendingTimeouts = new Map(); // Track per-message timeouts
|
|
49
|
-
this.pendingRetryQueue = []; // Queue of requests to retry after reconnection
|
|
50
|
-
this._lastConnectParams = null; // Connection params for reconnection (per-instance, not shared)
|
|
43
|
+
this._lastConnectParams = null; // Connection params for sandboxId injection
|
|
51
44
|
}
|
|
52
45
|
|
|
53
46
|
/**
|
|
@@ -118,7 +111,6 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
118
111
|
|
|
119
112
|
// Set up timeout to prevent hanging requests
|
|
120
113
|
const timeoutId = setTimeout(() => {
|
|
121
|
-
this.pendingTimeouts.delete(requestId);
|
|
122
114
|
if (this.ps[requestId]) {
|
|
123
115
|
delete this.ps[requestId];
|
|
124
116
|
rejectPromise(
|
|
@@ -133,19 +125,14 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
133
125
|
timeoutId.unref();
|
|
134
126
|
}
|
|
135
127
|
|
|
136
|
-
// Track timeout so close() can clear it
|
|
137
|
-
this.pendingTimeouts.set(requestId, timeoutId);
|
|
138
|
-
|
|
139
128
|
this.ps[requestId] = {
|
|
140
129
|
promise: p,
|
|
141
130
|
resolve: (result) => {
|
|
142
131
|
clearTimeout(timeoutId);
|
|
143
|
-
this.pendingTimeouts.delete(requestId);
|
|
144
132
|
resolvePromise(result);
|
|
145
133
|
},
|
|
146
134
|
reject: (error) => {
|
|
147
135
|
clearTimeout(timeoutId);
|
|
148
|
-
this.pendingTimeouts.delete(requestId);
|
|
149
136
|
rejectPromise(error);
|
|
150
137
|
},
|
|
151
138
|
message,
|
|
@@ -199,16 +186,9 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
199
186
|
}
|
|
200
187
|
|
|
201
188
|
/**
|
|
202
|
-
* Set connection params for
|
|
203
|
-
* Use this instead of directly assigning this._lastConnectParams from
|
|
204
|
-
* external code. Keeps the shape consistent and avoids stale state
|
|
205
|
-
* leaking across concurrent test runs.
|
|
189
|
+
* Set connection params for sandboxId injection.
|
|
206
190
|
* @param {Object|null} params
|
|
207
|
-
* @param {string} [params.type] - 'direct' for IP-based connections
|
|
208
|
-
* @param {string} [params.ip] - IP address for direct connections
|
|
209
191
|
* @param {string} [params.sandboxId] - Sandbox/instance ID
|
|
210
|
-
* @param {boolean} [params.persist] - Whether to persist the sandbox
|
|
211
|
-
* @param {number|null} [params.keepAlive] - Keep-alive TTL in ms
|
|
212
192
|
*/
|
|
213
193
|
setConnectionParams(params) {
|
|
214
194
|
this._lastConnectParams = params ? { ...params } : null;
|
|
@@ -238,163 +218,6 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
238
218
|
}
|
|
239
219
|
}
|
|
240
220
|
|
|
241
|
-
/**
|
|
242
|
-
* Reconnect to a direct IP-based sandbox after connection loss.
|
|
243
|
-
* Sends a 'direct' message instead of 'connect' to avoid the API
|
|
244
|
-
* treating the IP as an AWS instance ID.
|
|
245
|
-
*/
|
|
246
|
-
async reconnectDirect(ip) {
|
|
247
|
-
let reply = await this.send({
|
|
248
|
-
type: "direct",
|
|
249
|
-
ip,
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
if (reply.success) {
|
|
253
|
-
this.instanceSocketConnected = true;
|
|
254
|
-
emitter.emit(events.sandbox.connected);
|
|
255
|
-
return reply;
|
|
256
|
-
} else {
|
|
257
|
-
throw new Error(reply.errorMessage || "Failed to reconnect to direct sandbox");
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async handleConnectionLoss() {
|
|
262
|
-
if (this.intentionalDisconnect) return;
|
|
263
|
-
|
|
264
|
-
// Prevent duplicate reconnection attempts (both 'error' and 'close' fire)
|
|
265
|
-
if (this.reconnecting) return;
|
|
266
|
-
this.reconnecting = true;
|
|
267
|
-
|
|
268
|
-
// Remove listeners from the old socket to prevent "No pending promise found" warnings
|
|
269
|
-
// when late responses arrive on the dying connection
|
|
270
|
-
if (this.socket) {
|
|
271
|
-
try {
|
|
272
|
-
this.socket.removeAllListeners("message");
|
|
273
|
-
} catch (e) {
|
|
274
|
-
// Ignore errors removing listeners from closed socket
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Queue pending requests for retry after reconnection
|
|
279
|
-
// (they were sent on the old socket and will never receive responses)
|
|
280
|
-
const pendingRequestIds = Object.keys(this.ps);
|
|
281
|
-
if (pendingRequestIds.length > 0) {
|
|
282
|
-
console.log(`[Sandbox] Queuing ${pendingRequestIds.length} pending request(s) for retry after reconnection`);
|
|
283
|
-
for (const requestId of pendingRequestIds) {
|
|
284
|
-
const pending = this.ps[requestId];
|
|
285
|
-
if (pending) {
|
|
286
|
-
// Clear the timeout - we'll set a new one when we retry
|
|
287
|
-
const timeoutId = this.pendingTimeouts.get(requestId);
|
|
288
|
-
if (timeoutId) {
|
|
289
|
-
clearTimeout(timeoutId);
|
|
290
|
-
this.pendingTimeouts.delete(requestId);
|
|
291
|
-
}
|
|
292
|
-
// Queue for retry (store message and promise handlers)
|
|
293
|
-
this.pendingRetryQueue.push({
|
|
294
|
-
message: pending.message,
|
|
295
|
-
resolve: pending.resolve,
|
|
296
|
-
reject: pending.reject,
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
this.ps = {};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Cancel any existing reconnect timer
|
|
304
|
-
if (this.reconnectTimer) {
|
|
305
|
-
clearTimeout(this.reconnectTimer);
|
|
306
|
-
this.reconnectTimer = null;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
310
|
-
const errorMsg =
|
|
311
|
-
"Unable to reconnect to TestDriver sandbox after multiple attempts. Please check your internet connection.";
|
|
312
|
-
emitter.emit(events.error.sandbox, errorMsg);
|
|
313
|
-
console.error(errorMsg);
|
|
314
|
-
|
|
315
|
-
// Reject all queued requests since reconnection failed
|
|
316
|
-
if (this.pendingRetryQueue.length > 0) {
|
|
317
|
-
console.log(`[Sandbox] Rejecting ${this.pendingRetryQueue.length} queued request(s) - reconnection failed`);
|
|
318
|
-
for (const queued of this.pendingRetryQueue) {
|
|
319
|
-
queued.reject(new Error("Sandbox reconnection failed after multiple attempts"));
|
|
320
|
-
}
|
|
321
|
-
this.pendingRetryQueue = [];
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
this.reconnecting = false;
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
this.reconnectAttempts++;
|
|
329
|
-
const delay = Math.min(1000 * 2 ** (this.reconnectAttempts - 1), 60000);
|
|
330
|
-
|
|
331
|
-
console.log(
|
|
332
|
-
`[Sandbox] Connection lost. Reconnecting in ${delay}ms... (Attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
|
|
333
|
-
);
|
|
334
|
-
|
|
335
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
336
|
-
this.reconnectTimer = null;
|
|
337
|
-
try {
|
|
338
|
-
await this.boot(this.apiRoot);
|
|
339
|
-
if (this.apiKey) {
|
|
340
|
-
await this.auth(this.apiKey);
|
|
341
|
-
}
|
|
342
|
-
// Re-establish sandbox connection on the new API instance
|
|
343
|
-
// Without this, the new API instance has no connection.desktop
|
|
344
|
-
// and all Linux operations will fail with "sandbox not initialized"
|
|
345
|
-
if (this._lastConnectParams) {
|
|
346
|
-
if (this._lastConnectParams.type === 'direct') {
|
|
347
|
-
// Direct IP connections must reconnect via 'direct' message, not 'connect'
|
|
348
|
-
const { ip, persist, keepAlive } = this._lastConnectParams;
|
|
349
|
-
console.log(`[Sandbox] Re-establishing direct connection (${ip})...`);
|
|
350
|
-
await this.reconnectDirect(ip);
|
|
351
|
-
} else {
|
|
352
|
-
const { sandboxId, persist, keepAlive } = this._lastConnectParams;
|
|
353
|
-
console.log(`[Sandbox] Re-establishing sandbox connection (${sandboxId})...`);
|
|
354
|
-
await this.connect(sandboxId, persist, keepAlive);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
console.log("[Sandbox] Reconnected successfully.");
|
|
358
|
-
|
|
359
|
-
// Retry queued requests
|
|
360
|
-
await this._retryQueuedRequests();
|
|
361
|
-
} catch (e) {
|
|
362
|
-
// boot's close handler will trigger handleConnectionLoss again
|
|
363
|
-
} finally {
|
|
364
|
-
this.reconnecting = false;
|
|
365
|
-
}
|
|
366
|
-
}, delay);
|
|
367
|
-
// Don't let reconnect timer prevent Node process from exiting
|
|
368
|
-
if (this.reconnectTimer.unref) {
|
|
369
|
-
this.reconnectTimer.unref();
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Retry queued requests after successful reconnection
|
|
375
|
-
* @private
|
|
376
|
-
*/
|
|
377
|
-
async _retryQueuedRequests() {
|
|
378
|
-
if (this.pendingRetryQueue.length === 0) return;
|
|
379
|
-
|
|
380
|
-
console.log(`[Sandbox] Retrying ${this.pendingRetryQueue.length} queued request(s)...`);
|
|
381
|
-
|
|
382
|
-
// Take all queued requests and clear the queue
|
|
383
|
-
const toRetry = this.pendingRetryQueue.splice(0);
|
|
384
|
-
|
|
385
|
-
for (const queued of toRetry) {
|
|
386
|
-
try {
|
|
387
|
-
// Re-send the message and resolve/reject the original promise
|
|
388
|
-
const result = await this.send(queued.message);
|
|
389
|
-
queued.resolve(result);
|
|
390
|
-
} catch (err) {
|
|
391
|
-
queued.reject(err);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
console.log(`[Sandbox] Finished retrying queued requests.`);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
221
|
async boot(apiRoot) {
|
|
399
222
|
if (apiRoot) this.apiRoot = apiRoot;
|
|
400
223
|
return new Promise((resolve, reject) => {
|
|
@@ -424,12 +247,7 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
424
247
|
this.socket.on("close", () => {
|
|
425
248
|
clearInterval(this.heartbeat);
|
|
426
249
|
this.apiSocketConnected = false;
|
|
427
|
-
// Also mark instance socket as disconnected to prevent sending messages
|
|
428
|
-
// to a closed connection (e.g., when sandbox is killed due to test failure)
|
|
429
250
|
this.instanceSocketConnected = false;
|
|
430
|
-
// Reset reconnecting flag so handleConnectionLoss can run for this new disconnection
|
|
431
|
-
this.reconnecting = false;
|
|
432
|
-
this.handleConnectionLoss();
|
|
433
251
|
reject();
|
|
434
252
|
});
|
|
435
253
|
|
|
@@ -439,14 +257,10 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
439
257
|
clearInterval(this.heartbeat);
|
|
440
258
|
emitter.emit(events.error.sandbox, err);
|
|
441
259
|
this.apiSocketConnected = false;
|
|
442
|
-
// Don't call handleConnectionLoss here - the 'close' event always fires
|
|
443
|
-
// after 'error', so let 'close' handle reconnection to avoid duplicate attempts
|
|
444
260
|
reject(err);
|
|
445
261
|
});
|
|
446
262
|
|
|
447
263
|
this.socket.on("open", async () => {
|
|
448
|
-
this.reconnectAttempts = 0;
|
|
449
|
-
this.reconnecting = false;
|
|
450
264
|
this.apiSocketConnected = true;
|
|
451
265
|
|
|
452
266
|
this.heartbeat = setInterval(() => {
|
|
@@ -475,18 +289,15 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
475
289
|
}
|
|
476
290
|
|
|
477
291
|
if (!this.ps[message.requestId]) {
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
message.requestId,
|
|
488
|
-
);
|
|
489
|
-
}
|
|
292
|
+
// Can happen after timeout (promise was deleted). Expected during
|
|
293
|
+
// polling loops where short-timeout exec calls regularly expire
|
|
294
|
+
// before the sandbox responds. Only log in debug/verbose mode.
|
|
295
|
+
const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
|
|
296
|
+
if (debugMode) {
|
|
297
|
+
console.warn(
|
|
298
|
+
"No pending promise found for requestId:",
|
|
299
|
+
message.requestId,
|
|
300
|
+
);
|
|
490
301
|
}
|
|
491
302
|
return;
|
|
492
303
|
}
|
|
@@ -514,27 +325,12 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
514
325
|
* Close the WebSocket connection and clean up resources
|
|
515
326
|
*/
|
|
516
327
|
close() {
|
|
517
|
-
this.intentionalDisconnect = true;
|
|
518
|
-
this.reconnecting = false;
|
|
519
|
-
// Cancel any pending reconnect timer
|
|
520
|
-
if (this.reconnectTimer) {
|
|
521
|
-
clearTimeout(this.reconnectTimer);
|
|
522
|
-
this.reconnectTimer = null;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
328
|
if (this.heartbeat) {
|
|
526
329
|
clearInterval(this.heartbeat);
|
|
527
330
|
this.heartbeat = null;
|
|
528
331
|
}
|
|
529
332
|
|
|
530
|
-
// Clear all pending message timeouts to prevent timers keeping the process alive
|
|
531
|
-
for (const timeoutId of this.pendingTimeouts.values()) {
|
|
532
|
-
clearTimeout(timeoutId);
|
|
533
|
-
}
|
|
534
|
-
this.pendingTimeouts.clear();
|
|
535
|
-
|
|
536
333
|
if (this.socket) {
|
|
537
|
-
// Remove all listeners before closing to prevent reconnect attempts
|
|
538
334
|
this.socket.removeAllListeners();
|
|
539
335
|
try {
|
|
540
336
|
this.socket.close();
|
|
@@ -549,11 +345,7 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
|
549
345
|
this.authenticated = false;
|
|
550
346
|
this.instance = null;
|
|
551
347
|
this._lastConnectParams = null;
|
|
552
|
-
|
|
553
|
-
// Silently clear pending promises and retry queue without rejecting
|
|
554
|
-
// (rejecting causes unhandled promise rejections during cleanup)
|
|
555
348
|
this.ps = {};
|
|
556
|
-
this.pendingRetryQueue = [];
|
|
557
349
|
}
|
|
558
350
|
}
|
|
559
351
|
|
package/ai/agents/testdriver.md
CHANGED
|
@@ -621,7 +621,7 @@ await testdriver.screenshot(1, false, true);
|
|
|
621
621
|
3. **⚠️ SHARE THE TEST REPORT URL** - After EVERY test run, find `TESTDRIVER_RUN_URL=https://console.testdriver.ai/runs/...` in the output and share it with the user. This is CRITICAL - users need to view the recording to understand what happened.
|
|
622
622
|
3. **Screenshots are automatic** - TestDriver captures screenshots before/after every command by default. Each screenshot filename includes the line number (e.g., `001-click-before-L42-submit-button.png`) making it easy to trace issues.
|
|
623
623
|
4. **⚠️ USE SCREENSHOT VIEWING FOR DEBUGGING** - When tests fail, use `list_local_screenshots` and `view_local_screenshot` MCP commands to see exactly what the UI looked like. The filenames tell you which line of code triggered each screenshot.
|
|
624
|
-
5.
|
|
624
|
+
5. **Use `wait()` for simple delays** - Use `await testdriver.wait(ms)` when you need a pause (e.g., after actions, for animations). For waiting for specific elements, prefer `find()` with a `timeout` option.
|
|
625
625
|
6. **Use MCP tools for development** - Build tests interactively with visual feedback
|
|
626
626
|
7. **Always check `sdk.d.ts`** for method signatures and types when debugging generated tests
|
|
627
627
|
8. **Look at test samples** in `node_modules/testdriverai/test` for working examples
|
|
@@ -29,6 +29,9 @@ await testdriver.find('dropdown menu').hover();
|
|
|
29
29
|
await testdriver.scroll('down', 500);
|
|
30
30
|
await testdriver.scrollUntilText('Footer content');
|
|
31
31
|
|
|
32
|
+
// Waiting
|
|
33
|
+
await testdriver.wait(2000); // Wait 2 seconds for animation/state change
|
|
34
|
+
|
|
32
35
|
// Extracting information from screen
|
|
33
36
|
const price = await testdriver.extract('the total price');
|
|
34
37
|
const orderNumber = await testdriver.extract('the order confirmation number');
|
|
@@ -611,7 +611,7 @@ await testdriver.screenshot(1, false, true);
|
|
|
611
611
|
3. **⚠️ SHARE THE TEST REPORT URL** - After EVERY test run, find `TESTDRIVER_RUN_URL=https://console.testdriver.ai/runs/...` in the output and share it with the user. This is CRITICAL - users need to view the recording to understand what happened.
|
|
612
612
|
3. **Screenshots are automatic** - TestDriver captures screenshots before/after every command by default. Each screenshot filename includes the line number (e.g., `001-click-before-L42-submit-button.png`) making it easy to trace issues.
|
|
613
613
|
4. **⚠️ USE SCREENSHOT VIEWING FOR DEBUGGING** - When tests fail, use `list_local_screenshots` and `view_local_screenshot` MCP commands to see exactly what the UI looked like. The filenames tell you which line of code triggered each screenshot.
|
|
614
|
-
5.
|
|
614
|
+
5. **Use `wait()` for simple delays** - Use `await testdriver.wait(ms)` when you need a pause (e.g., after actions, for animations). For waiting for specific elements, prefer `find()` with a `timeout` option.
|
|
615
615
|
6. **Use MCP tools for development** - Build tests interactively with visual feedback
|
|
616
616
|
7. **Always check `sdk.d.ts`** for method signatures and types when debugging generated tests
|
|
617
617
|
8. **Look at test samples** in `node_modules/testdriverai/test` for working examples
|
|
@@ -70,3 +70,19 @@ const testdriver = TestDriver(context, {
|
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
72
|
```
|
|
73
|
+
|
|
74
|
+
## Simple Delays with `wait()`
|
|
75
|
+
|
|
76
|
+
For simple pauses — waiting for animations, transitions, or state changes after an action — use `wait()`:
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
// Wait for an animation to complete
|
|
80
|
+
await testdriver.find('menu toggle').click();
|
|
81
|
+
await testdriver.wait(2000);
|
|
82
|
+
|
|
83
|
+
// Wait for a page transition to settle
|
|
84
|
+
await testdriver.find('next page button').click();
|
|
85
|
+
await testdriver.wait(1000);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For waiting for specific **elements** to appear, prefer `find()` with a `timeout` option. Use `wait()` only for simple time-based pauses.
|
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
# Best Practices: Element Polling
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**When waiting for elements to appear, prefer `find()` with a `timeout` option over `wait()`.**
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## When to Use `wait()` vs `find()` with Timeout
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`wait()` is useful for **simple pauses** — after actions, for animations, or for state changes to settle:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
await testdriver.find('submit button').click();
|
|
11
|
+
await testdriver.wait(2000); // Wait for animation to complete
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
However, **don't use `wait()` to wait for elements to appear**. For that, use `find()` with a `timeout`:
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
// ✅ GOOD: Polls until the element appears (up to 30s)
|
|
18
|
+
const element = await testdriver.find('success message', { timeout: 30000 });
|
|
19
|
+
|
|
20
|
+
// ❌ BAD: Arbitrary wait then hope the element is there
|
|
21
|
+
await testdriver.wait(5000);
|
|
22
|
+
const element = await testdriver.find('success message');
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Why Prefer `find()` with Timeout for Element Waiting?
|
|
26
|
+
|
|
27
|
+
Using arbitrary waits for element detection has problems:
|
|
8
28
|
|
|
9
29
|
1. **Brittle**: Fixed timeouts may be too short (causing flaky tests) or too long (wasting time)
|
|
10
30
|
2. **Slow**: You always wait the full duration, even if the element appears sooner
|
|
@@ -147,8 +167,8 @@ await waitForElement(testdriver, "Processing complete indicator", 10, 2000);
|
|
|
147
167
|
| Pattern | Use Case |
|
|
148
168
|
|---------|----------|
|
|
149
169
|
| **Polling with `find()`** | ✅ Waiting for UI elements to appear or disappear |
|
|
150
|
-
| **`wait()`** | ❌
|
|
170
|
+
| **`wait()`** | ✅ Simple delays (animations, state changes) — ❌ Don't use for element waiting |
|
|
151
171
|
| **Helper function** | ✅ Recommended for cleaner, reusable code |
|
|
152
172
|
| **Conditional polling** | ✅ For optional elements (dialogs, notifications) |
|
|
153
173
|
|
|
154
|
-
Remember: **If you're waiting for something to appear on screen, use `find()`
|
|
174
|
+
Remember: **If you're waiting for something to appear on screen, use `find()` with a `timeout` option, not `wait()`. Use `wait()` for simple pauses between actions.**
|
|
@@ -29,6 +29,9 @@ await testdriver.find('dropdown menu').hover();
|
|
|
29
29
|
await testdriver.scroll('down', 500);
|
|
30
30
|
await testdriver.scrollUntilText('Footer content');
|
|
31
31
|
|
|
32
|
+
// Waiting
|
|
33
|
+
await testdriver.wait(2000); // Wait 2 seconds for animation/state change
|
|
34
|
+
|
|
32
35
|
// Extracting information from screen
|
|
33
36
|
const price = await testdriver.extract('the total price');
|
|
34
37
|
const orderNumber = await testdriver.extract('the order confirmation number');
|
package/docs/v7/wait.mdx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "wait"
|
|
3
|
+
sidebarTitle: "wait"
|
|
4
|
+
description: "Pause the execution of the script for a specified duration."
|
|
5
|
+
icon: "clock"
|
|
6
|
+
"mode": "wide"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Description
|
|
10
|
+
|
|
11
|
+
The `wait` method pauses test execution for a specified number of milliseconds before continuing. This is useful for adding delays between actions, waiting for animations to complete, or pausing for state changes to settle.
|
|
12
|
+
|
|
13
|
+
## Syntax
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
await testdriver.wait(timeout);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Arguments
|
|
20
|
+
|
|
21
|
+
| Argument | Type | Default | Description |
|
|
22
|
+
| --------- | -------- | ------- | ------------------------------------- |
|
|
23
|
+
| `timeout` | `number` | `3000` | The duration in milliseconds to wait. |
|
|
24
|
+
|
|
25
|
+
## Examples
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
// Wait 2 seconds for an animation to complete
|
|
29
|
+
await testdriver.find('submit button').click();
|
|
30
|
+
await testdriver.wait(2000);
|
|
31
|
+
|
|
32
|
+
// Wait 5 seconds
|
|
33
|
+
await testdriver.wait(5000);
|
|
34
|
+
|
|
35
|
+
// Wait with default timeout (3 seconds)
|
|
36
|
+
await testdriver.wait();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Best Practices
|
|
40
|
+
|
|
41
|
+
- **Use for simple delays** — waiting for animations, transitions, or state changes after an action.
|
|
42
|
+
- **Avoid for element waiting** — if you're waiting for a specific element to appear, use `find()` with a `timeout` option instead:
|
|
43
|
+
```javascript
|
|
44
|
+
// ✅ Better for waiting for elements
|
|
45
|
+
const element = await testdriver.find('success message', { timeout: 30000 });
|
|
46
|
+
|
|
47
|
+
// ❌ Don't do this for element waiting
|
|
48
|
+
await testdriver.wait(5000);
|
|
49
|
+
const element = await testdriver.find('success message');
|
|
50
|
+
```
|
|
51
|
+
- Avoid excessively long timeouts to keep tests efficient.
|
|
52
|
+
- Use sparingly — TestDriver's [redraw detection](/v7/waiting-for-elements) automatically waits for screen and network stability after each action.
|
|
@@ -70,3 +70,21 @@ const testdriver = TestDriver(context, {
|
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
72
|
```
|
|
73
|
+
|
|
74
|
+
## Simple Delays with `wait()`
|
|
75
|
+
|
|
76
|
+
For simple pauses — waiting for animations, transitions, or state changes after an action — use `wait()`:
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
// Wait for an animation to complete
|
|
80
|
+
await testdriver.find('menu toggle').click();
|
|
81
|
+
await testdriver.wait(2000);
|
|
82
|
+
|
|
83
|
+
// Wait for a page transition to settle
|
|
84
|
+
await testdriver.find('next page button').click();
|
|
85
|
+
await testdriver.wait(1000);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
<Note>
|
|
89
|
+
For waiting for specific **elements** to appear, prefer `find()` with a `timeout` option. Use `wait()` only for simple time-based pauses.
|
|
90
|
+
</Note>
|
|
@@ -60,6 +60,8 @@ describe("Chrome Extension Test", () => {
|
|
|
60
60
|
const helloExtension = await testdriver.find("Hello Extensions extension in the extensions dropdown");
|
|
61
61
|
await helloExtension.click();
|
|
62
62
|
|
|
63
|
+
await testdriver.wait(2000); // wait for the popup to open
|
|
64
|
+
|
|
63
65
|
// Verify the extension popup shows "Hello Extensions" text
|
|
64
66
|
const popupResult = await testdriver.assert("a popup shows with the text 'Hello Extensions'");
|
|
65
67
|
expect(popupResult).toBeTruthy();
|
|
@@ -10,6 +10,8 @@ import { getDefaults } from "./config.mjs";
|
|
|
10
10
|
describe("Assert Test", () => {
|
|
11
11
|
it("should assert the testdriver login page shows", async (context) => {
|
|
12
12
|
const testdriver = TestDriver(context, { ...getDefaults(context) });
|
|
13
|
+
|
|
14
|
+
await testdriver.wait(10000)
|
|
13
15
|
|
|
14
16
|
// Assert the TestDriver.ai Sandbox login page is displayed
|
|
15
17
|
const result = await testdriver.assert(
|
package/package.json
CHANGED
package/sdk.d.ts
CHANGED
|
@@ -1408,10 +1408,22 @@ export default class TestDriverSDK {
|
|
|
1408
1408
|
parse(): Promise<ParseResult>;
|
|
1409
1409
|
|
|
1410
1410
|
/**
|
|
1411
|
-
* Wait for specified time
|
|
1412
|
-
*
|
|
1411
|
+
* Wait for specified time. Useful for adding delays between actions, waiting for
|
|
1412
|
+
* animations to complete, or pausing for state changes to settle.
|
|
1413
|
+
*
|
|
1414
|
+
* For waiting for specific elements to appear, prefer `find()` with a `timeout` option instead.
|
|
1415
|
+
*
|
|
1413
1416
|
* @param timeout - Time to wait in milliseconds (default: 3000)
|
|
1414
1417
|
* @param options - Additional options (reserved for future use)
|
|
1418
|
+
*
|
|
1419
|
+
* @example
|
|
1420
|
+
* // Wait 2 seconds for an animation to complete
|
|
1421
|
+
* await testdriver.wait(2000);
|
|
1422
|
+
*
|
|
1423
|
+
* @example
|
|
1424
|
+
* // Wait after a click for state to settle
|
|
1425
|
+
* await testdriver.find('submit button').click();
|
|
1426
|
+
* await testdriver.wait(1000);
|
|
1415
1427
|
*/
|
|
1416
1428
|
wait(timeout?: number, options?: object): Promise<void>;
|
|
1417
1429
|
|