@testspectra/cli 1.0.0
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/CLI_IMPLEMENTATION_PLAN.md +369 -0
- package/README.md +167 -0
- package/bin/spectra.js +7 -0
- package/bin/testspectra-runner +0 -0
- package/dist/commands/devices.d.ts +3 -0
- package/dist/commands/devices.js +37 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +54 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +401 -0
- package/dist/commands/run.d.ts +8 -0
- package/dist/commands/run.js +82 -0
- package/dist/commands/watch.d.ts +3 -0
- package/dist/commands/watch.js +30 -0
- package/dist/config/loader.d.ts +7 -0
- package/dist/config/loader.js +79 -0
- package/dist/config/schema.d.ts +365 -0
- package/dist/config/schema.js +80 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +51 -0
- package/dist/runner/bridge.d.ts +20 -0
- package/dist/runner/bridge.js +122 -0
- package/dist/runner/reporter.d.ts +26 -0
- package/dist/runner/reporter.js +42 -0
- package/dist/types/generator.d.ts +7 -0
- package/dist/types/generator.js +195 -0
- package/package.json +32 -0
- package/src/commands/devices.ts +41 -0
- package/src/commands/doctor.ts +57 -0
- package/src/commands/init.ts +424 -0
- package/src/commands/run.ts +102 -0
- package/src/commands/watch.ts +34 -0
- package/src/config/loader.ts +82 -0
- package/src/config/schema.ts +489 -0
- package/src/index.ts +61 -0
- package/src/runner/bridge.ts +146 -0
- package/src/runner/reporter.ts +64 -0
- package/src/types/generator.ts +202 -0
- package/src/types/webdriverio.d.ts +46 -0
- package/testspectra-cli-1.0.0.tgz +0 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for web automation and browser test runners.
|
|
3
|
+
*/
|
|
4
|
+
export interface WebConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Base URL used to resolve relative paths in tests (e.g. `browser.url('/login')` or `browser.intercept('/api/users')`).
|
|
7
|
+
* @default "http://localhost:3000"
|
|
8
|
+
*/
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
/**
|
|
11
|
+
* Maximum number of parallel browser sessions/workers spawned during test execution.
|
|
12
|
+
* @default "1"
|
|
13
|
+
*/
|
|
14
|
+
maxConcurrentSessions: string;
|
|
15
|
+
/**
|
|
16
|
+
* Whether to run browsers in headless mode without a visible GUI window.
|
|
17
|
+
* @default true
|
|
18
|
+
*/
|
|
19
|
+
headlessMode: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Implicit wait timeout (in milliseconds) for finding elements before throwing an error.
|
|
22
|
+
* @default "5000"
|
|
23
|
+
*/
|
|
24
|
+
implicitWait: string;
|
|
25
|
+
/**
|
|
26
|
+
* Maximum time (in milliseconds) allowed for a webpage to complete navigation and initial load.
|
|
27
|
+
* @default "30000"
|
|
28
|
+
*/
|
|
29
|
+
pageLoadTimeout: string;
|
|
30
|
+
/**
|
|
31
|
+
* Maximum time (in milliseconds) allowed for asynchronous scripts to finish executing.
|
|
32
|
+
* @default "30000"
|
|
33
|
+
*/
|
|
34
|
+
scriptTimeout: string;
|
|
35
|
+
/**
|
|
36
|
+
* Parallelization granularity:
|
|
37
|
+
* - `"testcase"`: Each test case executes in parallel across available workers.
|
|
38
|
+
* - `"suite"`: Test cases within a suite run serially, while different suites run in parallel.
|
|
39
|
+
* @default "testcase"
|
|
40
|
+
*/
|
|
41
|
+
parallelizationMode: "testcase" | "suite";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Browser target definition for web test execution.
|
|
45
|
+
*/
|
|
46
|
+
export interface Browser {
|
|
47
|
+
/**
|
|
48
|
+
* Unique identifier for this browser configuration.
|
|
49
|
+
*/
|
|
50
|
+
id: string;
|
|
51
|
+
/**
|
|
52
|
+
* Browser type/engine: `"chrome"`, `"firefox"`, `"safari"`, `"edge"`.
|
|
53
|
+
* @default "chrome"
|
|
54
|
+
*/
|
|
55
|
+
type: string;
|
|
56
|
+
/**
|
|
57
|
+
* Whether to simulate mobile viewport and touch metrics.
|
|
58
|
+
* @default false
|
|
59
|
+
*/
|
|
60
|
+
mobileEmulation: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Emulated device name when `mobileEmulation` is true (e.g. `"iPhone 14"`, `"Pixel 7"`).
|
|
63
|
+
*/
|
|
64
|
+
deviceName?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Custom browser window width in pixels.
|
|
67
|
+
*/
|
|
68
|
+
width?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Custom browser window height in pixels.
|
|
71
|
+
*/
|
|
72
|
+
height?: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Configuration for Android mobile test automation with Appium & UiAutomator2.
|
|
76
|
+
*/
|
|
77
|
+
export interface AndroidConfig {
|
|
78
|
+
/**
|
|
79
|
+
* Appium server endpoint URL.
|
|
80
|
+
* @default "http://127.0.0.1:4723"
|
|
81
|
+
*/
|
|
82
|
+
appiumServer: string;
|
|
83
|
+
/**
|
|
84
|
+
* Mobile platform name.
|
|
85
|
+
* @default "Android"
|
|
86
|
+
*/
|
|
87
|
+
platformName: string;
|
|
88
|
+
/**
|
|
89
|
+
* Android OS version (e.g. `"13"`, `"14"`).
|
|
90
|
+
* @default "13"
|
|
91
|
+
*/
|
|
92
|
+
platformVersion: string;
|
|
93
|
+
/**
|
|
94
|
+
* Target Android device name or ADB serial ID (e.g. `"emulator-5554"`).
|
|
95
|
+
* @default "emulator-5554"
|
|
96
|
+
*/
|
|
97
|
+
deviceName: string;
|
|
98
|
+
/**
|
|
99
|
+
* Appium mobile automation driver engine.
|
|
100
|
+
* @default "UiAutomator2"
|
|
101
|
+
*/
|
|
102
|
+
automationName: string;
|
|
103
|
+
/**
|
|
104
|
+
* Android application package name (e.g. `"com.example.app"`).
|
|
105
|
+
*/
|
|
106
|
+
appPackage: string;
|
|
107
|
+
/**
|
|
108
|
+
* Main activity name launched when starting the app.
|
|
109
|
+
*/
|
|
110
|
+
appActivity: string;
|
|
111
|
+
/**
|
|
112
|
+
* Whether to automatically grant Android runtime permissions upon installation.
|
|
113
|
+
* @default true
|
|
114
|
+
*/
|
|
115
|
+
autoGrantPermissions: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Whether to preserve app state and prevent clearing app data/cache between sessions.
|
|
118
|
+
* @default false
|
|
119
|
+
*/
|
|
120
|
+
noReset: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Implicit wait timeout (in milliseconds) for locating mobile elements.
|
|
123
|
+
* @default "10000"
|
|
124
|
+
*/
|
|
125
|
+
implicitWait: string;
|
|
126
|
+
/**
|
|
127
|
+
* Parallelization execution mode for Android mobile tests.
|
|
128
|
+
* @default "suite"
|
|
129
|
+
*/
|
|
130
|
+
parallelizationMode: "testcase" | "suite";
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Configuration for iOS mobile test automation with Appium & XCUITest.
|
|
134
|
+
*/
|
|
135
|
+
export interface IosConfig {
|
|
136
|
+
/**
|
|
137
|
+
* Appium server endpoint URL.
|
|
138
|
+
* @default "http://127.0.0.1:4723"
|
|
139
|
+
*/
|
|
140
|
+
appiumServer: string;
|
|
141
|
+
/**
|
|
142
|
+
* Mobile platform name.
|
|
143
|
+
* @default "iOS"
|
|
144
|
+
*/
|
|
145
|
+
platformName: string;
|
|
146
|
+
/**
|
|
147
|
+
* iOS version (e.g. `"16.0"`, `"17.0"`).
|
|
148
|
+
* @default "16.0"
|
|
149
|
+
*/
|
|
150
|
+
platformVersion: string;
|
|
151
|
+
/**
|
|
152
|
+
* iOS simulator or connected physical device name (e.g. `"iPhone 14"`).
|
|
153
|
+
* @default "iPhone 14"
|
|
154
|
+
*/
|
|
155
|
+
deviceName: string;
|
|
156
|
+
/**
|
|
157
|
+
* Appium mobile automation driver engine for iOS.
|
|
158
|
+
* @default "XCUITest"
|
|
159
|
+
*/
|
|
160
|
+
automationName: string;
|
|
161
|
+
/**
|
|
162
|
+
* iOS Application Bundle Identifier (e.g. `"com.example.iosapp"`).
|
|
163
|
+
*/
|
|
164
|
+
bundleId: string;
|
|
165
|
+
/**
|
|
166
|
+
* Target device Unique Device Identifier (UDID) or `"auto"`.
|
|
167
|
+
* @default "auto"
|
|
168
|
+
*/
|
|
169
|
+
udid: string;
|
|
170
|
+
/**
|
|
171
|
+
* Apple Developer Team 10-character Organization ID for code signing.
|
|
172
|
+
*/
|
|
173
|
+
xcodeOrgId: string;
|
|
174
|
+
/**
|
|
175
|
+
* Xcode code signing identity string (e.g. `"iPhone Developer"`).
|
|
176
|
+
* @default "iPhone Developer"
|
|
177
|
+
*/
|
|
178
|
+
xcodeSigningId: string;
|
|
179
|
+
/**
|
|
180
|
+
* Whether to automatically accept iOS system permission alerts.
|
|
181
|
+
* @default true
|
|
182
|
+
*/
|
|
183
|
+
autoAcceptAlerts: boolean;
|
|
184
|
+
/**
|
|
185
|
+
* Whether to preserve app state between sessions.
|
|
186
|
+
* @default false
|
|
187
|
+
*/
|
|
188
|
+
noReset: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Implicit wait timeout (in milliseconds) for locating iOS UI elements.
|
|
191
|
+
* @default "10000"
|
|
192
|
+
*/
|
|
193
|
+
implicitWait: string;
|
|
194
|
+
/**
|
|
195
|
+
* Parallelization execution mode for iOS mobile tests.
|
|
196
|
+
* @default "suite"
|
|
197
|
+
*/
|
|
198
|
+
parallelizationMode: "testcase" | "suite";
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Load testing stage ramp-up profile.
|
|
202
|
+
*/
|
|
203
|
+
export interface LoadStage {
|
|
204
|
+
/**
|
|
205
|
+
* Stage identifier.
|
|
206
|
+
*/
|
|
207
|
+
id: string;
|
|
208
|
+
/**
|
|
209
|
+
* Duration for this load stage (e.g. `"30s"`, `"2m"`).
|
|
210
|
+
*/
|
|
211
|
+
duration: string;
|
|
212
|
+
/**
|
|
213
|
+
* Target virtual users (VUs) to scale to during this stage.
|
|
214
|
+
*/
|
|
215
|
+
targetVUs: string;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Performance and pass/fail metric threshold definition.
|
|
219
|
+
*/
|
|
220
|
+
export interface SuccessThreshold {
|
|
221
|
+
/**
|
|
222
|
+
* Unique threshold identifier.
|
|
223
|
+
*/
|
|
224
|
+
id: string;
|
|
225
|
+
/**
|
|
226
|
+
* Performance metric type (e.g. `"http_req_duration"`, `"http_req_failed"`).
|
|
227
|
+
*/
|
|
228
|
+
metricType: string;
|
|
229
|
+
/**
|
|
230
|
+
* Maximum acceptable value (e.g. `"500ms"`, `"0.01"`).
|
|
231
|
+
*/
|
|
232
|
+
maxValue: string;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Specific domain filter for real-time network interception and recording.
|
|
236
|
+
*/
|
|
237
|
+
export interface MonitoredDomain {
|
|
238
|
+
/**
|
|
239
|
+
* Hostname or regex pattern to monitor (e.g. `"api.example.com"`).
|
|
240
|
+
*/
|
|
241
|
+
domain: string;
|
|
242
|
+
/**
|
|
243
|
+
* Whether network logging and CDP interception is active for this domain.
|
|
244
|
+
*/
|
|
245
|
+
enabled: boolean;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Custom environment variable key-value pair injected into the test execution sandbox.
|
|
249
|
+
*/
|
|
250
|
+
export interface EnvironmentVariable {
|
|
251
|
+
/**
|
|
252
|
+
* Environment variable name (e.g. `"API_SECRET_KEY"`).
|
|
253
|
+
*/
|
|
254
|
+
key: string;
|
|
255
|
+
/**
|
|
256
|
+
* Environment variable value.
|
|
257
|
+
*/
|
|
258
|
+
value: string;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Test execution, CDP network monitoring, and sandbox environment configuration.
|
|
262
|
+
*/
|
|
263
|
+
export interface ExecutionConfig {
|
|
264
|
+
/**
|
|
265
|
+
* Whether to record network requests and timing metrics via Chrome DevTools Protocol.
|
|
266
|
+
* @default true
|
|
267
|
+
*/
|
|
268
|
+
networkMonitoringEnabled: boolean;
|
|
269
|
+
/**
|
|
270
|
+
* Upper threshold (in milliseconds) classified as a fast network response.
|
|
271
|
+
* @default "200"
|
|
272
|
+
*/
|
|
273
|
+
fastResponseTime: string;
|
|
274
|
+
/**
|
|
275
|
+
* Upper threshold (in milliseconds) classified as a normal network response.
|
|
276
|
+
* @default "1000"
|
|
277
|
+
*/
|
|
278
|
+
normalResponseTime: string;
|
|
279
|
+
/**
|
|
280
|
+
* Whitelist of domains to monitor. If empty, all requests are captured.
|
|
281
|
+
*/
|
|
282
|
+
monitoredDomains?: MonitoredDomain[];
|
|
283
|
+
/**
|
|
284
|
+
* Custom environment variables injected into the test process runtime.
|
|
285
|
+
*/
|
|
286
|
+
environmentVariables?: EnvironmentVariable[];
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Root configuration data structure matching TestSpectra project schema.
|
|
290
|
+
*/
|
|
291
|
+
export interface ConfigData {
|
|
292
|
+
/**
|
|
293
|
+
* Web browser execution settings.
|
|
294
|
+
*/
|
|
295
|
+
webConfig: WebConfig;
|
|
296
|
+
/**
|
|
297
|
+
* List of target browsers for web automation runs.
|
|
298
|
+
*/
|
|
299
|
+
browsers: Browser[];
|
|
300
|
+
/**
|
|
301
|
+
* Android Appium driver and device options.
|
|
302
|
+
*/
|
|
303
|
+
androidConfig: AndroidConfig;
|
|
304
|
+
/**
|
|
305
|
+
* iOS Appium driver and device options.
|
|
306
|
+
*/
|
|
307
|
+
iosConfig: IosConfig;
|
|
308
|
+
/**
|
|
309
|
+
* Load testing parameters.
|
|
310
|
+
*/
|
|
311
|
+
loadConfig: {
|
|
312
|
+
virtualUsers: string;
|
|
313
|
+
duration: string;
|
|
314
|
+
};
|
|
315
|
+
/**
|
|
316
|
+
* Load testing stages.
|
|
317
|
+
*/
|
|
318
|
+
loadStages: LoadStage[];
|
|
319
|
+
/**
|
|
320
|
+
* Pass/fail metric thresholds.
|
|
321
|
+
*/
|
|
322
|
+
thresholds: SuccessThreshold[];
|
|
323
|
+
/**
|
|
324
|
+
* Advanced execution and network recording settings.
|
|
325
|
+
*/
|
|
326
|
+
executionConfig?: ExecutionConfig;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Deep recursive partial type for user-friendly configuration objects.
|
|
330
|
+
*/
|
|
331
|
+
export type DeepPartial<T> = {
|
|
332
|
+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* User-provided configuration options with optional deep partial properties.
|
|
336
|
+
*/
|
|
337
|
+
export type UserConfig = DeepPartial<ConfigData>;
|
|
338
|
+
/**
|
|
339
|
+
* Helper function providing type hinting and autocompletion for `spectra.config.ts`.
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* ```ts
|
|
343
|
+
* import { defineConfig } from "@testspectra/cli";
|
|
344
|
+
*
|
|
345
|
+
* export default defineConfig({
|
|
346
|
+
* webConfig: {
|
|
347
|
+
* baseUrl: "https://example.com",
|
|
348
|
+
* headlessMode: false,
|
|
349
|
+
* },
|
|
350
|
+
* });
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
353
|
+
export declare function defineConfig(config: UserConfig): UserConfig;
|
|
354
|
+
/**
|
|
355
|
+
* Complete project persistence record.
|
|
356
|
+
*/
|
|
357
|
+
export interface ProjectConfig {
|
|
358
|
+
project_id: string;
|
|
359
|
+
config_data: ConfigData;
|
|
360
|
+
updated_at?: string;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Default fallback configuration object.
|
|
364
|
+
*/
|
|
365
|
+
export declare const DEFAULT_CONFIG_DATA: ConfigData;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper function providing type hinting and autocompletion for `spectra.config.ts`.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { defineConfig } from "@testspectra/cli";
|
|
7
|
+
*
|
|
8
|
+
* export default defineConfig({
|
|
9
|
+
* webConfig: {
|
|
10
|
+
* baseUrl: "https://example.com",
|
|
11
|
+
* headlessMode: false,
|
|
12
|
+
* },
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function defineConfig(config) {
|
|
17
|
+
return config;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Default fallback configuration object.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_CONFIG_DATA = {
|
|
23
|
+
webConfig: {
|
|
24
|
+
baseUrl: "http://localhost:3000",
|
|
25
|
+
maxConcurrentSessions: "1",
|
|
26
|
+
headlessMode: true,
|
|
27
|
+
implicitWait: "5000",
|
|
28
|
+
pageLoadTimeout: "30000",
|
|
29
|
+
scriptTimeout: "30000",
|
|
30
|
+
parallelizationMode: "testcase",
|
|
31
|
+
},
|
|
32
|
+
browsers: [
|
|
33
|
+
{
|
|
34
|
+
id: "chrome-desktop",
|
|
35
|
+
type: "chrome",
|
|
36
|
+
mobileEmulation: false,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
androidConfig: {
|
|
40
|
+
appiumServer: "http://127.0.0.1:4723",
|
|
41
|
+
platformName: "Android",
|
|
42
|
+
platformVersion: "13",
|
|
43
|
+
deviceName: "emulator-5554",
|
|
44
|
+
automationName: "UiAutomator2",
|
|
45
|
+
appPackage: "",
|
|
46
|
+
appActivity: "",
|
|
47
|
+
autoGrantPermissions: true,
|
|
48
|
+
noReset: false,
|
|
49
|
+
implicitWait: "10000",
|
|
50
|
+
parallelizationMode: "suite",
|
|
51
|
+
},
|
|
52
|
+
iosConfig: {
|
|
53
|
+
appiumServer: "http://127.0.0.1:4723",
|
|
54
|
+
platformName: "iOS",
|
|
55
|
+
platformVersion: "16.0",
|
|
56
|
+
deviceName: "iPhone 14",
|
|
57
|
+
automationName: "XCUITest",
|
|
58
|
+
bundleId: "",
|
|
59
|
+
udid: "auto",
|
|
60
|
+
xcodeOrgId: "",
|
|
61
|
+
xcodeSigningId: "iPhone Developer",
|
|
62
|
+
autoAcceptAlerts: true,
|
|
63
|
+
noReset: false,
|
|
64
|
+
implicitWait: "10000",
|
|
65
|
+
parallelizationMode: "suite",
|
|
66
|
+
},
|
|
67
|
+
loadConfig: {
|
|
68
|
+
virtualUsers: "10",
|
|
69
|
+
duration: "1m",
|
|
70
|
+
},
|
|
71
|
+
loadStages: [],
|
|
72
|
+
thresholds: [],
|
|
73
|
+
executionConfig: {
|
|
74
|
+
networkMonitoringEnabled: true,
|
|
75
|
+
fastResponseTime: "200",
|
|
76
|
+
normalResponseTime: "1000",
|
|
77
|
+
monitoredDomains: [],
|
|
78
|
+
environmentVariables: [],
|
|
79
|
+
},
|
|
80
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
export * from "./config/schema.js";
|
|
3
|
+
export * from "./config/loader.js";
|
|
4
|
+
export * from "./types/generator.js";
|
|
5
|
+
export declare function createCliProgram(): Command;
|
|
6
|
+
export declare function runCli(args?: string[]): Promise<void>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { devicesCommand } from "./commands/devices.js";
|
|
3
|
+
import { doctorCommand } from "./commands/doctor.js";
|
|
4
|
+
import { initCommand } from "./commands/init.js";
|
|
5
|
+
import { runCommand } from "./commands/run.js";
|
|
6
|
+
import { watchCommand } from "./commands/watch.js";
|
|
7
|
+
export * from "./config/schema.js";
|
|
8
|
+
export * from "./config/loader.js";
|
|
9
|
+
export * from "./types/generator.js";
|
|
10
|
+
export function createCliProgram() {
|
|
11
|
+
const program = new Command();
|
|
12
|
+
program
|
|
13
|
+
.name("spectra")
|
|
14
|
+
.description("TestSpectra Zero-Config Cross-Platform Test Runner CLI")
|
|
15
|
+
.version("2.0.0");
|
|
16
|
+
program
|
|
17
|
+
.command("init")
|
|
18
|
+
.description("Initialize testspectra.config.json and project directories")
|
|
19
|
+
.option("-f, --force", "Overwrite existing configuration if present")
|
|
20
|
+
.action(initCommand);
|
|
21
|
+
program
|
|
22
|
+
.command("doctor")
|
|
23
|
+
.description("Verify local environment prerequisites (ADB, Java, Chrome, Bun, Node)")
|
|
24
|
+
.option("--fix", "Attempt automatic fix / download of missing tools")
|
|
25
|
+
.action(doctorCommand);
|
|
26
|
+
program
|
|
27
|
+
.command("devices")
|
|
28
|
+
.description("List connected Android/iOS devices and local browsers")
|
|
29
|
+
.option("-t, --target <target>", "Filter scope: android, ios, web, all", "all")
|
|
30
|
+
.action(devicesCommand);
|
|
31
|
+
program
|
|
32
|
+
.command("run [path]")
|
|
33
|
+
.description("Execute test suite or individual test case")
|
|
34
|
+
.option("-t, --target <target>", "Target platform: web, android, ios, common", "web")
|
|
35
|
+
.option("-d, --device <device>", "Target device name or UDID")
|
|
36
|
+
.option("--headless", "Run in headless mode")
|
|
37
|
+
.option("--no-headless", "Run in headed mode")
|
|
38
|
+
.option("-w, --workdir <dir>", "Custom app data / temp workdir")
|
|
39
|
+
.option("-o, --output <path>", "Custom test report JSON output path")
|
|
40
|
+
.action(runCommand);
|
|
41
|
+
program
|
|
42
|
+
.command("watch")
|
|
43
|
+
.description("Watch page-objects, actions, steps, fixtures and auto-regenerate ambient types")
|
|
44
|
+
.option("--once", "Generate ambient types once and exit")
|
|
45
|
+
.action(watchCommand);
|
|
46
|
+
return program;
|
|
47
|
+
}
|
|
48
|
+
export async function runCli(args = process.argv) {
|
|
49
|
+
const program = createCliProgram();
|
|
50
|
+
await program.parseAsync(args);
|
|
51
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ConfigData } from "../config/schema.js";
|
|
2
|
+
import { Reporter, TestRunResult } from "./reporter.js";
|
|
3
|
+
export interface RunOptions {
|
|
4
|
+
baseDir: string;
|
|
5
|
+
appDataPath: string;
|
|
6
|
+
platform: "web" | "android" | "ios" | "common";
|
|
7
|
+
suite: string;
|
|
8
|
+
testCases: Array<{
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
executionOrder?: number;
|
|
12
|
+
}>;
|
|
13
|
+
config: ConfigData;
|
|
14
|
+
targetDevice?: string;
|
|
15
|
+
outputJsonPath?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class RustCoreBridge {
|
|
18
|
+
static resolveBinaryPath(): string;
|
|
19
|
+
static run(options: RunOptions, reporter: Reporter): Promise<TestRunResult>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
export class RustCoreBridge {
|
|
8
|
+
static resolveBinaryPath() {
|
|
9
|
+
// 1. Look for precompiled release/debug binary in workspace target
|
|
10
|
+
const workspaceRoot = path.resolve(__dirname, "../../..");
|
|
11
|
+
const candidatePaths = [
|
|
12
|
+
path.join(workspaceRoot, "target/debug/testspectra-runner"),
|
|
13
|
+
path.join(workspaceRoot, "target/release/testspectra-runner"),
|
|
14
|
+
path.join(workspaceRoot, "core/test-runner/target/debug/testspectra-runner"),
|
|
15
|
+
path.join(workspaceRoot, "core/test-runner/target/release/testspectra-runner"),
|
|
16
|
+
path.join(__dirname, "../../bin/testspectra-runner"),
|
|
17
|
+
];
|
|
18
|
+
for (const p of candidatePaths) {
|
|
19
|
+
if (fs.existsSync(p)) {
|
|
20
|
+
return p;
|
|
21
|
+
}
|
|
22
|
+
if (fs.existsSync(`${p}.exe`)) {
|
|
23
|
+
return `${p}.exe`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return "cargo"; // Fallback to cargo run
|
|
27
|
+
}
|
|
28
|
+
static async run(options, reporter) {
|
|
29
|
+
const binPath = this.resolveBinaryPath();
|
|
30
|
+
const payload = JSON.stringify({
|
|
31
|
+
baseDir: options.baseDir,
|
|
32
|
+
appDataPath: options.appDataPath,
|
|
33
|
+
platform: options.platform,
|
|
34
|
+
suite: options.suite,
|
|
35
|
+
testCases: options.testCases,
|
|
36
|
+
config: options.config,
|
|
37
|
+
targetDevice: options.targetDevice,
|
|
38
|
+
});
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
let child;
|
|
41
|
+
if (binPath === "cargo") {
|
|
42
|
+
const workspaceRoot = path.resolve(__dirname, "../../..");
|
|
43
|
+
child = spawn("cargo", ["run", "--manifest-path", "core/test-runner/Cargo.toml", "--bin", "testspectra-runner", "--", payload], {
|
|
44
|
+
cwd: workspaceRoot,
|
|
45
|
+
env: { ...process.env, RUST_LOG: "info" },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
child = spawn(binPath, [payload], {
|
|
50
|
+
env: { ...process.env, RUST_LOG: "info" },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
let runStatus = "failed";
|
|
54
|
+
let runDuration = "0s";
|
|
55
|
+
child.stdout.on("data", (data) => {
|
|
56
|
+
const lines = data.toString().split("\n");
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (!line.trim())
|
|
59
|
+
continue;
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(line);
|
|
62
|
+
if (parsed.type === "log") {
|
|
63
|
+
reporter.addLog({
|
|
64
|
+
timestamp: parsed.timestamp,
|
|
65
|
+
level: parsed.level,
|
|
66
|
+
message: parsed.message,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else if (parsed.type === "network") {
|
|
70
|
+
reporter.addNetwork({
|
|
71
|
+
requestId: parsed.requestId,
|
|
72
|
+
url: parsed.url,
|
|
73
|
+
method: parsed.method,
|
|
74
|
+
status: parsed.status,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
else if (parsed.type === "complete") {
|
|
78
|
+
runStatus = parsed.status;
|
|
79
|
+
runDuration = parsed.duration;
|
|
80
|
+
}
|
|
81
|
+
else if (parsed.type === "error") {
|
|
82
|
+
reporter.addLog({
|
|
83
|
+
timestamp: new Date().toLocaleTimeString(),
|
|
84
|
+
level: "ERROR",
|
|
85
|
+
message: parsed.message,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Raw non-JSON output (cargo compile warnings, etc.)
|
|
91
|
+
console.log(line);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
child.stderr.on("data", (data) => {
|
|
96
|
+
const str = data.toString();
|
|
97
|
+
// Ignore normal compilation progress
|
|
98
|
+
if (!str.includes("Compiling") && !str.includes("Checking") && !str.includes("Finished")) {
|
|
99
|
+
reporter.addLog({
|
|
100
|
+
timestamp: new Date().toLocaleTimeString(),
|
|
101
|
+
level: "DEBUG",
|
|
102
|
+
message: str.trim(),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
child.on("close", (code) => {
|
|
107
|
+
const result = reporter.generateResult(code === 0 ? "passed" : runStatus, runDuration);
|
|
108
|
+
if (options.outputJsonPath) {
|
|
109
|
+
const outDir = path.dirname(options.outputJsonPath);
|
|
110
|
+
if (!fs.existsSync(outDir)) {
|
|
111
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
fs.writeFileSync(options.outputJsonPath, JSON.stringify(result, null, 2), "utf-8");
|
|
114
|
+
}
|
|
115
|
+
resolve(result);
|
|
116
|
+
});
|
|
117
|
+
child.on("error", (err) => {
|
|
118
|
+
reject(err);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface TestLog {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
level: "INFO" | "SUCCESS" | "WARNING" | "ERROR" | "DEBUG" | string;
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export interface NetworkResource {
|
|
7
|
+
requestId: string;
|
|
8
|
+
url: string;
|
|
9
|
+
method: string;
|
|
10
|
+
status: number;
|
|
11
|
+
}
|
|
12
|
+
export interface TestRunResult {
|
|
13
|
+
status: "passed" | "failed" | "error" | string;
|
|
14
|
+
duration: string;
|
|
15
|
+
logs: TestLog[];
|
|
16
|
+
networkResources?: NetworkResource[];
|
|
17
|
+
}
|
|
18
|
+
export declare class Reporter {
|
|
19
|
+
private logs;
|
|
20
|
+
private networkEvents;
|
|
21
|
+
addLog(log: TestLog): void;
|
|
22
|
+
addNetwork(res: NetworkResource): void;
|
|
23
|
+
getLogs(): TestLog[];
|
|
24
|
+
getNetworkEvents(): NetworkResource[];
|
|
25
|
+
generateResult(status: string, duration: string): TestRunResult;
|
|
26
|
+
}
|