testdriverai 7.2.46 → 7.2.47

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/lib/sdk.js CHANGED
@@ -95,31 +95,126 @@ const createSDK = (emitter, config, sessionInstance) => {
95
95
  };
96
96
 
97
97
  const auth = async () => {
98
- if (config["TD_API_KEY"]) {
99
- const url = [config["TD_API_ROOT"], "auth/exchange-api-key"].join("/");
100
- const c = {
101
- method: "post",
102
- headers: {
103
- "Content-Type": "application/json",
104
- },
105
- data: {
106
- apiKey: config["TD_API_KEY"],
107
- version,
108
- },
109
- };
110
-
111
- try {
112
- let res = await axios(url, c);
113
-
114
- token = res.data.token;
115
- return token;
116
- } catch (error) {
117
- outputError(error);
118
- throw error; // Re-throw the error so calling code can handle it properly
119
- }
98
+ if (!config["TD_API_KEY"]) {
99
+ const error = new Error(
100
+ "TD_API_KEY is not configured. Get your API key at https://console.testdriver.ai/team"
101
+ );
102
+ error.code = "MISSING_API_KEY";
103
+ error.isAuthError = true;
104
+ throw error;
105
+ }
106
+
107
+ const url = [config["TD_API_ROOT"], "auth/exchange-api-key"].join("/");
108
+ const c = {
109
+ method: "post",
110
+ headers: {
111
+ "Content-Type": "application/json",
112
+ },
113
+ timeout: 15000, // 15 second timeout for auth requests
114
+ data: {
115
+ apiKey: config["TD_API_KEY"],
116
+ version,
117
+ },
118
+ };
119
+
120
+ try {
121
+ let res = await axios(url, c);
122
+
123
+ token = res.data.token;
124
+ return token;
125
+ } catch (error) {
126
+ // Classify the error for better user feedback
127
+ const classifiedError = classifyAuthError(error, config["TD_API_ROOT"]);
128
+ outputError(classifiedError);
129
+ throw classifiedError;
120
130
  }
121
131
  };
122
132
 
133
+ /**
134
+ * Classify authentication errors into user-friendly categories
135
+ * @param {Error} error - The original axios error
136
+ * @param {string} apiRoot - The API root URL for context
137
+ * @returns {Error} A classified error with code and helpful message
138
+ */
139
+ function classifyAuthError(error, apiRoot) {
140
+ const status = error.response?.status;
141
+ const data = error.response?.data;
142
+
143
+ // Check for network-level errors (no response received)
144
+ if (!error.response) {
145
+ const networkError = new Error(
146
+ `Unable to reach TestDriver API at ${apiRoot}. ` +
147
+ getNetworkErrorHint(error.code)
148
+ );
149
+ networkError.code = "NETWORK_ERROR";
150
+ networkError.isNetworkError = true;
151
+ networkError.originalError = error;
152
+ return networkError;
153
+ }
154
+
155
+ // Invalid API key (401)
156
+ if (status === 401) {
157
+ const authError = new Error(
158
+ data?.message ||
159
+ "Invalid API key. Please check your TD_API_KEY and try again. " +
160
+ "Get your API key at https://console.testdriver.ai/team"
161
+ );
162
+ authError.code = data?.error || "INVALID_API_KEY";
163
+ authError.isAuthError = true;
164
+ authError.originalError = error;
165
+ return authError;
166
+ }
167
+
168
+ // Server errors (5xx) - API is down or having issues
169
+ if (status >= 500) {
170
+ const serverError = new Error(
171
+ data?.message ||
172
+ `TestDriver API is currently unavailable (HTTP ${status}). Please try again later.`
173
+ );
174
+ serverError.code = data?.error || "API_UNAVAILABLE";
175
+ serverError.isServerError = true;
176
+ serverError.originalError = error;
177
+ return serverError;
178
+ }
179
+
180
+ // Rate limiting (429)
181
+ if (status === 429) {
182
+ const rateLimitError = new Error(
183
+ "Too many requests to TestDriver API. Please wait a moment and try again."
184
+ );
185
+ rateLimitError.code = "RATE_LIMITED";
186
+ rateLimitError.isRateLimitError = true;
187
+ rateLimitError.originalError = error;
188
+ return rateLimitError;
189
+ }
190
+
191
+ // Other HTTP errors - return with context
192
+ const genericError = new Error(
193
+ `Authentication failed: ${status} ${error.response?.statusText || "Unknown error"}`
194
+ );
195
+ genericError.code = "AUTH_FAILED";
196
+ genericError.originalError = error;
197
+ return genericError;
198
+ }
199
+
200
+ /**
201
+ * Get a helpful hint based on the network error code
202
+ * @param {string} code - The error code (ECONNREFUSED, ETIMEDOUT, etc.)
203
+ * @returns {string} A helpful message for the user
204
+ */
205
+ function getNetworkErrorHint(code) {
206
+ const hints = {
207
+ ECONNREFUSED: "The server refused the connection. Check if the API is running.",
208
+ ETIMEDOUT: "The connection timed out. Check your internet connection.",
209
+ ENOTFOUND: "Could not resolve the hostname. Check your internet connection or DNS settings.",
210
+ ENETUNREACH: "Network is unreachable. Check your internet connection.",
211
+ ECONNRESET: "Connection was reset. This may be a temporary network issue.",
212
+ ERR_NETWORK: "A network error occurred. Check your internet connection.",
213
+ ECONNABORTED: "The request was aborted due to a timeout.",
214
+ };
215
+ return hints[code] || "Check your internet connection and try again.";
216
+ }
217
+
123
218
  const req = async (path, data, onChunk) => {
124
219
  // for each value of data, if it is empty remove it
125
220
  for (let key in data) {
@@ -219,6 +314,26 @@ const createSDK = (emitter, config, sessionInstance) => {
219
314
 
220
315
  return value;
221
316
  } catch (error) {
317
+ // Check for network-level errors (no response received)
318
+ if (!error.response) {
319
+ const networkError = new Error(
320
+ `Unable to reach TestDriver API at ${config["TD_API_ROOT"]}. ` +
321
+ getNetworkErrorHint(error.code)
322
+ );
323
+ networkError.code = "NETWORK_ERROR";
324
+ networkError.isNetworkError = true;
325
+ networkError.originalError = error;
326
+ networkError.path = path;
327
+
328
+ emitter.emit(events.error.sdk, {
329
+ message: networkError.message,
330
+ code: networkError.code,
331
+ fullError: error,
332
+ });
333
+
334
+ throw networkError;
335
+ }
336
+
222
337
  // Check if this is an API validation error with detailed problems
223
338
  if (error.response?.data?.problems) {
224
339
  const problems = error.response.data.problems;
@@ -239,6 +354,46 @@ const createSDK = (emitter, config, sessionInstance) => {
239
354
 
240
355
  throw detailedError;
241
356
  }
357
+
358
+ // Server errors (5xx) - API is down or having issues
359
+ const status = error.response?.status;
360
+ if (status >= 500) {
361
+ const serverError = new Error(
362
+ error.response?.data?.message ||
363
+ `TestDriver API is currently unavailable (HTTP ${status}). Please try again later.`
364
+ );
365
+ serverError.code = error.response?.data?.error || "API_UNAVAILABLE";
366
+ serverError.isServerError = true;
367
+ serverError.originalError = error;
368
+ serverError.path = path;
369
+
370
+ emitter.emit(events.error.sdk, {
371
+ message: serverError.message,
372
+ code: serverError.code,
373
+ fullError: error,
374
+ });
375
+
376
+ throw serverError;
377
+ }
378
+
379
+ // Rate limiting (429)
380
+ if (status === 429) {
381
+ const rateLimitError = new Error(
382
+ "Too many requests to TestDriver API. Please wait a moment and try again."
383
+ );
384
+ rateLimitError.code = "RATE_LIMITED";
385
+ rateLimitError.isRateLimitError = true;
386
+ rateLimitError.originalError = error;
387
+ rateLimitError.path = path;
388
+
389
+ emitter.emit(events.error.sdk, {
390
+ message: rateLimitError.message,
391
+ code: rateLimitError.code,
392
+ fullError: error,
393
+ });
394
+
395
+ throw rateLimitError;
396
+ }
242
397
 
243
398
  outputError(error);
244
399
  throw error; // Re-throw the error so calling code can handle it properly
package/docs/v7/find.mdx CHANGED
@@ -202,6 +202,41 @@ The `timeout` option:
202
202
  - Logs progress during polling
203
203
  - Returns the element (check `element.found()` if not throwing on failure)
204
204
 
205
+ ## Zoom Mode for Crowded UIs
206
+
207
+ When dealing with many similar icons or elements clustered together (like browser toolbars), enable `zoom` mode for better precision:
208
+
209
+ ```javascript
210
+ // Enable zoom for better precision in crowded UIs
211
+ const extensionsBtn = await testdriver.find('extensions puzzle icon in Chrome toolbar', { zoom: true });
212
+ await extensionsBtn.click();
213
+ ```
214
+
215
+ ### How Zoom Mode Works
216
+
217
+ 1. **Phase 1**: AI identifies the approximate location of the element
218
+ 2. **Phase 2**: A 30% crop of the screen is created around that location
219
+ 3. **Phase 3**: AI performs precise location on the zoomed/cropped image
220
+ 4. **Result**: Coordinates are converted back to absolute screen position
221
+
222
+ This two-phase approach gives the AI a higher-resolution view of the target area, improving accuracy when multiple similar elements are close together.
223
+
224
+ <Tip>
225
+ Use `zoom: true` when:
226
+ - Clicking small icons in toolbars
227
+ - Selecting from a grid of similar items
228
+ - Targeting elements in dense UI areas
229
+ - The default locate is clicking the wrong similar element
230
+ </Tip>
231
+
232
+ ```javascript
233
+ // Without zoom - may click wrong icon in toolbar
234
+ const icon = await testdriver.find('settings icon');
235
+
236
+ // With zoom - better precision for crowded areas
237
+ const icon = await testdriver.find('settings icon', { zoom: true });
238
+ ```
239
+
205
240
  ### Manual Polling (Alternative)
206
241
 
207
242
  If you need custom polling logic:
@@ -185,22 +185,87 @@ export function getPluginState() {
185
185
 
186
186
  // Export API helper functions for direct use from tests
187
187
  export async function authenticateWithApiKey(apiKey, apiRoot) {
188
+ if (!apiKey) {
189
+ const error = new Error(
190
+ "TD_API_KEY is not configured. Get your API key at https://console.testdriver.ai/team"
191
+ );
192
+ error.code = "MISSING_API_KEY";
193
+ error.isAuthError = true;
194
+ throw error;
195
+ }
196
+
188
197
  const url = `${apiRoot}/auth/exchange-api-key`;
189
- const response = await withTimeout(
190
- fetch(url, {
191
- method: "POST",
192
- headers: {
193
- "Content-Type": "application/json",
194
- },
195
- body: JSON.stringify({ apiKey }),
196
- }),
197
- 10000,
198
- "Authentication",
199
- );
198
+ let response;
199
+
200
+ try {
201
+ response = await withTimeout(
202
+ fetch(url, {
203
+ method: "POST",
204
+ headers: {
205
+ "Content-Type": "application/json",
206
+ },
207
+ body: JSON.stringify({ apiKey }),
208
+ }),
209
+ 15000,
210
+ "Authentication",
211
+ );
212
+ } catch (fetchError) {
213
+ // Network-level error (fetch failed entirely)
214
+ const networkError = new Error(
215
+ `Unable to reach TestDriver API at ${apiRoot}. ` +
216
+ "Check your internet connection and try again."
217
+ );
218
+ networkError.code = "NETWORK_ERROR";
219
+ networkError.isNetworkError = true;
220
+ networkError.originalError = fetchError;
221
+ throw networkError;
222
+ }
200
223
 
201
224
  if (!response.ok) {
225
+ let data = {};
226
+ try {
227
+ data = await response.json();
228
+ } catch {
229
+ // Response wasn't JSON, use empty object
230
+ }
231
+
232
+ // Invalid API key (401)
233
+ if (response.status === 401) {
234
+ const authError = new Error(
235
+ data.message ||
236
+ "Invalid API key. Please check your TD_API_KEY and try again. " +
237
+ "Get your API key at https://console.testdriver.ai/team"
238
+ );
239
+ authError.code = data.error || "INVALID_API_KEY";
240
+ authError.isAuthError = true;
241
+ throw authError;
242
+ }
243
+
244
+ // Server errors (5xx) - API is down or having issues
245
+ if (response.status >= 500) {
246
+ const serverError = new Error(
247
+ data.message ||
248
+ `TestDriver API is currently unavailable (HTTP ${response.status}). Please try again later.`
249
+ );
250
+ serverError.code = data.error || "API_UNAVAILABLE";
251
+ serverError.isServerError = true;
252
+ throw serverError;
253
+ }
254
+
255
+ // Rate limiting (429)
256
+ if (response.status === 429) {
257
+ const rateLimitError = new Error(
258
+ "Too many requests to TestDriver API. Please wait a moment and try again."
259
+ );
260
+ rateLimitError.code = "RATE_LIMITED";
261
+ rateLimitError.isRateLimitError = true;
262
+ throw rateLimitError;
263
+ }
264
+
265
+ // Other HTTP errors
202
266
  throw new Error(
203
- `Authentication failed: ${response.status} ${response.statusText}`,
267
+ `Authentication failed: ${response.status} ${response.statusText}` +
268
+ (data.message ? ` - ${data.message}` : "")
204
269
  );
205
270
  }
206
271
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.2.46",
3
+ "version": "7.2.47",
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",
package/sdk.js CHANGED
@@ -413,6 +413,7 @@ class Element {
413
413
  // Handle options - can be a number (cacheThreshold) or object with cacheKey/cacheThreshold
414
414
  let cacheKey = null;
415
415
  let cacheThreshold = null;
416
+ let zoom = false; // Default to disabled, enable with zoom: true
416
417
 
417
418
  if (typeof options === 'number') {
418
419
  // Legacy: options is just a number threshold
@@ -421,18 +422,26 @@ class Element {
421
422
  // New: options is an object with cacheKey and/or cacheThreshold
422
423
  cacheKey = options.cacheKey || null;
423
424
  cacheThreshold = options.cacheThreshold ?? null;
425
+ // zoom defaults to false unless explicitly set to true
426
+ zoom = options.zoom === true;
424
427
  }
425
428
 
426
429
  // Use default cacheKey from SDK constructor if not provided in find() options
427
- if (!cacheKey && this.sdk.options?.cacheKey) {
430
+ // BUT only if cache is not explicitly disabled via cache: false option
431
+ if (!cacheKey && this.sdk.options?.cacheKey && this.sdk.cacheThresholds?.find !== -1) {
428
432
  cacheKey = this.sdk.options.cacheKey;
429
433
  }
430
434
 
431
435
  // Determine threshold:
436
+ // - If cache is explicitly disabled (threshold = -1), don't use cache even with cacheKey
432
437
  // - If cacheKey is provided, enable cache (threshold = 0.01 or custom)
433
438
  // - If no cacheKey, disable cache (threshold = -1) unless explicitly overridden
434
439
  let threshold;
435
- if (cacheKey) {
440
+ if (this.sdk.cacheThresholds?.find === -1) {
441
+ // Cache explicitly disabled via cache: false option
442
+ threshold = -1;
443
+ cacheKey = null; // Clear any cacheKey to ensure cache is truly disabled
444
+ } else if (cacheKey) {
436
445
  // cacheKey provided - enable cache with threshold
437
446
  threshold = cacheThreshold ?? 0.01;
438
447
  } else if (cacheThreshold !== null) {
@@ -466,6 +475,7 @@ class Element {
466
475
  cacheKey: cacheKey,
467
476
  os: this.sdk.os,
468
477
  resolution: this.sdk.resolution,
478
+ zoom: zoom,
469
479
  });
470
480
 
471
481
  const duration = Date.now() - startTime;
@@ -2449,15 +2459,21 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2449
2459
  }
2450
2460
 
2451
2461
  // Use default cacheKey from SDK constructor if not provided in findAll() options
2452
- if (!cacheKey && this.options?.cacheKey) {
2462
+ // BUT only if cache is not explicitly disabled via cache: false option
2463
+ if (!cacheKey && this.options?.cacheKey && this.cacheThresholds?.findAll !== -1) {
2453
2464
  cacheKey = this.options.cacheKey;
2454
2465
  }
2455
2466
 
2456
2467
  // Determine threshold:
2468
+ // - If cache is explicitly disabled (threshold = -1), don't use cache even with cacheKey
2457
2469
  // - If cacheKey is provided, enable cache (threshold = 0.01 or custom)
2458
2470
  // - If no cacheKey, disable cache (threshold = -1) unless explicitly overridden
2459
2471
  let threshold;
2460
- if (cacheKey) {
2472
+ if (this.cacheThresholds?.findAll === -1) {
2473
+ // Cache explicitly disabled via cache: false option
2474
+ threshold = -1;
2475
+ cacheKey = null; // Clear any cacheKey to ensure cache is truly disabled
2476
+ } else if (cacheKey) {
2461
2477
  // cacheKey provided - enable cache with threshold
2462
2478
  threshold = cacheThreshold ?? 0.01;
2463
2479
  } else if (cacheThreshold !== null) {
@@ -15,7 +15,7 @@ describe("Chrome Extension Test", () => {
15
15
 
16
16
  console.log('connecting to', process.env.TD_IP)
17
17
 
18
- const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP });
18
+ const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP, cacheKey: new Date().getTime().toString() });
19
19
 
20
20
  // Wait for connection to be ready before running exec
21
21
  await testdriver.ready();
@@ -87,7 +87,7 @@ describe("Chrome Extension Test", () => {
87
87
  expect(pageResult).toBeTruthy();
88
88
 
89
89
  // Click on the extensions button (puzzle piece icon) in Chrome toolbar
90
- const extensionsButton = await testdriver.find("The puzzle-shaped icon in the Chrome toolbar.");
90
+ const extensionsButton = await testdriver.find("The puzzle-shaped icon in the Chrome toolbar.", {zoom: true});
91
91
  await extensionsButton.click();
92
92
 
93
93
  // Look for Loom in the extensions menu