testdriverai 7.2.46 → 7.2.48
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/workflows/acceptance-linux-scheduled.yaml +1 -1
- package/.github/workflows/acceptance.yaml +2 -2
- package/.github/workflows/windows-self-hosted.yaml +1 -1
- package/README.md +2 -2
- package/agent/lib/sdk.js +177 -22
- package/docs/TEST-GITHUB-COMMENTS.md +2 -2
- package/docs/v7/_drafts/plugin-migration.mdx +1 -1
- package/docs/v7/aws-setup.mdx +1 -1
- package/docs/v7/examples.mdx +1 -1
- package/docs/v7/find.mdx +35 -0
- package/{test/testdriver → examples}/chrome-extension.test.mjs +3 -3
- package/interfaces/vitest-plugin.mjs +77 -12
- package/package.json +1 -1
- package/sdk.js +20 -4
- package/test/manual/reconnect-provision.test.mjs +2 -2
- package/testdriver-plugin/skills/actions/SKILL.md +93 -0
- package/testdriver-plugin/skills/assertions/SKILL.md +77 -0
- package/testdriver-plugin/skills/caching/SKILL.md +66 -0
- package/testdriver-plugin/skills/creating-tests/SKILL.md +104 -0
- package/testdriver-plugin/skills/finding-elements/SKILL.md +77 -0
- package/testdriver-plugin/skills/github-actions/SKILL.md +100 -0
- package/testdriver-plugin/skills/running-tests/SKILL.md +77 -0
- package/testdriver-plugin/skills/secrets/SKILL.md +87 -0
- package/testdriver-plugin/skills/self-hosting/SKILL.md +89 -0
- package/testdriver-plugin/skills/setup/SKILL.md +76 -0
- package/testdriver-plugin/skills/variables/SKILL.md +88 -0
- package/testdriver-plugin/skills/waiting/SKILL.md +72 -0
- package/debug/01-table-initial.png +0 -0
- package/debug/02-after-ai-explore.png +0 -0
- package/debug/02-after-scroll.png +0 -0
- package/examples/github-actions.yml +0 -68
- package/examples/run-tests-with-recording.sh +0 -70
- package/examples/screenshot-example.js +0 -63
- package/examples/sdk-awesome-logs-demo.js +0 -177
- package/examples/sdk-cache-thresholds.js +0 -96
- package/examples/sdk-element-properties.js +0 -155
- package/examples/sdk-simple-example.js +0 -65
- package/examples/test-recording-example.test.js +0 -166
- /package/{test/testdriver → examples}/ai.test.mjs +0 -0
- /package/{test/testdriver → examples}/assert.test.mjs +0 -0
- /package/{test/testdriver → examples}/drag-and-drop.test.mjs +0 -0
- /package/{test/testdriver → examples}/element-not-found.test.mjs +0 -0
- /package/{test/testdriver → examples}/exec-output.test.mjs +0 -0
- /package/{test/testdriver → examples}/exec-pwsh.test.mjs +0 -0
- /package/{test/testdriver → examples}/focus-window.test.mjs +0 -0
- /package/{test/testdriver → examples}/formatted-logging.test.mjs +0 -0
- /package/{test/testdriver → examples}/hover-image.test.mjs +0 -0
- /package/{test/testdriver → examples}/hover-text-with-description.test.mjs +0 -0
- /package/{test/testdriver → examples}/hover-text.test.mjs +0 -0
- /package/{test/testdriver → examples}/installer.test.mjs +0 -0
- /package/{test/testdriver → examples}/launch-vscode-linux.test.mjs +0 -0
- /package/{test/testdriver → examples}/match-image.test.mjs +0 -0
- /package/{test/testdriver → examples}/press-keys.test.mjs +0 -0
- /package/{test/testdriver → examples}/prompt.test.mjs +0 -0
- /package/{test/testdriver → examples}/scroll-keyboard.test.mjs +0 -0
- /package/{test/testdriver → examples}/scroll-until-image.test.mjs +0 -0
- /package/{test/testdriver → examples}/scroll-until-text.test.mjs +0 -0
- /package/{test/testdriver → examples}/scroll.test.mjs +0 -0
- /package/{test/testdriver → examples}/type.test.mjs +0 -0
- /package/{test/testdriver → examples}/windows-installer.test.mjs +0 -0
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
|
|
32
32
|
- name: Run Linux tests with Sentry Cron monitoring
|
|
33
33
|
run: |
|
|
34
|
-
sentry-cli monitors run testdriver-linux-acceptance -- npx vitest run
|
|
34
|
+
sentry-cli monitors run testdriver-linux-acceptance -- npx vitest run examples/*.test.mjs
|
|
35
35
|
env:
|
|
36
36
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
|
37
37
|
TD_API_KEY: ${{ secrets.TD_API_KEY }}
|
|
@@ -29,7 +29,7 @@ jobs:
|
|
|
29
29
|
run: npm ci
|
|
30
30
|
|
|
31
31
|
- name: Run Linux tests
|
|
32
|
-
run: npx vitest run
|
|
32
|
+
run: npx vitest run examples/*.test.mjs
|
|
33
33
|
env:
|
|
34
34
|
TD_API_KEY: ${{ secrets.TD_API_KEY }}
|
|
35
35
|
TD_OS: linux
|
|
@@ -52,7 +52,7 @@ jobs:
|
|
|
52
52
|
if: contains(github.event.pull_request.labels.*.name, 'test-windows')
|
|
53
53
|
uses: ./.github/workflows/windows-self-hosted.yaml
|
|
54
54
|
with:
|
|
55
|
-
test_pattern: '
|
|
55
|
+
test_pattern: 'examples/assert.test.mjs'
|
|
56
56
|
secrets:
|
|
57
57
|
TD_API_KEY: ${{ secrets.TD_API_KEY }}
|
|
58
58
|
TD_WEBSITE: ${{ secrets.TD_WEBSITE }}
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<br />
|
|
12
12
|
<br />
|
|
13
13
|
|
|
14
|
-
[🚀 **Quick Start**](#-quick-start) • [📖 **Documentation**](https://docs.testdriver.ai) • [💻 **Examples**](https://github.com/testdriverai/testdriverai/tree/main/
|
|
14
|
+
[🚀 **Quick Start**](#-quick-start) • [📖 **Documentation**](https://docs.testdriver.ai) • [💻 **Examples**](https://github.com/testdriverai/testdriverai/tree/main/examples) • [📖 **Pricing**](https://docs.testdriver.ai) • [💬 **Discord**](https://discord.com/invite/cWDFW8DzPm) • [🌐 **Website**](https://testdriver.ai)
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
@@ -41,7 +41,7 @@ const result = await testdriver.assert(
|
|
|
41
41
|
expect(result).toBeTruthy();
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
[See Full Example](https://github.com/testdriverai/testdriverai/blob/main/
|
|
44
|
+
[See Full Example](https://github.com/testdriverai/testdriverai/blob/main/examples/drag-and-drop.test.mjs) • [Browse All Examples](https://github.com/testdriverai/testdriverai/tree/main/examples)
|
|
45
45
|
|
|
46
46
|
---
|
|
47
47
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
## What the Test Does
|
|
40
40
|
|
|
41
|
-
The workflow runs `
|
|
41
|
+
The workflow runs `examples/assert.test.mjs` which:
|
|
42
42
|
- Provisions a Chrome browser
|
|
43
43
|
- Navigates to https://saucedemo.com
|
|
44
44
|
- Performs login actions
|
|
@@ -72,7 +72,7 @@ Skipped: 0 ⏭️
|
|
|
72
72
|
|
|
73
73
|
| Status | Test | File | Duration | Replay |
|
|
74
74
|
|--------|------|------|----------|--------|
|
|
75
|
-
| ✅ | Assert Test | `
|
|
75
|
+
| ✅ | Assert Test | `examples/assert.test.mjs` | 25.3s | [🎥 View](https://console.testdriver.ai/replay/...) |
|
|
76
76
|
|
|
77
77
|
## 🎥 Dashcam Replays
|
|
78
78
|
|
|
@@ -142,7 +142,7 @@ export default defineConfig({
|
|
|
142
142
|
|
|
143
143
|
### Test Helpers
|
|
144
144
|
|
|
145
|
-
- ✅ **Removed**: `
|
|
145
|
+
- ✅ **Removed**: `examples/setup/` folder (legacy helpers)
|
|
146
146
|
- Code moved to `lib/vitest/hooks.mjs` framework
|
|
147
147
|
- Tests now use `TestDriver(context, options)` pattern directly
|
|
148
148
|
|
package/docs/v7/aws-setup.mdx
CHANGED
package/docs/v7/examples.mdx
CHANGED
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:
|
|
@@ -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();
|
|
@@ -55,7 +55,7 @@ describe("Chrome Extension Test", () => {
|
|
|
55
55
|
// When clicked, it shows a popup with "Hello Extensions"
|
|
56
56
|
|
|
57
57
|
// Click on the extensions button (puzzle piece icon) in Chrome toolbar
|
|
58
|
-
const extensionsButton = await testdriver.find("The extensions button in the Chrome toolbar");
|
|
58
|
+
const extensionsButton = await testdriver.find("The extensions button in the Chrome toolbar", {zoom: true});
|
|
59
59
|
await extensionsButton.click();
|
|
60
60
|
|
|
61
61
|
// Look for the hello world extension in the extensions menu
|
|
@@ -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
|
|
@@ -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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
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
|
|
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 (
|
|
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
|
|
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 (
|
|
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) {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* Run reconnect-signin.test.mjs within 2 minutes of this test completing.
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
|
-
* 1. npm test --
|
|
12
|
-
* 2. (within 2 minutes)
|
|
11
|
+
* 1. npm test -- examples/reconnect-provision.test.mjs
|
|
12
|
+
* 2. (within 2 minutes) examples/reconnect-signin.test.mjs
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { afterAll, describe, expect, it } from "vitest";
|