testdriverai 7.8.0-canary.6 → 7.8.0-test.1
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 +5 -6
- package/agent/lib/commands.js +2 -3
- package/agent/lib/sandbox.js +102 -117
- package/agent/lib/sdk.js +2 -4
- package/agent/lib/system.js +65 -25
- package/ai/skills/testdriver-running-tests/SKILL.md +1 -1
- package/docs/changelog.mdx +2 -2
- package/docs/docs.json +30 -31
- package/docs/v7/{hosted.mdx → cloud.mdx} +5 -43
- package/docs/v7/enterprise.mdx +110 -3
- package/docs/v7/quickstart.mdx +2 -30
- package/docs/v7/running-tests.mdx +1 -1
- package/docs/v7/self-hosted.mdx +44 -127
- package/{manual → examples}/drag-and-drop.test.mjs +1 -1
- package/interfaces/logger.js +12 -0
- package/interfaces/vitest-plugin.mjs +0 -3
- package/lib/core/Dashcam.js +7 -11
- package/lib/resolve-channel.js +3 -4
- package/package.json +1 -1
- package/sdk.js +3 -3
- package/vitest.config.mjs +32 -20
- package/agent/lib/http.js +0 -144
- package/ai/skills/testdriver-mcp/SKILL.md +0 -7
- package/docs/v7/copilot/auto-healing.mdx +0 -265
- package/docs/v7/copilot/creating-tests.mdx +0 -156
- package/docs/v7/copilot/github.mdx +0 -143
- package/docs/v7/copilot/running-tests.mdx +0 -149
- package/docs/v7/copilot/setup.mdx +0 -143
- package/docs/v7/mcp.mdx +0 -9
- package/lib/environments.json +0 -18
- /package/{manual → examples}/flake-diffthreshold-001.test.mjs +0 -0
- /package/{manual → examples}/flake-diffthreshold-01.test.mjs +0 -0
- /package/{manual → examples}/flake-diffthreshold-05.test.mjs +0 -0
- /package/{manual → examples}/flake-noredraw-cache.test.mjs +0 -0
- /package/{manual → examples}/flake-noredraw-nocache.test.mjs +0 -0
- /package/{manual → examples}/flake-redraw-cache.test.mjs +0 -0
- /package/{manual → examples}/flake-redraw-nocache.test.mjs +0 -0
- /package/{manual → examples}/flake-rocket-match.test.mjs +0 -0
- /package/{manual → examples}/flake-shared.mjs +0 -0
- /package/{manual → examples}/no-provision.test.mjs +0 -0
- /package/{manual → examples}/scroll-until-text.test.mjs +0 -0
package/agent/index.js
CHANGED
|
@@ -1941,19 +1941,18 @@ ${regression}
|
|
|
1941
1941
|
// Allow explicit override via env (e.g. VITE_DOMAIN from .env)
|
|
1942
1942
|
if (process.env.VITE_DOMAIN) return process.env.VITE_DOMAIN;
|
|
1943
1943
|
|
|
1944
|
-
const environments = require("../lib/environments.json");
|
|
1945
1944
|
const mapping = {
|
|
1946
|
-
"https://
|
|
1945
|
+
"https://api.testdriver.ai": "https://console.testdriver.ai",
|
|
1946
|
+
"https://v6.testdriver.ai": "https://console.testdriver.ai",
|
|
1947
|
+
"https://api-canary.testdriver.ai": "https://console-canary.testdriver.ai",
|
|
1948
|
+
"https://api-test.testdriver.ai": "https://console-test.testdriver.ai",
|
|
1947
1949
|
};
|
|
1948
|
-
for (const env of Object.values(environments)) {
|
|
1949
|
-
mapping[env.apiRoot] = env.consoleUrl;
|
|
1950
|
-
}
|
|
1951
1950
|
if (mapping[apiRoot]) return mapping[apiRoot];
|
|
1952
1951
|
// Local dev: API on localhost:1337 -> Web on localhost:3001
|
|
1953
1952
|
if (apiRoot.includes("localhost:1337") || apiRoot.includes("127.0.0.1:1337")) {
|
|
1954
1953
|
return "http://localhost:3001";
|
|
1955
1954
|
}
|
|
1956
|
-
return
|
|
1955
|
+
return "https://console.testdriver.ai";
|
|
1957
1956
|
}
|
|
1958
1957
|
|
|
1959
1958
|
// Write session file for IDE preview (VSCode extension watches for these)
|
package/agent/lib/commands.js
CHANGED
|
@@ -580,12 +580,11 @@ const createCommands = (
|
|
|
580
580
|
} else if (action === "mouseDown") {
|
|
581
581
|
await sandbox.send({ type: "mousePress", button: "left", x, y, ...elementData });
|
|
582
582
|
} else if (action === "mouseUp") {
|
|
583
|
-
// Move first to create drag motion, then release
|
|
584
|
-
// (pyautogui.mouseUp with x/y teleports instead of dragging)
|
|
585
|
-
await sandbox.send({ type: "moveMouse", x, y, ...elementData });
|
|
586
583
|
await sandbox.send({
|
|
587
584
|
type: "mouseRelease",
|
|
588
585
|
button: "left",
|
|
586
|
+
x,
|
|
587
|
+
y,
|
|
589
588
|
...elementData
|
|
590
589
|
});
|
|
591
590
|
}
|
package/agent/lib/sandbox.js
CHANGED
|
@@ -1,9 +1,83 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
1
2
|
const Ably = require("ably");
|
|
2
|
-
const axios = require("axios");
|
|
3
3
|
const { events } = require("../events");
|
|
4
4
|
const logger = require("./logger");
|
|
5
5
|
const { version } = require("../../package.json");
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
function getSentryTraceHeaders(sessionId) {
|
|
8
|
+
if (!sessionId) return {};
|
|
9
|
+
const traceId = crypto.createHash("md5").update(sessionId).digest("hex");
|
|
10
|
+
const spanId = crypto.randomBytes(8).toString("hex");
|
|
11
|
+
return {
|
|
12
|
+
"sentry-trace": traceId + "-" + spanId + "-1",
|
|
13
|
+
baggage:
|
|
14
|
+
"sentry-trace_id=" +
|
|
15
|
+
traceId +
|
|
16
|
+
",sentry-sample_rate=1.0,sentry-sampled=true",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function httpPost(apiRoot, path, body, timeout) {
|
|
21
|
+
const http = require("http");
|
|
22
|
+
const https = require("https");
|
|
23
|
+
const url = new URL(apiRoot + path);
|
|
24
|
+
const transport = url.protocol === "https:" ? https : http;
|
|
25
|
+
const bodyStr = JSON.stringify(body);
|
|
26
|
+
|
|
27
|
+
return new Promise(function (resolve, reject) {
|
|
28
|
+
var timeoutId = timeout
|
|
29
|
+
? setTimeout(function () {
|
|
30
|
+
req.destroy();
|
|
31
|
+
reject(
|
|
32
|
+
new Error("HTTP request timed out after " + timeout + "ms"),
|
|
33
|
+
);
|
|
34
|
+
}, timeout)
|
|
35
|
+
: null;
|
|
36
|
+
|
|
37
|
+
var req = transport.request(
|
|
38
|
+
url,
|
|
39
|
+
{
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"Content-Length": Buffer.byteLength(bodyStr),
|
|
44
|
+
"Connection": "close",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
function (res) {
|
|
48
|
+
var data = "";
|
|
49
|
+
res.on("data", function (chunk) {
|
|
50
|
+
data += chunk;
|
|
51
|
+
});
|
|
52
|
+
res.on("end", function () {
|
|
53
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
54
|
+
try {
|
|
55
|
+
var parsed = JSON.parse(data);
|
|
56
|
+
if (res.statusCode >= 400) {
|
|
57
|
+
var err = new Error(
|
|
58
|
+
parsed.errorMessage ||
|
|
59
|
+
parsed.message ||
|
|
60
|
+
"HTTP " + res.statusCode,
|
|
61
|
+
);
|
|
62
|
+
err.responseData = parsed;
|
|
63
|
+
reject(err);
|
|
64
|
+
} else {
|
|
65
|
+
resolve(parsed);
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
reject(new Error("Failed to parse API response: " + data));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
req.on("error", function (err) {
|
|
74
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
75
|
+
reject(err);
|
|
76
|
+
});
|
|
77
|
+
req.write(bodyStr);
|
|
78
|
+
req.end();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
7
81
|
|
|
8
82
|
const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
9
83
|
class Sandbox {
|
|
@@ -31,12 +105,6 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
31
105
|
this._lastConnectParams = null;
|
|
32
106
|
this._teamId = null;
|
|
33
107
|
this._sandboxId = null;
|
|
34
|
-
|
|
35
|
-
// Rate limiting state for Ably publishes (Ably limits to 50 msg/sec per connection)
|
|
36
|
-
this._publishLastTime = 0;
|
|
37
|
-
this._publishMinIntervalMs = 25; // 40 msg/sec max, safely under Ably's 50 limit
|
|
38
|
-
this._publishCount = 0;
|
|
39
|
-
this._publishWindowStart = Date.now();
|
|
40
108
|
}
|
|
41
109
|
|
|
42
110
|
getTraceId() {
|
|
@@ -230,79 +298,40 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
230
298
|
}
|
|
231
299
|
|
|
232
300
|
/**
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
301
|
+
* Wrapper around httpPost that retries when the server responds with
|
|
302
|
+
* CONCURRENCY_LIMIT_EXCEEDED (HTTP 429). Instead of failing the test
|
|
303
|
+
* immediately, we wait for a slot to become available — polling every
|
|
304
|
+
* 10 s until vitest's testTimeout kills the test.
|
|
236
305
|
*/
|
|
237
306
|
async _httpPostWithConcurrencyRetry(path, body, timeout) {
|
|
238
|
-
var
|
|
307
|
+
var retryInterval = 10000; // 10 seconds between retries
|
|
239
308
|
var startTime = Date.now();
|
|
240
|
-
var sessionId = this.sessionInstance ? this.sessionInstance.get() : null;
|
|
241
|
-
|
|
242
|
-
var self = this;
|
|
243
|
-
var makeRequest = function () {
|
|
244
|
-
return axios({
|
|
245
|
-
method: "post",
|
|
246
|
-
url: self.apiRoot + path,
|
|
247
|
-
data: body,
|
|
248
|
-
headers: {
|
|
249
|
-
"Content-Type": "application/json",
|
|
250
|
-
"User-Agent": "TestDriverSDK/" + version + " (Node.js " + process.version + ")",
|
|
251
|
-
...getSentryTraceHeaders(sessionId),
|
|
252
|
-
},
|
|
253
|
-
timeout: timeout || 120000,
|
|
254
|
-
});
|
|
255
|
-
};
|
|
256
309
|
|
|
257
310
|
while (true) {
|
|
258
311
|
try {
|
|
259
|
-
|
|
260
|
-
retryConfig: {
|
|
261
|
-
maxRetries: 3,
|
|
262
|
-
baseDelayMs: 2000,
|
|
263
|
-
retryableStatusCodes: [500, 502, 503, 504], // Don't retry 429 — handled below
|
|
264
|
-
},
|
|
265
|
-
onRetry: function (attempt, error, delayMs) {
|
|
266
|
-
var elapsed = Date.now() - startTime;
|
|
267
|
-
logger.warn(
|
|
268
|
-
"Transient network error: " + (error.message || error.code) +
|
|
269
|
-
" — POST " + path +
|
|
270
|
-
" — retry " + attempt + "/3" +
|
|
271
|
-
" in " + (delayMs / 1000).toFixed(1) + "s" +
|
|
272
|
-
" (" + Math.round(elapsed / 1000) + "s elapsed)...",
|
|
273
|
-
);
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
|
-
return response.data;
|
|
312
|
+
return await httpPost(this.apiRoot, path, body, timeout);
|
|
277
313
|
} catch (err) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
var elapsed = Date.now() - startTime;
|
|
282
|
-
logger.log(
|
|
283
|
-
"Concurrency limit reached — waiting " +
|
|
284
|
-
concurrencyRetryInterval / 1000 +
|
|
285
|
-
"s for a slot to become available (" +
|
|
286
|
-
Math.round(elapsed / 1000) +
|
|
287
|
-
"s elapsed)...",
|
|
288
|
-
);
|
|
289
|
-
await new Promise(function (resolve) {
|
|
290
|
-
var t = setTimeout(resolve, concurrencyRetryInterval);
|
|
291
|
-
if (t.unref) t.unref();
|
|
292
|
-
});
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
314
|
+
var isConcurrencyLimit =
|
|
315
|
+
err.responseData &&
|
|
316
|
+
err.responseData.errorCode === "CONCURRENCY_LIMIT_EXCEEDED";
|
|
295
317
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
var httpErr = new Error(
|
|
299
|
-
responseData.errorMessage || responseData.message || "HTTP " + err.response.status,
|
|
300
|
-
);
|
|
301
|
-
httpErr.responseData = responseData;
|
|
302
|
-
throw httpErr;
|
|
318
|
+
if (!isConcurrencyLimit) {
|
|
319
|
+
throw err;
|
|
303
320
|
}
|
|
304
321
|
|
|
305
|
-
|
|
322
|
+
var elapsed = Date.now() - startTime;
|
|
323
|
+
|
|
324
|
+
logger.log(
|
|
325
|
+
"Concurrency limit reached — waiting " +
|
|
326
|
+
retryInterval / 1000 +
|
|
327
|
+
"s for a slot to become available (" +
|
|
328
|
+
Math.round(elapsed / 1000) +
|
|
329
|
+
"s elapsed)...",
|
|
330
|
+
);
|
|
331
|
+
await new Promise(function (resolve) {
|
|
332
|
+
var t = setTimeout(resolve, retryInterval);
|
|
333
|
+
if (t.unref) t.unref();
|
|
334
|
+
});
|
|
306
335
|
}
|
|
307
336
|
}
|
|
308
337
|
}
|
|
@@ -730,7 +759,8 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
730
759
|
p.catch(function () {});
|
|
731
760
|
}
|
|
732
761
|
|
|
733
|
-
this.
|
|
762
|
+
this._cmdChannel
|
|
763
|
+
.publish("command", message)
|
|
734
764
|
.then(function () {
|
|
735
765
|
emitter.emit(events.sandbox.sent, message);
|
|
736
766
|
})
|
|
@@ -747,51 +777,6 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
747
777
|
return p;
|
|
748
778
|
}
|
|
749
779
|
|
|
750
|
-
/**
|
|
751
|
-
* Throttled publish to stay under Ably's 50 msg/sec per-connection limit.
|
|
752
|
-
* Also tracks and logs the current publish rate for debugging.
|
|
753
|
-
* @param {Object} channel - Ably channel to publish on
|
|
754
|
-
* @param {string} eventName - Event name for the publish
|
|
755
|
-
* @param {Object} message - Message payload
|
|
756
|
-
* @returns {Promise} - Resolves when publish completes
|
|
757
|
-
*/
|
|
758
|
-
async _throttledPublish(channel, eventName, message) {
|
|
759
|
-
var self = this;
|
|
760
|
-
var now = Date.now();
|
|
761
|
-
|
|
762
|
-
// Rate limiting: wait if too soon since last publish
|
|
763
|
-
var elapsed = now - this._publishLastTime;
|
|
764
|
-
if (elapsed < this._publishMinIntervalMs) {
|
|
765
|
-
var waitMs = this._publishMinIntervalMs - elapsed;
|
|
766
|
-
await new Promise(function (resolve) {
|
|
767
|
-
var timer = setTimeout(resolve, waitMs);
|
|
768
|
-
if (timer.unref) timer.unref();
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
this._publishLastTime = Date.now();
|
|
772
|
-
|
|
773
|
-
// Metrics: track messages per second
|
|
774
|
-
this._publishCount++;
|
|
775
|
-
var windowElapsed = Date.now() - this._publishWindowStart;
|
|
776
|
-
if (windowElapsed >= 1000) {
|
|
777
|
-
var rate = (this._publishCount / windowElapsed) * 1000;
|
|
778
|
-
var rateStr = rate.toFixed(1);
|
|
779
|
-
|
|
780
|
-
// Log rate - warning if approaching limit, debug otherwise
|
|
781
|
-
if (rate > 45) {
|
|
782
|
-
logger.warn("Ably publish rate: " + rateStr + " msg/sec (approaching 50/sec limit)");
|
|
783
|
-
} else if (process.env.VERBOSE || process.env.TD_DEBUG) {
|
|
784
|
-
logger.log("Ably publish rate: " + rateStr + " msg/sec");
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// Reset window
|
|
788
|
-
this._publishCount = 0;
|
|
789
|
-
this._publishWindowStart = Date.now();
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
return channel.publish(eventName, message);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
780
|
async auth(apiKey) {
|
|
796
781
|
this.apiKey = apiKey;
|
|
797
782
|
var sessionId = this.sessionInstance
|
package/agent/lib/sdk.js
CHANGED
|
@@ -231,7 +231,6 @@ const createSDK = (emitter, config, sessionInstance) => {
|
|
|
231
231
|
let res = await withRetry(
|
|
232
232
|
() => axios(url, c),
|
|
233
233
|
{
|
|
234
|
-
retryConfig: { maxRetries: 2 },
|
|
235
234
|
onRetry: (attempt, error, delayMs) => {
|
|
236
235
|
emitter.emit(events.sdk.retry, {
|
|
237
236
|
path: 'auth/exchange-api-key',
|
|
@@ -382,7 +381,6 @@ const createSDK = (emitter, config, sessionInstance) => {
|
|
|
382
381
|
"Content-Type": "application/json",
|
|
383
382
|
"User-Agent": `TestDriverSDK/${version} (Node.js ${process.version})`,
|
|
384
383
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
385
|
-
...getSentryTraceHeaders(sessionInstance.get()),
|
|
386
384
|
},
|
|
387
385
|
timeout: 15000,
|
|
388
386
|
data: {
|
|
@@ -629,5 +627,5 @@ const createSDK = (emitter, config, sessionInstance) => {
|
|
|
629
627
|
return { req, auth };
|
|
630
628
|
};
|
|
631
629
|
|
|
632
|
-
// Export the factory function
|
|
633
|
-
module.exports = { createSDK
|
|
630
|
+
// Export the factory function
|
|
631
|
+
module.exports = { createSDK };
|
package/agent/lib/system.js
CHANGED
|
@@ -4,8 +4,6 @@ const os = require("os");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const { randomUUID } = require("crypto");
|
|
6
6
|
const Jimp = require("jimp");
|
|
7
|
-
const axios = require("axios");
|
|
8
|
-
const { withRetry } = require("./sdk");
|
|
9
7
|
const { events } = require("../events.js");
|
|
10
8
|
|
|
11
9
|
const createSystem = (emitter, sandbox, config) => {
|
|
@@ -13,37 +11,79 @@ const createSystem = (emitter, sandbox, config) => {
|
|
|
13
11
|
// Download a screenshot from S3 when the runner returns an s3Key
|
|
14
12
|
// (screenshots exceed Ably's 64KB message limit)
|
|
15
13
|
const downloadFromS3 = async (s3Key) => {
|
|
14
|
+
const https = require("https");
|
|
15
|
+
const http = require("http");
|
|
16
16
|
const apiRoot = config["TD_API_ROOT"] || sandbox.apiRoot;
|
|
17
17
|
const apiKey = sandbox.apiKey;
|
|
18
18
|
|
|
19
|
-
// Step 1: Get presigned download URL from API (with retry)
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
19
|
+
// Step 1: Get presigned download URL from API (with retry on rate-limit)
|
|
20
|
+
const body = JSON.stringify({ apiKey, s3Key });
|
|
21
|
+
const url = new URL(apiRoot + "/api/v7/runner/download-url");
|
|
22
|
+
const transport = url.protocol === "https:" ? https : http;
|
|
23
|
+
|
|
24
|
+
const MAX_RETRIES = 3;
|
|
25
|
+
let downloadUrl;
|
|
26
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
27
|
+
try {
|
|
28
|
+
downloadUrl = await new Promise((resolve, reject) => {
|
|
29
|
+
const req = transport.request(url, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
"Content-Length": Buffer.byteLength(body),
|
|
34
|
+
"Connection": "close",
|
|
35
|
+
},
|
|
36
|
+
}, (res) => {
|
|
37
|
+
let data = "";
|
|
38
|
+
res.on("data", (chunk) => { data += chunk; });
|
|
39
|
+
res.on("end", () => {
|
|
40
|
+
if (res.statusCode === 429) {
|
|
41
|
+
return reject({ retryable: true, message: "Rate limited (429) from download-url endpoint" });
|
|
42
|
+
}
|
|
43
|
+
if (res.statusCode >= 400) {
|
|
44
|
+
return reject(new Error(`download-url request failed (HTTP ${res.statusCode}): ${data}`));
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(data);
|
|
48
|
+
if (parsed.downloadUrl) {
|
|
49
|
+
resolve(parsed.downloadUrl);
|
|
50
|
+
} else {
|
|
51
|
+
reject(new Error("No downloadUrl in response: " + data));
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {
|
|
54
|
+
reject({ retryable: true, message: "Failed to parse download-url response: " + data });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
req.on("error", reject);
|
|
59
|
+
req.write(body);
|
|
60
|
+
req.end();
|
|
61
|
+
});
|
|
62
|
+
break; // success — exit retry loop
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err && err.retryable && attempt < MAX_RETRIES) {
|
|
65
|
+
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
|
|
66
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
throw err instanceof Error ? err : new Error(err.message || String(err));
|
|
70
|
+
}
|
|
36
71
|
}
|
|
37
72
|
|
|
38
73
|
// Step 2: Download the image from S3
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
74
|
+
const imageUrl = new URL(downloadUrl);
|
|
75
|
+
const s3Transport = imageUrl.protocol === "https:" ? https : http;
|
|
76
|
+
|
|
77
|
+
const imageBuffer = await new Promise((resolve, reject) => {
|
|
78
|
+
s3Transport.get(downloadUrl, { headers: { "Connection": "close" } }, (res) => {
|
|
79
|
+
const chunks = [];
|
|
80
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
81
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
82
|
+
res.on("error", reject);
|
|
83
|
+
}).on("error", reject);
|
|
44
84
|
});
|
|
45
85
|
|
|
46
|
-
return
|
|
86
|
+
return imageBuffer.toString("base64");
|
|
47
87
|
};
|
|
48
88
|
|
|
49
89
|
const screenshot = async (options) => {
|
package/docs/changelog.mdx
CHANGED
|
@@ -5,8 +5,8 @@ rss: true
|
|
|
5
5
|
icon: "code-compare"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
<Update label="v7.8.0-
|
|
9
|
-
|
|
8
|
+
<Update label="v7.8.0-canary.5" description="March 2026" tags={["canary"]}>
|
|
9
|
+
This release includes all changes from v7.8.0-canary.4 with version bumps.
|
|
10
10
|
|
|
11
11
|
✨ New features
|
|
12
12
|
|
package/docs/docs.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://mintlify.com/docs.json",
|
|
3
|
-
"theme": "
|
|
3
|
+
"theme": "palm",
|
|
4
4
|
"name": "TestDriver",
|
|
5
5
|
"colors": {
|
|
6
6
|
"primary": "#b3d334",
|
|
@@ -48,20 +48,20 @@
|
|
|
48
48
|
"/v7/examples/windows-installer"
|
|
49
49
|
]
|
|
50
50
|
},
|
|
51
|
-
|
|
52
51
|
{
|
|
53
|
-
"group": "
|
|
52
|
+
"group": "Plans & Pricing",
|
|
54
53
|
"icon": "server",
|
|
55
54
|
"pages": [
|
|
56
|
-
"/v7/
|
|
57
|
-
"/v7/self-hosted"
|
|
55
|
+
"/v7/cloud",
|
|
56
|
+
"/v7/self-hosted",
|
|
57
|
+
"/v7/enterprise"
|
|
58
58
|
]
|
|
59
59
|
},
|
|
60
60
|
"/changelog"
|
|
61
61
|
]
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
|
-
"group": "
|
|
64
|
+
"group": "Creating Tests",
|
|
65
65
|
"pages": [
|
|
66
66
|
"/v7/generating-tests",
|
|
67
67
|
"/v7/device-config",
|
|
@@ -95,33 +95,32 @@
|
|
|
95
95
|
]
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
|
-
"group": "
|
|
98
|
+
"group": "Actions",
|
|
99
|
+
"pages": [
|
|
100
|
+
"/v7/ai",
|
|
101
|
+
"/v7/assert",
|
|
102
|
+
"/v7/captcha",
|
|
103
|
+
"/v7/click",
|
|
104
|
+
"/v7/double-click",
|
|
105
|
+
"/v7/exec",
|
|
106
|
+
"/v7/find",
|
|
107
|
+
"/v7/focus-application",
|
|
108
|
+
"/v7/hover",
|
|
109
|
+
"/v7/mouse-down",
|
|
110
|
+
"/v7/mouse-up",
|
|
111
|
+
"/v7/parse",
|
|
112
|
+
"/v7/press-keys",
|
|
113
|
+
"/v7/right-click",
|
|
114
|
+
"/v7/screenshot",
|
|
115
|
+
"/v7/type",
|
|
116
|
+
"/v7/scroll"
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"group": "SDK",
|
|
99
121
|
"pages": [
|
|
100
|
-
"/v7/client",
|
|
101
122
|
"/v7/elements",
|
|
102
|
-
|
|
103
|
-
"group": "Actions",
|
|
104
|
-
"icon": "bolt",
|
|
105
|
-
"pages": [
|
|
106
|
-
"/v7/ai",
|
|
107
|
-
"/v7/assert",
|
|
108
|
-
"/v7/captcha",
|
|
109
|
-
"/v7/click",
|
|
110
|
-
"/v7/double-click",
|
|
111
|
-
"/v7/exec",
|
|
112
|
-
"/v7/find",
|
|
113
|
-
"/v7/focus-application",
|
|
114
|
-
"/v7/hover",
|
|
115
|
-
"/v7/mouse-down",
|
|
116
|
-
"/v7/mouse-up",
|
|
117
|
-
"/v7/parse",
|
|
118
|
-
"/v7/press-keys",
|
|
119
|
-
"/v7/right-click",
|
|
120
|
-
"/v7/screenshot",
|
|
121
|
-
"/v7/scroll",
|
|
122
|
-
"/v7/type"
|
|
123
|
-
]
|
|
124
|
-
},
|
|
123
|
+
"/v7/client",
|
|
125
124
|
"/v7/dashcam"
|
|
126
125
|
]
|
|
127
126
|
} ]
|
|
@@ -1,56 +1,18 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "
|
|
3
|
-
sidebarTitle: "
|
|
2
|
+
title: "Cloud Plan"
|
|
3
|
+
sidebarTitle: "Cloud"
|
|
4
4
|
description: "The fastest way to get started with TestDriver. Just set your API key and start testing."
|
|
5
5
|
icon: "cloud"
|
|
6
|
-
mode: "wide"
|
|
7
6
|
---
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
Cloud pricing is based on **device-seconds**: the amount of time your tests run on **our infrastructure**.
|
|
10
9
|
|
|
11
10
|
- **Zero Setup** — Start testing immediately. No DevOps required.
|
|
12
11
|
- **Free Tier** — Get started with a limited preview at no cost.
|
|
13
12
|
- **Pay As You Go** — Only pay for the device-seconds you use.
|
|
14
13
|
|
|
15
|
-
## Hosted Plans
|
|
16
|
-
|
|
17
|
-
<CardGroup cols={3}>
|
|
18
|
-
<Card title="Free Trial" icon="gift" href="https://docs.testdriver.ai">
|
|
19
|
-
**$0/month**
|
|
20
|
-
|
|
21
|
-
- 1 Concurrent Sandbox
|
|
22
|
-
- 60 Minutes Included
|
|
23
|
-
- 1 Team User
|
|
24
|
-
- Community Support
|
|
25
|
-
</Card>
|
|
26
|
-
|
|
27
|
-
<Card title="Pro" icon="rocket" href="https://console.testdriver.ai/checkout/pro">
|
|
28
|
-
**$20/month**
|
|
29
|
-
|
|
30
|
-
- 2 Concurrent Sandboxes
|
|
31
|
-
- 600 Minutes Included
|
|
32
|
-
- Overage: $0.002/second
|
|
33
|
-
- 1 Team User
|
|
34
|
-
- Test Recordings
|
|
35
|
-
- Community Support
|
|
36
|
-
</Card>
|
|
37
|
-
|
|
38
|
-
<Card title="Team" icon="users" href="https://console.testdriver.ai/checkout/team">
|
|
39
|
-
**$600/month**
|
|
40
|
-
|
|
41
|
-
- 8 Concurrent Sandboxes
|
|
42
|
-
- 10,000 Minutes Included
|
|
43
|
-
- Overage: $0.001/second
|
|
44
|
-
- 5 Team Users
|
|
45
|
-
- Test Recordings
|
|
46
|
-
- Private Support
|
|
47
|
-
- Test Analytics
|
|
48
|
-
- CPU, RAM, & Network Profiles
|
|
49
|
-
</Card>
|
|
50
|
-
</CardGroup>
|
|
51
|
-
|
|
52
14
|
## Get Started
|
|
53
|
-
|
|
15
|
+
Cloud is the default when you follow the Quickstart guide.
|
|
54
16
|
<Card
|
|
55
17
|
title="Try the Quickstart"
|
|
56
18
|
icon="play"
|
|
@@ -140,7 +102,7 @@ To prevent tests from failing due to exceeding your license slot limit, we recom
|
|
|
140
102
|
|
|
141
103
|
## When to Consider Self-Hosted
|
|
142
104
|
|
|
143
|
-
|
|
105
|
+
Cloud is perfect for getting started and for teams that want zero infrastructure management. However, you might consider [Self-Hosted](/v7/self-hosted) if you:
|
|
144
106
|
|
|
145
107
|
- Want to escape per-second billing with a flat license fee
|
|
146
108
|
- Require greater concurrency than offered in Cloud plans
|