testdriverai 7.2.56 → 7.2.58
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/.env.example +2 -0
- package/.github/workflows/acceptance-windows-scheduled.yaml +29 -28
- package/.github/workflows/acceptance.yaml +54 -52
- package/.github/workflows/publish.yaml +59 -43
- package/.github/workflows/testdriver.yml +157 -156
- package/.github/workflows/windows-self-hosted.yaml +60 -46
- package/CHANGELOG.md +10 -0
- package/docs/docs.json +1 -0
- package/docs/v7/captcha.mdx +160 -0
- package/examples/captcha-api.test.mjs +50 -0
- package/lib/captcha/solver.js +296 -0
- package/lib/core/Dashcam.js +135 -95
- package/lib/vitest/hooks.mjs +175 -126
- package/lib/vitest/setup-aws.mjs +69 -46
- package/package.json +1 -1
- package/sdk.d.ts +67 -20
- package/sdk.js +733 -402
- package/test/captcha-solver.test.mjs +70 -0
- package/test/chrome-remote-debugging.test.mjs +66 -0
- package/vitest.config.mjs +10 -6
package/lib/vitest/setup-aws.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Vitest Setup File for AWS Self-Hosted TestDriver Instances
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* This setup file spawns a fresh AWS instance before each test
|
|
5
5
|
* and terminates it after each test completes.
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* Usage in vitest.config.mjs:
|
|
8
8
|
* ```js
|
|
9
9
|
* export default defineConfig({
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* },
|
|
16
16
|
* });
|
|
17
17
|
* ```
|
|
18
|
-
*
|
|
18
|
+
*
|
|
19
19
|
* Required environment variables:
|
|
20
20
|
* - AWS_ACCESS_KEY_ID
|
|
21
21
|
* - AWS_SECRET_ACCESS_KEY
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
* - AMI_ID
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { execSync, spawn } from
|
|
28
|
-
import { dirname, join } from
|
|
29
|
-
import { fileURLToPath } from
|
|
30
|
-
import { beforeEach } from
|
|
27
|
+
import { execSync, spawn } from "child_process";
|
|
28
|
+
import { dirname, join } from "path";
|
|
29
|
+
import { fileURLToPath } from "url";
|
|
30
|
+
import { beforeEach } from "vitest";
|
|
31
31
|
|
|
32
32
|
const __filename = fileURLToPath(import.meta.url);
|
|
33
33
|
const __dirname = dirname(__filename);
|
|
@@ -56,20 +56,23 @@ globalThis.__testdriverAWS = globalThis.__testdriverAWS || {
|
|
|
56
56
|
execSync(
|
|
57
57
|
`aws ec2 terminate-instances --region "${awsRegion}" --instance-ids "${instanceId}"`,
|
|
58
58
|
{
|
|
59
|
-
encoding:
|
|
59
|
+
encoding: "utf-8",
|
|
60
60
|
env: process.env,
|
|
61
|
-
stdio:
|
|
62
|
-
}
|
|
61
|
+
stdio: "inherit",
|
|
62
|
+
},
|
|
63
63
|
);
|
|
64
64
|
|
|
65
65
|
console.log(`[TestDriver] Instance terminated: ${instanceId}`);
|
|
66
66
|
} catch (error) {
|
|
67
|
-
console.error(
|
|
67
|
+
console.error(
|
|
68
|
+
"[TestDriver] Failed to terminate instance:",
|
|
69
|
+
error.message,
|
|
70
|
+
);
|
|
68
71
|
// Don't throw - we don't want to fail the test because of cleanup issues
|
|
69
72
|
} finally {
|
|
70
73
|
testInstances.delete(testId);
|
|
71
74
|
}
|
|
72
|
-
}
|
|
75
|
+
},
|
|
73
76
|
};
|
|
74
77
|
|
|
75
78
|
/**
|
|
@@ -81,22 +84,27 @@ function cleanupAllInstances() {
|
|
|
81
84
|
return;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
console.log(
|
|
87
|
+
console.log(
|
|
88
|
+
`[TestDriver] Emergency cleanup: terminating ${testInstances.size} instance(s)`,
|
|
89
|
+
);
|
|
85
90
|
|
|
86
91
|
for (const [testId, instanceInfo] of testInstances.entries()) {
|
|
87
92
|
const { instanceId, awsRegion } = instanceInfo;
|
|
88
|
-
|
|
93
|
+
|
|
89
94
|
try {
|
|
90
95
|
console.log(`[TestDriver] Terminating instance: ${instanceId}`);
|
|
91
96
|
execSync(
|
|
92
97
|
`aws ec2 terminate-instances --region "${awsRegion}" --instance-ids "${instanceId}"`,
|
|
93
98
|
{
|
|
94
|
-
encoding:
|
|
95
|
-
stdio:
|
|
96
|
-
}
|
|
99
|
+
encoding: "utf-8",
|
|
100
|
+
stdio: "inherit",
|
|
101
|
+
},
|
|
97
102
|
);
|
|
98
103
|
} catch (error) {
|
|
99
|
-
console.error(
|
|
104
|
+
console.error(
|
|
105
|
+
`[TestDriver] Failed to terminate instance ${instanceId}:`,
|
|
106
|
+
error.message,
|
|
107
|
+
);
|
|
100
108
|
}
|
|
101
109
|
}
|
|
102
110
|
|
|
@@ -104,26 +112,26 @@ function cleanupAllInstances() {
|
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
// Register cleanup handlers for various exit scenarios
|
|
107
|
-
process.on(
|
|
108
|
-
process.on(
|
|
109
|
-
console.log(
|
|
115
|
+
process.on("exit", cleanupAllInstances);
|
|
116
|
+
process.on("SIGINT", () => {
|
|
117
|
+
console.log("\n[TestDriver] Received SIGINT, cleaning up instances...");
|
|
110
118
|
cleanupAllInstances();
|
|
111
119
|
// Don't call process.exit here - let the signal handler do its job
|
|
112
120
|
});
|
|
113
|
-
process.on(
|
|
114
|
-
console.log(
|
|
121
|
+
process.on("SIGTERM", () => {
|
|
122
|
+
console.log("\n[TestDriver] Received SIGTERM, cleaning up instances...");
|
|
115
123
|
cleanupAllInstances();
|
|
116
124
|
// Don't call process.exit here - let the signal handler do its job
|
|
117
125
|
});
|
|
118
|
-
process.on(
|
|
119
|
-
console.error(
|
|
126
|
+
process.on("uncaughtException", (error) => {
|
|
127
|
+
console.error("[TestDriver] Uncaught exception:", error);
|
|
120
128
|
cleanupAllInstances();
|
|
121
129
|
// Don't call process.exit here - let Node.js handle the exception
|
|
122
130
|
});
|
|
123
131
|
|
|
124
132
|
beforeEach(async (context) => {
|
|
125
133
|
// Only spawn if TD_OS=windows (indicates Windows self-hosted mode)
|
|
126
|
-
if (process.env.TD_OS !==
|
|
134
|
+
if (process.env.TD_OS !== "windows") {
|
|
127
135
|
return;
|
|
128
136
|
}
|
|
129
137
|
|
|
@@ -135,61 +143,76 @@ beforeEach(async (context) => {
|
|
|
135
143
|
}
|
|
136
144
|
|
|
137
145
|
// Verify required parameters are available
|
|
146
|
+
if (process.env.TD_OS === "windows") {
|
|
147
|
+
console.log(
|
|
148
|
+
"[TestDriver] startup check: TWOCAPTCHA_API_KEY is " +
|
|
149
|
+
(process.env.TWOCAPTCHA_API_KEY ? "REQUIRED" : "MISSING"),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
138
153
|
if (!process.env.AWS_LAUNCH_TEMPLATE_ID || !process.env.AMI_ID) {
|
|
139
|
-
throw new Error(
|
|
154
|
+
throw new Error(
|
|
155
|
+
"[TestDriver] TD_OS=windows requires AWS_LAUNCH_TEMPLATE_ID and AMI_ID environment variables",
|
|
156
|
+
);
|
|
140
157
|
}
|
|
141
158
|
|
|
142
159
|
// Check if AWS CLI is installed
|
|
143
160
|
try {
|
|
144
|
-
execSync(
|
|
161
|
+
execSync("which aws", { stdio: "ignore" });
|
|
145
162
|
} catch (error) {
|
|
146
|
-
throw new Error(
|
|
163
|
+
throw new Error(
|
|
164
|
+
"[TestDriver] AWS CLI is not installed. Install it from https://aws.amazon.com/cli/",
|
|
165
|
+
);
|
|
147
166
|
}
|
|
148
167
|
|
|
149
168
|
const testId = context.task.id;
|
|
150
|
-
|
|
151
|
-
console.log(
|
|
152
|
-
|
|
169
|
+
|
|
170
|
+
console.log(
|
|
171
|
+
`[TestDriver] Spawning AWS instance for test: ${context.task.name}`,
|
|
172
|
+
);
|
|
173
|
+
|
|
153
174
|
try {
|
|
154
175
|
// Find the spawn-runner.sh script (relative to this file)
|
|
155
|
-
const spawnScriptPath = join(__dirname,
|
|
156
|
-
|
|
176
|
+
const spawnScriptPath = join(__dirname, "../../setup/aws/spawn-runner.sh");
|
|
177
|
+
|
|
157
178
|
// Execute spawn-runner.sh with live output streaming
|
|
158
179
|
const output = await new Promise((resolve, reject) => {
|
|
159
|
-
const child = spawn(
|
|
180
|
+
const child = spawn("bash", [spawnScriptPath], {
|
|
160
181
|
env: {
|
|
161
182
|
...process.env,
|
|
162
|
-
AWS_REGION: process.env.AWS_REGION ||
|
|
163
|
-
RESOLUTION: process.env.RESOLUTION ||
|
|
183
|
+
AWS_REGION: process.env.AWS_REGION || "us-east-2",
|
|
184
|
+
RESOLUTION: process.env.RESOLUTION || "1920x1080",
|
|
164
185
|
},
|
|
165
186
|
});
|
|
166
187
|
|
|
167
|
-
let stdout =
|
|
168
|
-
let stderr =
|
|
188
|
+
let stdout = "";
|
|
189
|
+
let stderr = "";
|
|
169
190
|
|
|
170
191
|
// Stream stdout in real-time
|
|
171
|
-
child.stdout.on(
|
|
192
|
+
child.stdout.on("data", (data) => {
|
|
172
193
|
const str = data.toString();
|
|
173
194
|
process.stdout.write(str); // Show output immediately
|
|
174
195
|
stdout += str;
|
|
175
196
|
});
|
|
176
197
|
|
|
177
198
|
// Stream stderr in real-time
|
|
178
|
-
child.stderr.on(
|
|
199
|
+
child.stderr.on("data", (data) => {
|
|
179
200
|
const str = data.toString();
|
|
180
201
|
process.stderr.write(str); // Show errors immediately
|
|
181
202
|
stderr += str;
|
|
182
203
|
});
|
|
183
204
|
|
|
184
|
-
child.on(
|
|
205
|
+
child.on("close", (code) => {
|
|
185
206
|
if (code !== 0) {
|
|
186
|
-
reject(
|
|
207
|
+
reject(
|
|
208
|
+
new Error(`spawn-runner.sh exited with code ${code}\n${stderr}`),
|
|
209
|
+
);
|
|
187
210
|
} else {
|
|
188
211
|
resolve(stdout);
|
|
189
212
|
}
|
|
190
213
|
});
|
|
191
214
|
|
|
192
|
-
child.on(
|
|
215
|
+
child.on("error", (err) => {
|
|
193
216
|
reject(err);
|
|
194
217
|
});
|
|
195
218
|
});
|
|
@@ -200,7 +223,7 @@ beforeEach(async (context) => {
|
|
|
200
223
|
const awsRegionMatch = output.match(/AWS_REGION=(.+)/);
|
|
201
224
|
|
|
202
225
|
if (!publicIpMatch || !instanceIdMatch || !awsRegionMatch) {
|
|
203
|
-
throw new Error(
|
|
226
|
+
throw new Error("Failed to parse spawn-runner.sh output");
|
|
204
227
|
}
|
|
205
228
|
|
|
206
229
|
const publicIp = publicIpMatch[1].trim();
|
|
@@ -215,7 +238,7 @@ beforeEach(async (context) => {
|
|
|
215
238
|
|
|
216
239
|
console.log(`[TestDriver] Instance spawned: ${instanceId} at ${publicIp}`);
|
|
217
240
|
} catch (error) {
|
|
218
|
-
console.error(
|
|
241
|
+
console.error("[TestDriver] Failed to spawn AWS instance:", error.message);
|
|
219
242
|
throw error;
|
|
220
243
|
}
|
|
221
244
|
});
|
package/package.json
CHANGED
package/sdk.d.ts
CHANGED
|
@@ -216,7 +216,7 @@ export interface TestDriverOptions {
|
|
|
216
216
|
/** Sandbox resolution (default: '1366x768') */
|
|
217
217
|
resolution?: string;
|
|
218
218
|
/** Operating system for the sandbox (default: 'linux') */
|
|
219
|
-
os?:
|
|
219
|
+
os?: "windows" | "linux";
|
|
220
220
|
/** Enable analytics tracking (default: true) */
|
|
221
221
|
analytics?: boolean;
|
|
222
222
|
/** Enable console logging output (default: true) */
|
|
@@ -247,16 +247,18 @@ export interface TestDriverOptions {
|
|
|
247
247
|
/** Enable/disable Dashcam video recording (default: true) */
|
|
248
248
|
dashcam?: boolean;
|
|
249
249
|
/** Redraw configuration for screen change detection */
|
|
250
|
-
redraw?:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
250
|
+
redraw?:
|
|
251
|
+
| boolean
|
|
252
|
+
| {
|
|
253
|
+
/** Enable redraw detection (default: true) */
|
|
254
|
+
enabled?: boolean;
|
|
255
|
+
/** Pixel difference threshold for redraw detection */
|
|
256
|
+
diffThreshold?: number;
|
|
257
|
+
/** Enable screen redraw detection */
|
|
258
|
+
screenRedraw?: boolean;
|
|
259
|
+
/** Enable network activity monitoring */
|
|
260
|
+
networkMonitor?: boolean;
|
|
261
|
+
};
|
|
260
262
|
/** @deprecated Use redraw.diffThreshold instead */
|
|
261
263
|
redrawThreshold?: number | object;
|
|
262
264
|
/** Additional environment variables */
|
|
@@ -277,7 +279,7 @@ export interface ConnectOptions {
|
|
|
277
279
|
/** EC2 instance type for sandbox (e.g., 'i3.metal') */
|
|
278
280
|
sandboxInstance?: string;
|
|
279
281
|
/** Operating system for the sandbox (default: 'linux') */
|
|
280
|
-
os?:
|
|
282
|
+
os?: "windows" | "linux";
|
|
281
283
|
/** Run in headless mode (default: false) */
|
|
282
284
|
headless?: boolean;
|
|
283
285
|
/** Reuse recent connection if available (default: true) */
|
|
@@ -498,6 +500,36 @@ export interface ExecOptions {
|
|
|
498
500
|
silent?: boolean;
|
|
499
501
|
}
|
|
500
502
|
|
|
503
|
+
/** Options for captcha command */
|
|
504
|
+
export interface CaptchaOptions {
|
|
505
|
+
/** 2captcha API key (required) */
|
|
506
|
+
apiKey: string;
|
|
507
|
+
/** Override auto-detected sitekey */
|
|
508
|
+
sitekey?: string;
|
|
509
|
+
/** Captcha type: 'recaptcha_v2', 'recaptcha_v3', 'hcaptcha', 'turnstile' */
|
|
510
|
+
type?: string;
|
|
511
|
+
/** reCAPTCHA v3 action (default: 'verify') */
|
|
512
|
+
action?: string;
|
|
513
|
+
/** Whether to auto-submit the form (default: true) */
|
|
514
|
+
autoSubmit?: boolean;
|
|
515
|
+
/** Polling interval in ms for 2captcha (default: 5000) */
|
|
516
|
+
pollInterval?: number;
|
|
517
|
+
/** Max time in ms to wait for solution (default: 120000) */
|
|
518
|
+
timeout?: number;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Result of captcha solving */
|
|
522
|
+
export interface CaptchaResult {
|
|
523
|
+
/** Whether the captcha was solved successfully */
|
|
524
|
+
success: boolean;
|
|
525
|
+
/** Success/error message */
|
|
526
|
+
message: string;
|
|
527
|
+
/** The solved captcha token */
|
|
528
|
+
token: string | null;
|
|
529
|
+
/** Raw output from the solver script */
|
|
530
|
+
output: string;
|
|
531
|
+
}
|
|
532
|
+
|
|
501
533
|
/**
|
|
502
534
|
* A Promise that resolves to an Element but also has chainable element methods.
|
|
503
535
|
* This enables syntax like: await testdriver.find("button").click()
|
|
@@ -800,7 +832,7 @@ export default class TestDriverSDK {
|
|
|
800
832
|
/**
|
|
801
833
|
* The operating system of the sandbox
|
|
802
834
|
*/
|
|
803
|
-
readonly os:
|
|
835
|
+
readonly os: "windows" | "linux";
|
|
804
836
|
|
|
805
837
|
/**
|
|
806
838
|
* Provision API for launching applications
|
|
@@ -844,7 +876,7 @@ export default class TestDriverSDK {
|
|
|
844
876
|
*/
|
|
845
877
|
getLastSandboxId(): {
|
|
846
878
|
sandboxId: string | null;
|
|
847
|
-
os:
|
|
879
|
+
os: "windows" | "linux";
|
|
848
880
|
ami: string | null;
|
|
849
881
|
instanceType: string | null;
|
|
850
882
|
timestamp: string | null;
|
|
@@ -879,7 +911,10 @@ export default class TestDriverSDK {
|
|
|
879
911
|
* await element.click();
|
|
880
912
|
*/
|
|
881
913
|
find(description: string, cacheThreshold?: number): ChainableElementPromise;
|
|
882
|
-
find(
|
|
914
|
+
find(
|
|
915
|
+
description: string,
|
|
916
|
+
options?: { cacheThreshold?: number; cacheKey?: string; timeout?: number },
|
|
917
|
+
): ChainableElementPromise;
|
|
883
918
|
|
|
884
919
|
/**
|
|
885
920
|
* Find all elements matching a description
|
|
@@ -924,20 +959,23 @@ export default class TestDriverSDK {
|
|
|
924
959
|
* Type text
|
|
925
960
|
* @param text - Text to type
|
|
926
961
|
* @param options - Options object with delay and secret
|
|
927
|
-
*
|
|
962
|
+
*
|
|
928
963
|
* @example
|
|
929
964
|
* // Type regular text
|
|
930
965
|
* await client.type('hello world');
|
|
931
|
-
*
|
|
966
|
+
*
|
|
932
967
|
* @example
|
|
933
968
|
* // Type a password securely (not logged or stored)
|
|
934
969
|
* await client.type(process.env.TD_PASSWORD, { secret: true });
|
|
935
|
-
*
|
|
970
|
+
*
|
|
936
971
|
* @example
|
|
937
972
|
* // Type with custom delay
|
|
938
973
|
* await client.type('slow typing', { delay: 100 });
|
|
939
974
|
*/
|
|
940
|
-
type(
|
|
975
|
+
type(
|
|
976
|
+
text: string | number,
|
|
977
|
+
options?: { delay?: number; secret?: boolean },
|
|
978
|
+
): Promise<void>;
|
|
941
979
|
|
|
942
980
|
/**
|
|
943
981
|
* Wait for text to appear on screen
|
|
@@ -1081,7 +1119,10 @@ export default class TestDriverSDK {
|
|
|
1081
1119
|
* @param direction - Direction to scroll (default: 'down')
|
|
1082
1120
|
* @param options - Options object with amount
|
|
1083
1121
|
*/
|
|
1084
|
-
scroll(
|
|
1122
|
+
scroll(
|
|
1123
|
+
direction?: ScrollDirection,
|
|
1124
|
+
options?: { amount?: number },
|
|
1125
|
+
): Promise<void>;
|
|
1085
1126
|
|
|
1086
1127
|
// Application Control
|
|
1087
1128
|
|
|
@@ -1112,6 +1153,12 @@ export default class TestDriverSDK {
|
|
|
1112
1153
|
*/
|
|
1113
1154
|
extract(description: string): Promise<string>;
|
|
1114
1155
|
|
|
1156
|
+
/**
|
|
1157
|
+
* Solve a captcha on the current page using 2captcha service
|
|
1158
|
+
* @param options - Captcha solving options
|
|
1159
|
+
*/
|
|
1160
|
+
captcha(options: CaptchaOptions): Promise<CaptchaResult>;
|
|
1161
|
+
|
|
1115
1162
|
// Code Execution
|
|
1116
1163
|
|
|
1117
1164
|
/**
|