@testdriverai/agent 7.8.0-canary.15 → 7.8.0-canary.18

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/agent/index.js CHANGED
@@ -70,6 +70,7 @@ class TestDriverAgent extends EventEmitter2 {
70
70
  this.sandboxId = flags["sandbox-id"] || null;
71
71
  this.sandboxAmi = flags["sandbox-ami"] || null;
72
72
  this.sandboxInstance = flags["sandbox-instance"] || null;
73
+ this.e2bTemplateId = flags["e2b-template-id"] || null;
73
74
  this.sandboxOs = flags.os || "linux";
74
75
  this.ip = flags.ip || null;
75
76
  this.workingDir = flags.workingDir || process.cwd();
@@ -2188,6 +2189,9 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2188
2189
  if (this.sandboxInstance) {
2189
2190
  sandboxConfig.instanceType = this.sandboxInstance;
2190
2191
  }
2192
+ if (this.e2bTemplateId) {
2193
+ sandboxConfig.e2bTemplateId = this.e2bTemplateId;
2194
+ }
2191
2195
  // Add keepAlive TTL if specified
2192
2196
  if (this.keepAlive !== undefined && this.keepAlive !== null) {
2193
2197
  sandboxConfig.keepAlive = this.keepAlive;
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  const useStderr = process.env.TD_STDIO === 'stderr';
10
+ const isDebug = process.env.TD_DEBUG === 'true' || process.env.VERBOSE === 'true';
10
11
 
11
12
  /**
12
13
  * Log a message - uses stdout by default, stderr if TD_STDIO=stderr
@@ -40,6 +41,19 @@ function warn(...args) {
40
41
  }
41
42
  }
42
43
 
44
+ /**
45
+ * Log a debug message - only outputs when DEBUG=true
46
+ * @param {...any} args - Arguments to log
47
+ */
48
+ function debug(...args) {
49
+ if (!isDebug) return;
50
+ if (useStderr) {
51
+ console.error(...args);
52
+ } else {
53
+ console.log(...args);
54
+ }
55
+ }
56
+
43
57
  /**
44
58
  * Check if logger is configured to use stderr
45
59
  * @returns {boolean}
@@ -50,6 +64,7 @@ function isStderrMode() {
50
64
 
51
65
  module.exports = {
52
66
  log,
67
+ debug,
53
68
  error,
54
69
  warn,
55
70
  isStderrMode,
@@ -29,7 +29,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
29
29
  this._lastConnectParams = null;
30
30
  this._teamId = null;
31
31
  this._sandboxId = null;
32
- this._disconnectedAt = null; // tracks when Ably connection dropped (for timeout extension on reconnect)
32
+ this._disconnectedAt = null; // tracks when Realtime connection dropped (for timeout extension on reconnect)
33
33
 
34
34
  // Rate limiting state for Ably publishes (Ably limits to 50 msg/sec per connection)
35
35
  this._publishLastTime = 0;
@@ -49,6 +49,10 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
49
49
  );
50
50
  }
51
51
 
52
+ getPublishCount() {
53
+ return this._publishCount;
54
+ }
55
+
52
56
  async _initAbly(ablyToken, channelName) {
53
57
  if (this._ably) {
54
58
  try {
@@ -85,21 +89,21 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
85
89
  suspendedRetryTimeout: 15000, // retry from suspended every 15s (default 30s)
86
90
  });
87
91
 
88
- logger.log(`[ably] Connecting as sdk-${this._sandboxId}...`);
92
+ logger.debug(`[realtime] Connecting as sdk-${this._sandboxId}...`);
89
93
 
90
94
  await new Promise(function (resolve, reject) {
91
95
  self._ably.connection.on("connected", resolve);
92
96
  self._ably.connection.on("failed", function () {
93
- reject(new Error("Ably connection failed"));
97
+ reject(new Error("Realtime connection failed"));
94
98
  });
95
99
  setTimeout(function () {
96
- reject(new Error("Ably connection timeout"));
100
+ reject(new Error("Realtime connection timeout"));
97
101
  }, 30000);
98
102
  });
99
103
 
100
104
  this._sessionChannel = this._ably.channels.get(channelName);
101
105
 
102
- logger.log(`[ably] Channel initialized: ${channelName}`);
106
+ logger.debug(`[realtime] Channel initialized: ${channelName}`);
103
107
 
104
108
  // Enter presence on the session channel so the API can count connected SDK clients
105
109
  try {
@@ -107,7 +111,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
107
111
  sandboxId: this._sandboxId,
108
112
  connectedAt: Date.now(),
109
113
  });
110
- logger.log(`[ably] Entered presence on session channel (sandbox=${this._sandboxId})`);
114
+ logger.debug(`[realtime] Entered presence on session channel (sandbox=${this._sandboxId})`);
111
115
  } catch (e) {
112
116
  // Non-fatal — presence is used for concurrency counting, not critical path
113
117
  logger.warn("Failed to enter presence on session channel: " + (e.message || e));
@@ -118,7 +122,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
118
122
  var message = msg.data;
119
123
  if (!message) return;
120
124
 
121
- logger.log(`[ably] Received response: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
125
+ logger.debug(`[realtime] Received response: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
122
126
 
123
127
  if (message.type === "sandbox.progress") {
124
128
  emitter.emit(events.sandbox.progress, {
@@ -177,8 +181,8 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
177
181
  return rid + '(' + (e && e.message ? e.message.type : '?') + ')';
178
182
  }).join(', ')
179
183
  : 'none';
180
- logger.warn(
181
- '[ably] No pending promise for requestId=' + (message.requestId || 'null') +
184
+ logger.debug(
185
+ '[realtime] No pending promise for requestId=' + (message.requestId || 'null') +
182
186
  ' | response type=' + (message.type || 'unknown') +
183
187
  ' | error=' + (message.error ? (message.errorMessage || 'true') : 'false') +
184
188
  ' | currently pending: [' + pendingSummary + ']'
@@ -192,8 +196,8 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
192
196
  var pendingAge = pendingEntry && pendingEntry.startTime
193
197
  ? ((Date.now() - pendingEntry.startTime) / 1000).toFixed(1) + 's'
194
198
  : '?';
195
- logger.warn(
196
- '[ably] Promise REJECTED: requestId=' + message.requestId +
199
+ logger.debug(
200
+ '[realtime] Promise REJECTED: requestId=' + message.requestId +
197
201
  ' | type=' + (pendingMessage ? pendingMessage.type : 'unknown') +
198
202
  ' | age=' + pendingAge +
199
203
  ' | error=' + (message.errorMessage || 'Sandbox error')
@@ -212,8 +216,8 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
212
216
  var resolveAge = resolveEntry.startTime
213
217
  ? ((Date.now() - resolveEntry.startTime) / 1000).toFixed(1) + 's'
214
218
  : '?';
215
- logger.log(
216
- '[ably] Promise RESOLVED: requestId=' + message.requestId +
219
+ logger.debug(
220
+ '[realtime] Promise RESOLVED: requestId=' + message.requestId +
217
221
  ' | type=' + (resolveEntry.message ? resolveEntry.message.type : 'unknown') +
218
222
  ' | age=' + resolveAge
219
223
  );
@@ -241,7 +245,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
241
245
  this._onFileMsg = function (msg) {
242
246
  var message = msg.data;
243
247
  if (!message) return;
244
- logger.log(`[ably] Received file: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
248
+ logger.debug(`[realtime] Received file: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
245
249
  if (message.requestId && self.ps[message.requestId]) {
246
250
  emitter.emit(events.sandbox.received);
247
251
  self.ps[message.requestId].resolve(message);
@@ -260,7 +264,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
260
264
  const chState = this._sessionChannel ? this._sessionChannel.state : 'null';
261
265
  const pendingIds = Object.keys(this.ps);
262
266
  const pending = pendingIds.length;
263
- logger.log(`[ably][stats] connection=${connState} | sandbox=${this._sandboxId} | pending=${pending} | channel=${chState}`);
267
+ logger.debug(`[realtime][stats] connection=${connState} | sandbox=${this._sandboxId} | pending=${pending} | channel=${chState}`);
264
268
  if (pending > 0) {
265
269
  const now = Date.now();
266
270
  for (const rid of pendingIds) {
@@ -268,20 +272,20 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
268
272
  if (!entry) continue;
269
273
  const type = entry.message ? entry.message.type : 'unknown';
270
274
  const ageSec = ((now - (entry.startTime || now)) / 1000).toFixed(1);
271
- logger.log(`[ably][stats] pending: requestId=${rid} | type=${type} | age=${ageSec}s`);
275
+ logger.debug(`[realtime][stats] pending: requestId=${rid} | type=${type} | age=${ageSec}s`);
272
276
  }
273
277
  }
274
278
  }, 10000);
275
279
  if (this._statsInterval.unref) this._statsInterval.unref();
276
280
 
277
281
  this._ably.connection.on("disconnected", function () {
278
- logger.log("[ably] Connection: disconnected - will auto-reconnect");
282
+ logger.debug("[realtime] Connection: disconnected - will auto-reconnect");
279
283
  self._disconnectedAt = Date.now();
280
284
  });
281
285
 
282
286
  this._ably.connection.on("connected", function () {
283
287
  // Log reconnection so the user knows the blip was recovered
284
- logger.log("[ably] Connection: reconnected");
288
+ logger.debug("[realtime] Connection: reconnected");
285
289
  // Extend any pending command timeouts by the disconnection duration so
286
290
  // commands whose timer was counting down while the connection was down
287
291
  // don't get incorrectly timed out.
@@ -290,8 +294,8 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
290
294
  self._disconnectedAt = null;
291
295
  var pendingIds = Object.keys(self.ps);
292
296
  if (pendingIds.length > 0) {
293
- logger.log(
294
- '[ably] Extending ' + pendingIds.length + ' pending timeout(s) by ' +
297
+ logger.debug(
298
+ '[realtime] Extending ' + pendingIds.length + ' pending timeout(s) by ' +
295
299
  disconnectionDurationMs + 'ms after disconnection'
296
300
  );
297
301
  for (var i = 0; i < pendingIds.length; i++) {
@@ -305,14 +309,14 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
305
309
  });
306
310
 
307
311
  this._ably.connection.on("suspended", function () {
308
- logger.warn("[ably] Connection: suspended - connection lost for extended period, will keep retrying");
312
+ logger.debug("[realtime] Connection: suspended - connection lost for extended period, will keep retrying");
309
313
  });
310
314
 
311
315
  this._ably.connection.on("failed", function () {
312
- logger.error("[ably] Connection: failed");
316
+ logger.debug("[realtime] Connection: failed");
313
317
  self.apiSocketConnected = false;
314
318
  self.instanceSocketConnected = false;
315
- emitter.emit(events.error.sandbox, "Ably connection failed");
319
+ emitter.emit(events.error.sandbox, "Realtime connection failed");
316
320
  });
317
321
 
318
322
  // ─── Channel discontinuity detection ──────────────────────────────
@@ -326,7 +330,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
326
330
  var reasonMsg = reason ? (reason.message || reason.code || String(reason)) : '';
327
331
 
328
332
  if (current === 'attached' && stateChange.resumed === false && previous === 'attached') {
329
- logger.warn('[ably] Channel DISCONTINUITY detected (resumed=false)' + (reasonMsg ? ' — ' + reasonMsg : ''));
333
+ logger.debug('[realtime] Channel DISCONTINUITY detected (resumed=false)' + (reasonMsg ? ' — ' + reasonMsg : ''));
330
334
  emitter.emit(events.sandbox.progress, {
331
335
  step: 'discontinuity',
332
336
  message: 'Recovering missed messages after connection interruption...',
@@ -353,7 +357,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
353
357
  var entry = subs[i];
354
358
  if (!entry.sub) continue;
355
359
  try {
356
- logger.log('[ably] Discontinuity recovery: fetching historyBeforeSubscribe for ' + entry.name + '...');
360
+ logger.debug('[realtime] Discontinuity recovery: fetching historyBeforeSubscribe for ' + entry.name + '...');
357
361
  var page = await entry.sub.historyBeforeSubscribe({ limit: 100 });
358
362
  var recovered = 0;
359
363
  while (page) {
@@ -363,25 +367,25 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
363
367
  recovered++;
364
368
  try {
365
369
  if (entry.handler) {
366
- logger.log('[ably] Replaying recovered ' + entry.name + ' message (requestId=' + (page.items[j].data && page.items[j].data.requestId || 'none') + ')');
370
+ logger.debug('[realtime] Replaying recovered ' + entry.name + ' message (requestId=' + (page.items[j].data && page.items[j].data.requestId || 'none') + ')');
367
371
  entry.handler(page.items[j]);
368
372
  }
369
373
  } catch (replayErr) {
370
- logger.error('[ably] Error replaying recovered message: ' + (replayErr.message || replayErr));
374
+ logger.debug('[realtime] Error replaying recovered message: ' + (replayErr.message || replayErr));
371
375
  }
372
376
  }
373
377
  page = page.hasNext() ? await page.next() : null;
374
378
  }
375
379
  totalRecovered += recovered;
376
- logger.log('[ably] Discontinuity recovery: replayed ' + recovered + ' ' + entry.name + ' message(s) from gap');
380
+ logger.debug('[realtime] Discontinuity recovery: replayed ' + recovered + ' ' + entry.name + ' message(s) from gap');
377
381
  } catch (err) {
378
- logger.error('[ably] Discontinuity recovery failed for ' + entry.name + ': ' + (err.message || err));
382
+ logger.debug('[realtime] Discontinuity recovery failed for ' + entry.name + ': ' + (err.message || err));
379
383
  }
380
384
  }
381
385
  if (totalRecovered > 0) {
382
- logger.warn('[ably] Recovered and replayed ' + totalRecovered + ' message(s) that were missed during connection interruption');
386
+ logger.debug('[realtime] Recovered and replayed ' + totalRecovered + ' message(s) that were missed during connection interruption');
383
387
  } else {
384
- logger.log('[ably] Discontinuity recovery: no missed messages found');
388
+ logger.debug('[realtime] Discontinuity recovery: no missed messages found');
385
389
  }
386
390
  }
387
391
 
@@ -489,6 +493,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
489
493
  body.ci = message.ci;
490
494
  if (message.ami) body.ami = message.ami;
491
495
  if (message.instanceType) body.instanceType = message.instanceType;
496
+ if (message.e2bTemplateId) body.e2bTemplateId = message.e2bTemplateId;
492
497
  if (message.keepAlive !== undefined) body.keepAlive = message.keepAlive;
493
498
  }
494
499
 
@@ -540,6 +545,15 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
540
545
  const noVncPort = reply.runner && reply.runner.noVncPort;
541
546
  const runnerVncUrl = reply.runner && reply.runner.vncUrl;
542
547
 
548
+ // Log image version info (AMI for Windows, E2B template for Linux)
549
+ if (reply.imageVersion) {
550
+ if (isE2B) {
551
+ logger.log('E2B image version: v' + reply.imageVersion + (reply.e2bTemplateId ? ' (template: ' + reply.e2bTemplateId + ')' : ''));
552
+ } else {
553
+ logger.log('AMI image version: v' + reply.imageVersion + (reply.amiId ? ' (ami: ' + reply.amiId + ')' : ''));
554
+ }
555
+ }
556
+
543
557
  if (!isE2B) {
544
558
  logger.log(`Runner claimed — ip=${runnerIp || 'none'}, os=${reply.runner?.os || 'unknown'}, noVncPort=${noVncPort || 'not reported'}, vncUrl=${runnerVncUrl || 'not reported'}`);
545
559
  }
@@ -569,7 +583,18 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
569
583
  if (data && data.os && reply.runner) reply.runner.os = data.os;
570
584
  if (data && data.ip && reply.runner) reply.runner.ip = data.ip;
571
585
  if (data && data.runnerVersion && reply.runner) reply.runner.version = data.runnerVersion;
586
+ // Persist version metadata for test result reporting
587
+ self._runnerVersionBefore = reply.imageVersion || null;
588
+ self._runnerVersionAfter = (data && data.runnerVersion) || reply.imageVersion || null;
589
+ self._wasUpdated = !!(data && data.runnerVersion && reply.imageVersion && data.runnerVersion !== reply.imageVersion);
572
590
  logger.log('Runner agent ready (os=' + ((data && data.os) || 'unknown') + ', runner v' + ((data && data.runnerVersion) || 'unknown') + ')');
591
+ // Show upgrade info: if the runner's npm version differs from the baked image version,
592
+ // the runner was upgraded during provisioning.
593
+ var runnerVer = data && data.runnerVersion;
594
+ var imageVer = reply.imageVersion;
595
+ if (runnerVer && imageVer && runnerVer !== imageVer) {
596
+ logger.log('Runner upgraded during provisioning: v' + imageVer + ' \u2192 v' + runnerVer);
597
+ }
573
598
  if (data && data.update) {
574
599
  var u = data.update;
575
600
  if (u.status === 'up-to-date') {
@@ -662,6 +687,15 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
662
687
  url: url,
663
688
  vncPort: noVncPort || undefined,
664
689
  runner: reply.runner,
690
+ // Extra metadata for test result reporting
691
+ amiId: reply.amiId || null,
692
+ e2bTemplateId: reply.e2bTemplateId || null,
693
+ imageVersion: reply.imageVersion || null,
694
+ runnerVersionBefore: this._runnerVersionBefore || reply.imageVersion || null,
695
+ runnerVersionAfter: this._runnerVersionAfter || reply.runner?.version || null,
696
+ wasUpdated: this._wasUpdated || false,
697
+ vncUrl: url || null,
698
+ channelName: this._channelName || null,
665
699
  },
666
700
  };
667
701
  }
@@ -809,7 +843,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
809
843
  function onFailed() {
810
844
  clearTimeout(timer);
811
845
  self._ably.connection.off("connected", onConnected);
812
- reject(new Error("Ably connection failed while waiting to send"));
846
+ reject(new Error("Realtime connection failed while waiting to send"));
813
847
  }
814
848
  self._ably.connection.once("connected", onConnected);
815
849
  self._ably.connection.once("failed", onFailed);
@@ -881,7 +915,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
881
915
  return rid + '(' + (e && e.message ? e.message.type : '?') + ', ' + age + ')';
882
916
  }).join(', ');
883
917
  logger.error(
884
- '[ably] Promise TIMEOUT: requestId=' + requestId +
918
+ '[realtime] Promise TIMEOUT: requestId=' + requestId +
885
919
  ' | type=' + message.type +
886
920
  ' | timeout=' + timeout + 'ms' +
887
921
  ' | all pending: [' + pendingSummary + ']'
@@ -930,7 +964,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
930
964
  timeoutId = setTimeout(timeoutFn, newRemaining);
931
965
  if (timeoutId.unref) timeoutId.unref();
932
966
  logger.log(
933
- '[ably] Extended timeout for requestId=' + requestId +
967
+ '[realtime] Extended timeout for requestId=' + requestId +
934
968
  ' by ' + disconnectionDurationMs + 'ms (new remaining: ' + Math.round(newRemaining / 1000) + 's)'
935
969
  );
936
970
  },
@@ -1002,7 +1036,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
1002
1036
  }
1003
1037
 
1004
1038
  return channel.publish(eventName, message).then(function () {
1005
- logger.log(`[ably] Published: channel=${channel.name.split(':').pop()}, event=${eventName}, type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
1039
+ logger.debug(`[realtime] Published: channel=${channel.name.split(':').pop()}, event=${eventName}, type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
1006
1040
  });
1007
1041
  }
1008
1042
 
@@ -1132,7 +1166,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
1132
1166
  // Send end-session control message to runner before disconnecting
1133
1167
  if (this._sessionChannel && this._ably?.connection?.state === 'connected') {
1134
1168
  try {
1135
- logger.log('[ably] Publishing control: type=end-session');
1169
+ logger.debug('[realtime] Publishing control: type=end-session');
1136
1170
  await this._sessionChannel.publish('control', { type: 'end-session' });
1137
1171
  } catch (e) {
1138
1172
  // Ignore - best effort
@@ -1142,7 +1176,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
1142
1176
  // Leave presence on session channel
1143
1177
  if (this._sessionChannel) {
1144
1178
  try {
1145
- logger.log('[ably] Leaving presence on session channel');
1179
+ logger.debug('[realtime] Leaving presence on session channel');
1146
1180
  await this._sessionChannel.presence.leave();
1147
1181
  } catch (e) {
1148
1182
  // ignore - best effort, Ably will auto-leave on disconnect
@@ -1150,7 +1184,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
1150
1184
  }
1151
1185
 
1152
1186
  try {
1153
- logger.log('[ably] Detaching session channel');
1187
+ logger.debug('[realtime] Detaching session channel');
1154
1188
  if (this._sessionChannel) {
1155
1189
  await this._sessionChannel.detach();
1156
1190
  }
@@ -1160,7 +1194,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
1160
1194
 
1161
1195
  if (this._ably) {
1162
1196
  try {
1163
- logger.log('[ably] Closing Ably connection');
1197
+ logger.debug('[realtime] Closing Realtime connection');
1164
1198
  this._ably.close();
1165
1199
  } catch (e) {
1166
1200
  /* ignore */
@@ -49,8 +49,8 @@ const element = await testdriver.find(description, options)
49
49
  - `"any"` — No wrapping, uses the description as-is (default behavior)
50
50
  </ParamField>
51
51
 
52
- <ParamField path="zoom" type="boolean" default={false}>
53
- Enable two-phase zoom mode for better precision in crowded UIs with many similar elements.
52
+ <ParamField path="zoom" type="boolean" default={true}>
53
+ Two-phase zoom mode for better precision in crowded UIs with many similar elements. Enabled by default.
54
54
  </ParamField>
55
55
 
56
56
  <ParamField path="ai" type="object">
@@ -332,14 +332,19 @@ The `timeout` option:
332
332
  - Returns the element (check `element.found()` if not throwing on failure)
333
333
  - Set to `0` to disable polling and make a single attempt
334
334
 
335
- ## Zoom Mode for Crowded UIs
335
+ ## Zoom Mode
336
336
 
337
- When dealing with many similar icons or elements clustered together (like browser toolbars), enable `zoom` mode for better precision:
337
+ Zoom mode is **enabled by default**. It uses a two-phase approach for better precision when locating elements, especially in crowded UIs with many similar elements.
338
+
339
+ To disable zoom for a specific find call, pass `zoom: false`:
338
340
 
339
341
  ```javascript
340
- // Enable zoom for better precision in crowded UIs
341
- const extensionsBtn = await testdriver.find('extensions puzzle icon in Chrome toolbar', { zoom: true });
342
+ // Zoom is on by default no option needed
343
+ const extensionsBtn = await testdriver.find('extensions puzzle icon in Chrome toolbar');
342
344
  await extensionsBtn.click();
345
+
346
+ // Disable zoom for a specific call if needed
347
+ const largeButton = await testdriver.find('big hero button', { zoom: false });
343
348
  ```
344
349
 
345
350
  ### How Zoom Mode Works
@@ -352,22 +357,11 @@ await extensionsBtn.click();
352
357
  This two-phase approach gives the AI a higher-resolution view of the target area, improving accuracy when multiple similar elements are close together.
353
358
 
354
359
  <Tip>
355
- Use `zoom: true` when:
356
- - Clicking small icons in toolbars
357
- - Selecting from a grid of similar items
358
- - Targeting elements in dense UI areas
359
- - The default locate is clicking the wrong similar element
360
- - You get an AI verification rejection like "The crosshair is located in the empty space of the browser's tab bar/title bar area" — this means the initial locate was imprecise and zoom will help the AI pinpoint the correct element
360
+ You may want to disable zoom with `zoom: false` when:
361
+ - Targeting large, isolated elements where the extra precision isn't needed
362
+ - You want to speed up find calls in simple UIs
361
363
  </Tip>
362
364
 
363
- ```javascript
364
- // Without zoom - may click wrong icon in toolbar
365
- const icon = await testdriver.find('settings icon');
366
-
367
- // With zoom - better precision for crowded areas
368
- const icon = await testdriver.find('settings icon', { zoom: true });
369
- ```
370
-
371
365
  ## Cache Options
372
366
 
373
367
  Control caching behavior to optimize performance, especially when using dynamic variables in prompts.