@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,424 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { ConfigLoader } from "../config/loader.js";
|
|
4
|
+
import { TypeGenerator } from "../types/generator.js";
|
|
5
|
+
|
|
6
|
+
export async function initCommand(options: { force?: boolean }) {
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const existingConfig = ConfigLoader.findConfigFile(cwd);
|
|
9
|
+
|
|
10
|
+
if (existingConfig && !options.force) {
|
|
11
|
+
console.log(`\x1b[33m[TestSpectra]\x1b[0m Config already exists at ${path.basename(existingConfig)}. Use --force to overwrite.`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 1. Ensure directories exist
|
|
16
|
+
const dirs = [
|
|
17
|
+
"specs/TC-LOGIN-01",
|
|
18
|
+
"page-objects/LoginPage",
|
|
19
|
+
"actions/verifyOtp",
|
|
20
|
+
"steps/loginUser",
|
|
21
|
+
"fixtures",
|
|
22
|
+
"hooks/default",
|
|
23
|
+
".testspectra",
|
|
24
|
+
];
|
|
25
|
+
for (const d of dirs) {
|
|
26
|
+
const dirPath = path.join(cwd, d);
|
|
27
|
+
if (!fs.existsSync(dirPath)) {
|
|
28
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 2. Create spectra.config.ts
|
|
33
|
+
const configContent = `import { defineConfig } from "@testspectra/cli";
|
|
34
|
+
|
|
35
|
+
export default defineConfig({
|
|
36
|
+
webConfig: {
|
|
37
|
+
baseUrl: "https://the-internet.herokuapp.com",
|
|
38
|
+
maxConcurrentSessions: "1",
|
|
39
|
+
headlessMode: true,
|
|
40
|
+
implicitWait: "5000",
|
|
41
|
+
pageLoadTimeout: "30000",
|
|
42
|
+
scriptTimeout: "30000",
|
|
43
|
+
parallelizationMode: "testcase",
|
|
44
|
+
},
|
|
45
|
+
browsers: [
|
|
46
|
+
{
|
|
47
|
+
id: "chrome-desktop",
|
|
48
|
+
type: "chrome",
|
|
49
|
+
mobileEmulation: false,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
androidConfig: {
|
|
53
|
+
appiumServer: "http://127.0.0.1:4723",
|
|
54
|
+
platformName: "Android",
|
|
55
|
+
platformVersion: "13",
|
|
56
|
+
deviceName: "emulator-5554",
|
|
57
|
+
automationName: "UiAutomator2",
|
|
58
|
+
appPackage: "",
|
|
59
|
+
appActivity: "",
|
|
60
|
+
autoGrantPermissions: true,
|
|
61
|
+
noReset: false,
|
|
62
|
+
implicitWait: "10000",
|
|
63
|
+
parallelizationMode: "suite",
|
|
64
|
+
},
|
|
65
|
+
iosConfig: {
|
|
66
|
+
appiumServer: "http://127.0.0.1:4723",
|
|
67
|
+
platformName: "iOS",
|
|
68
|
+
platformVersion: "16.0",
|
|
69
|
+
deviceName: "iPhone 14",
|
|
70
|
+
automationName: "XCUITest",
|
|
71
|
+
bundleId: "",
|
|
72
|
+
udid: "auto",
|
|
73
|
+
xcodeOrgId: "",
|
|
74
|
+
xcodeSigningId: "iPhone Developer",
|
|
75
|
+
autoAcceptAlerts: true,
|
|
76
|
+
noReset: false,
|
|
77
|
+
implicitWait: "10000",
|
|
78
|
+
parallelizationMode: "suite",
|
|
79
|
+
},
|
|
80
|
+
loadConfig: {
|
|
81
|
+
virtualUsers: "10",
|
|
82
|
+
duration: "1m",
|
|
83
|
+
},
|
|
84
|
+
loadStages: [],
|
|
85
|
+
thresholds: [],
|
|
86
|
+
executionConfig: {
|
|
87
|
+
networkMonitoringEnabled: true,
|
|
88
|
+
fastResponseTime: "200",
|
|
89
|
+
normalResponseTime: "1000",
|
|
90
|
+
monitoredDomains: [],
|
|
91
|
+
environmentVariables: [],
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
`;
|
|
95
|
+
fs.writeFileSync(path.join(cwd, "spectra.config.ts"), configContent, "utf-8");
|
|
96
|
+
console.log(`\x1b[32m[TestSpectra]\x1b[0m Initialized spectra.config.ts with defineConfig`);
|
|
97
|
+
|
|
98
|
+
// 3. Create or update package.json with npm scripts & devDependencies
|
|
99
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
100
|
+
let pkg: any = {
|
|
101
|
+
name: path.basename(cwd),
|
|
102
|
+
version: "1.0.0",
|
|
103
|
+
private: true,
|
|
104
|
+
type: "module",
|
|
105
|
+
scripts: {
|
|
106
|
+
dev: "spectra watch",
|
|
107
|
+
test: "spectra run",
|
|
108
|
+
"type-check": "tsc -b",
|
|
109
|
+
},
|
|
110
|
+
devDependencies: {
|
|
111
|
+
"@testspectra/cli": "workspace:*",
|
|
112
|
+
"@types/node": "^20.14.0",
|
|
113
|
+
"@wdio/globals": "^9.2.8",
|
|
114
|
+
"@wdio/mocha-framework": "^9.2.8",
|
|
115
|
+
"webdriverio": "^9.2.8",
|
|
116
|
+
"typescript": "^5.4.5",
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
120
|
+
try {
|
|
121
|
+
const existing = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
122
|
+
pkg = {
|
|
123
|
+
...existing,
|
|
124
|
+
scripts: {
|
|
125
|
+
...(existing.scripts || {}),
|
|
126
|
+
dev: existing.scripts?.dev || "spectra watch",
|
|
127
|
+
test: existing.scripts?.test || "spectra run",
|
|
128
|
+
"type-check": existing.scripts?.["type-check"] || "tsc -b",
|
|
129
|
+
},
|
|
130
|
+
devDependencies: {
|
|
131
|
+
...(existing.devDependencies || {}),
|
|
132
|
+
"@testspectra/cli": existing.devDependencies?.["@testspectra/cli"] || "workspace:*",
|
|
133
|
+
"@types/node": existing.devDependencies?.["@types/node"] || "^20.14.0",
|
|
134
|
+
"@wdio/globals": existing.devDependencies?.["@wdio/globals"] || "^9.2.8",
|
|
135
|
+
"@wdio/mocha-framework": existing.devDependencies?.["@wdio/mocha-framework"] || "^9.2.8",
|
|
136
|
+
"webdriverio": existing.devDependencies?.["webdriverio"] || "^9.2.8",
|
|
137
|
+
"typescript": existing.devDependencies?.["typescript"] || "^5.4.5",
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
} catch {}
|
|
141
|
+
}
|
|
142
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), "utf-8");
|
|
143
|
+
console.log(`\x1b[32m[TestSpectra]\x1b[0m Updated package.json (scripts & devDependencies)`);
|
|
144
|
+
|
|
145
|
+
// 3b. Create .gitignore
|
|
146
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
147
|
+
if (!fs.existsSync(gitignorePath) || options.force) {
|
|
148
|
+
const gitignoreContent = `node_modules/\ndist/\n.testspectra/\n*.tsbuildinfo\n`;
|
|
149
|
+
fs.writeFileSync(gitignorePath, gitignoreContent, "utf-8");
|
|
150
|
+
console.log(`\x1b[32m[TestSpectra]\x1b[0m Created .gitignore`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 4. Create platform tsconfigs with Solution-Style Project References
|
|
154
|
+
const tsconfigRoot = {
|
|
155
|
+
compilerOptions: {
|
|
156
|
+
target: "ES2022",
|
|
157
|
+
module: "NodeNext",
|
|
158
|
+
moduleResolution: "NodeNext",
|
|
159
|
+
types: ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
|
|
160
|
+
skipLibCheck: true,
|
|
161
|
+
strict: true,
|
|
162
|
+
noEmit: true,
|
|
163
|
+
},
|
|
164
|
+
files: ["spectra.config.ts"],
|
|
165
|
+
references: [
|
|
166
|
+
{ path: "./tsconfig.web.json" },
|
|
167
|
+
{ path: "./tsconfig.android.json" },
|
|
168
|
+
{ path: "./tsconfig.ios.json" },
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
fs.writeFileSync(path.join(cwd, "tsconfig.json"), JSON.stringify(tsconfigRoot, null, 2), "utf-8");
|
|
172
|
+
|
|
173
|
+
const tsconfigWeb = {
|
|
174
|
+
compilerOptions: {
|
|
175
|
+
target: "ES2022",
|
|
176
|
+
module: "NodeNext",
|
|
177
|
+
moduleResolution: "NodeNext",
|
|
178
|
+
types: ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
|
|
179
|
+
skipLibCheck: true,
|
|
180
|
+
strict: true,
|
|
181
|
+
composite: true,
|
|
182
|
+
emitDeclarationOnly: true,
|
|
183
|
+
outDir: "./.testspectra/.build/web",
|
|
184
|
+
},
|
|
185
|
+
include: [
|
|
186
|
+
"specs/**/web.test.ts",
|
|
187
|
+
"specs/**/common.test.ts",
|
|
188
|
+
"page-objects/**/web.ts",
|
|
189
|
+
"page-objects/**/common.ts",
|
|
190
|
+
"actions/**/web.action.ts",
|
|
191
|
+
"actions/**/common.action.ts",
|
|
192
|
+
"steps/**/web.step.ts",
|
|
193
|
+
"steps/**/common.step.ts",
|
|
194
|
+
"hooks/**/before.web.hook.ts",
|
|
195
|
+
"hooks/**/before.hook.ts",
|
|
196
|
+
".testspectra/types/web.d.ts",
|
|
197
|
+
".testspectra/types/common.d.ts",
|
|
198
|
+
".testspectra/types/fixtures.d.ts",
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
fs.writeFileSync(path.join(cwd, "tsconfig.web.json"), JSON.stringify(tsconfigWeb, null, 2), "utf-8");
|
|
202
|
+
|
|
203
|
+
const tsconfigAndroid = {
|
|
204
|
+
compilerOptions: {
|
|
205
|
+
target: "ES2022",
|
|
206
|
+
module: "NodeNext",
|
|
207
|
+
moduleResolution: "NodeNext",
|
|
208
|
+
types: ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
|
|
209
|
+
skipLibCheck: true,
|
|
210
|
+
strict: true,
|
|
211
|
+
composite: true,
|
|
212
|
+
emitDeclarationOnly: true,
|
|
213
|
+
outDir: "./.testspectra/.build/android",
|
|
214
|
+
},
|
|
215
|
+
include: [
|
|
216
|
+
"specs/**/android.test.ts",
|
|
217
|
+
"specs/**/mobile.test.ts",
|
|
218
|
+
"specs/**/common.test.ts",
|
|
219
|
+
"page-objects/**/android.ts",
|
|
220
|
+
"page-objects/**/mobile.ts",
|
|
221
|
+
"page-objects/**/common.ts",
|
|
222
|
+
"actions/**/android.action.ts",
|
|
223
|
+
"actions/**/mobile.action.ts",
|
|
224
|
+
"actions/**/common.action.ts",
|
|
225
|
+
"steps/**/android.step.ts",
|
|
226
|
+
"steps/**/mobile.step.ts",
|
|
227
|
+
"steps/**/common.step.ts",
|
|
228
|
+
"hooks/**/before.android.hook.ts",
|
|
229
|
+
"hooks/**/before.mobile.hook.ts",
|
|
230
|
+
"hooks/**/before.hook.ts",
|
|
231
|
+
".testspectra/types/android.d.ts",
|
|
232
|
+
".testspectra/types/mobile.d.ts",
|
|
233
|
+
".testspectra/types/common.d.ts",
|
|
234
|
+
".testspectra/types/fixtures.d.ts",
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
fs.writeFileSync(path.join(cwd, "tsconfig.android.json"), JSON.stringify(tsconfigAndroid, null, 2), "utf-8");
|
|
238
|
+
|
|
239
|
+
const tsconfigIos = {
|
|
240
|
+
compilerOptions: {
|
|
241
|
+
target: "ES2022",
|
|
242
|
+
module: "NodeNext",
|
|
243
|
+
moduleResolution: "NodeNext",
|
|
244
|
+
types: ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
|
|
245
|
+
skipLibCheck: true,
|
|
246
|
+
strict: true,
|
|
247
|
+
composite: true,
|
|
248
|
+
emitDeclarationOnly: true,
|
|
249
|
+
outDir: "./.testspectra/.build/ios",
|
|
250
|
+
},
|
|
251
|
+
include: [
|
|
252
|
+
"specs/**/ios.test.ts",
|
|
253
|
+
"specs/**/mobile.test.ts",
|
|
254
|
+
"specs/**/common.test.ts",
|
|
255
|
+
"page-objects/**/ios.ts",
|
|
256
|
+
"page-objects/**/mobile.ts",
|
|
257
|
+
"page-objects/**/common.ts",
|
|
258
|
+
"actions/**/ios.action.ts",
|
|
259
|
+
"actions/**/mobile.action.ts",
|
|
260
|
+
"actions/**/common.action.ts",
|
|
261
|
+
"steps/**/ios.step.ts",
|
|
262
|
+
"steps/**/mobile.step.ts",
|
|
263
|
+
"steps/**/common.step.ts",
|
|
264
|
+
"hooks/**/before.ios.hook.ts",
|
|
265
|
+
"hooks/**/before.mobile.hook.ts",
|
|
266
|
+
"hooks/**/before.hook.ts",
|
|
267
|
+
".testspectra/types/ios.d.ts",
|
|
268
|
+
".testspectra/types/mobile.d.ts",
|
|
269
|
+
".testspectra/types/common.d.ts",
|
|
270
|
+
".testspectra/types/fixtures.d.ts",
|
|
271
|
+
],
|
|
272
|
+
};
|
|
273
|
+
fs.writeFileSync(path.join(cwd, "tsconfig.ios.json"), JSON.stringify(tsconfigIos, null, 2), "utf-8");
|
|
274
|
+
console.log(`\x1b[32m[TestSpectra]\x1b[0m Created platform-isolated tsconfigs with solution references`);
|
|
275
|
+
|
|
276
|
+
// 5. Scaffold Fixture File
|
|
277
|
+
const fixturePath = path.join(cwd, "fixtures", "userData.json");
|
|
278
|
+
if (!fs.existsSync(fixturePath) || options.force) {
|
|
279
|
+
fs.writeFileSync(fixturePath, JSON.stringify({ username: "tomsmith", role: "admin" }, null, 2), "utf-8");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 6. Scaffold Page Objects (Clean zero-import methods with explicit ChainablePromiseElement types)
|
|
283
|
+
const poWeb = `export default class LoginPage {
|
|
284
|
+
static get usernameInput(): ChainablePromiseElement {
|
|
285
|
+
return $('#username');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
static get passwordInput(): ChainablePromiseElement {
|
|
289
|
+
return $('#password');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
static get submitButton(): ChainablePromiseElement {
|
|
293
|
+
return $('button[type="submit"]');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
static async open() {
|
|
297
|
+
await browser.url('/login');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
static async login(username: string, pass: string) {
|
|
301
|
+
await this.usernameInput.setValue(username);
|
|
302
|
+
await this.passwordInput.setValue(pass);
|
|
303
|
+
await this.submitButton.click();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
`;
|
|
307
|
+
fs.writeFileSync(path.join(cwd, "page-objects/LoginPage/web.ts"), poWeb, "utf-8");
|
|
308
|
+
|
|
309
|
+
const poMobile = `export default class LoginPage {
|
|
310
|
+
static get usernameInput(): ChainablePromiseElement {
|
|
311
|
+
return $('~username_input');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
static get passwordInput(): ChainablePromiseElement {
|
|
315
|
+
return $('~password_input');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
static get submitButton(): ChainablePromiseElement {
|
|
319
|
+
return $('~login_button');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
static async open() {
|
|
323
|
+
// Mobile app startup
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
static async login(username: string, pass: string) {
|
|
327
|
+
await this.usernameInput.setValue(username);
|
|
328
|
+
await this.passwordInput.setValue(pass);
|
|
329
|
+
await this.submitButton.click();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
`;
|
|
333
|
+
fs.writeFileSync(path.join(cwd, "page-objects/LoginPage/mobile.ts"), poMobile, "utf-8");
|
|
334
|
+
|
|
335
|
+
// 7. Scaffold Actions (Zero triple-slash lines)
|
|
336
|
+
const actionWeb = `export default async function verifyOtp(otp: string): Promise<void> {
|
|
337
|
+
const otpInput = await $('#otp');
|
|
338
|
+
await otpInput.setValue(otp);
|
|
339
|
+
const verifyBtn = await $('#verify-btn');
|
|
340
|
+
await verifyBtn.click();
|
|
341
|
+
}
|
|
342
|
+
`;
|
|
343
|
+
fs.writeFileSync(path.join(cwd, "actions/verifyOtp/web.action.ts"), actionWeb, "utf-8");
|
|
344
|
+
|
|
345
|
+
const actionMobile = `export default async function verifyOtp(otp: string): Promise<void> {
|
|
346
|
+
const otpInput = await $('~otp_input');
|
|
347
|
+
await otpInput.setValue(otp);
|
|
348
|
+
const verifyBtn = await $('~verify_btn');
|
|
349
|
+
await verifyBtn.click();
|
|
350
|
+
}
|
|
351
|
+
`;
|
|
352
|
+
fs.writeFileSync(path.join(cwd, "actions/verifyOtp/mobile.action.ts"), actionMobile, "utf-8");
|
|
353
|
+
|
|
354
|
+
// 8. Scaffold Steps (Zero triple-slash lines)
|
|
355
|
+
const stepWeb = `export default async function loginUser(u: string, p: string): Promise<void> {
|
|
356
|
+
await LoginPage.open();
|
|
357
|
+
await LoginPage.login(u, p);
|
|
358
|
+
}
|
|
359
|
+
`;
|
|
360
|
+
fs.writeFileSync(path.join(cwd, "steps/loginUser/web.step.ts"), stepWeb, "utf-8");
|
|
361
|
+
|
|
362
|
+
const stepMobile = `export default async function loginUser(u: string, p: string): Promise<void> {
|
|
363
|
+
await LoginPage.open();
|
|
364
|
+
await LoginPage.login(u, p);
|
|
365
|
+
}
|
|
366
|
+
`;
|
|
367
|
+
fs.writeFileSync(path.join(cwd, "steps/loginUser/mobile.step.ts"), stepMobile, "utf-8");
|
|
368
|
+
|
|
369
|
+
// 9. Scaffold Hooks (Zero triple-slash lines)
|
|
370
|
+
const hookWeb = `export default async function (): Promise<void> {
|
|
371
|
+
await browser.maximizeWindow();
|
|
372
|
+
}
|
|
373
|
+
`;
|
|
374
|
+
fs.writeFileSync(path.join(cwd, "hooks/default/before.web.hook.ts"), hookWeb, "utf-8");
|
|
375
|
+
|
|
376
|
+
const hookAndroid = `export default async function (): Promise<void> {
|
|
377
|
+
// Setup Android Appium capabilities
|
|
378
|
+
}
|
|
379
|
+
`;
|
|
380
|
+
fs.writeFileSync(path.join(cwd, "hooks/default/before.android.hook.ts"), hookAndroid, "utf-8");
|
|
381
|
+
|
|
382
|
+
const hookIos = `export default async function (): Promise<void> {
|
|
383
|
+
// Setup iOS Appium capabilities
|
|
384
|
+
}
|
|
385
|
+
`;
|
|
386
|
+
fs.writeFileSync(path.join(cwd, "hooks/default/before.ios.hook.ts"), hookIos, "utf-8");
|
|
387
|
+
|
|
388
|
+
// 10. Scaffold Specs (Pure test scripts, ZERO triple-slash lines)
|
|
389
|
+
const specWeb = `it("should authenticate user using web locators and fixtures", async () => {
|
|
390
|
+
await browser.intercept("/api/status", "GET", Fixture.userData);
|
|
391
|
+
await LoginPage.open();
|
|
392
|
+
await LoginPage.login("tomsmith", "SuperSecretPassword!");
|
|
393
|
+
|
|
394
|
+
const flash = await $("#flash");
|
|
395
|
+
await expect(flash).toBeDisplayed();
|
|
396
|
+
});
|
|
397
|
+
`;
|
|
398
|
+
fs.writeFileSync(path.join(cwd, "specs/TC-LOGIN-01/web.test.ts"), specWeb, "utf-8");
|
|
399
|
+
|
|
400
|
+
const specAndroid = `it("should authenticate user on Android Appium device", async () => {
|
|
401
|
+
await LoginPage.open();
|
|
402
|
+
await LoginPage.login("tomsmith", "SuperSecretPassword!");
|
|
403
|
+
|
|
404
|
+
const welcome = await $("~welcome_text");
|
|
405
|
+
await expect(welcome).toBeDisplayed();
|
|
406
|
+
});
|
|
407
|
+
`;
|
|
408
|
+
fs.writeFileSync(path.join(cwd, "specs/TC-LOGIN-01/android.test.ts"), specAndroid, "utf-8");
|
|
409
|
+
|
|
410
|
+
const specIos = `it("should authenticate user on iOS Appium device", async () => {
|
|
411
|
+
await LoginPage.open();
|
|
412
|
+
await LoginPage.login("tomsmith", "SuperSecretPassword!");
|
|
413
|
+
|
|
414
|
+
const welcome = await $("~welcome_text");
|
|
415
|
+
await expect(welcome).toBeDisplayed();
|
|
416
|
+
});
|
|
417
|
+
`;
|
|
418
|
+
fs.writeFileSync(path.join(cwd, "specs/TC-LOGIN-01/ios.test.ts"), specIos, "utf-8");
|
|
419
|
+
|
|
420
|
+
// 11. Generate ambient declaration files
|
|
421
|
+
TypeGenerator.writeDeclarationFiles(cwd);
|
|
422
|
+
console.log(`\x1b[32m[TestSpectra]\x1b[0m Generated ambient multi-platform types in .testspectra/types/`);
|
|
423
|
+
console.log(`\x1b[32m[TestSpectra]\x1b[0m Project initialized successfully!`);
|
|
424
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { ConfigLoader } from "../config/loader.js";
|
|
4
|
+
import { RustCoreBridge } from "../runner/bridge.js";
|
|
5
|
+
import { Reporter } from "../runner/reporter.js";
|
|
6
|
+
import { TypeGenerator } from "../types/generator.js";
|
|
7
|
+
|
|
8
|
+
export interface RunCommandOptions {
|
|
9
|
+
target?: "web" | "android" | "ios" | "common";
|
|
10
|
+
device?: string;
|
|
11
|
+
headless?: boolean;
|
|
12
|
+
workdir?: string;
|
|
13
|
+
output?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function runCommand(specPath?: string, options: RunCommandOptions = {}) {
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
|
|
19
|
+
// Ensure latest ambient types are up-to-date
|
|
20
|
+
TypeGenerator.writeDeclarationFiles(cwd);
|
|
21
|
+
|
|
22
|
+
const config = await ConfigLoader.loadConfig(cwd);
|
|
23
|
+
|
|
24
|
+
if (options.headless !== undefined) {
|
|
25
|
+
config.webConfig.headlessMode = options.headless;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const platform = options.target || "web";
|
|
29
|
+
const appDataPath = options.workdir ? path.resolve(options.workdir) : path.join(cwd, ".testspectra");
|
|
30
|
+
|
|
31
|
+
if (!fs.existsSync(appDataPath)) {
|
|
32
|
+
fs.mkdirSync(appDataPath, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Determine suite/test case name
|
|
36
|
+
let suiteName = "default";
|
|
37
|
+
let testCases: Array<{ id: string; title: string; executionOrder?: number }> = [];
|
|
38
|
+
|
|
39
|
+
if (specPath) {
|
|
40
|
+
const parsed = path.parse(specPath);
|
|
41
|
+
const baseName = parsed.name.replace(/\.(web|android|ios|common|test|spec)/g, "");
|
|
42
|
+
suiteName = baseName;
|
|
43
|
+
testCases.push({
|
|
44
|
+
id: baseName,
|
|
45
|
+
title: baseName,
|
|
46
|
+
executionOrder: 1,
|
|
47
|
+
});
|
|
48
|
+
} else {
|
|
49
|
+
// Scan specs/ directory (both entity folders specs/TC-001/web.test.ts and flat files)
|
|
50
|
+
const specsDir = path.join(cwd, "specs");
|
|
51
|
+
if (fs.existsSync(specsDir)) {
|
|
52
|
+
const entries = fs.readdirSync(specsDir, { withFileTypes: true });
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (entry.name.startsWith(".")) continue;
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
testCases.push({ id: entry.name, title: entry.name });
|
|
57
|
+
} else if (entry.isFile() && (entry.name.endsWith(".test.ts") || entry.name.endsWith(".spec.ts"))) {
|
|
58
|
+
const id = entry.name.replace(/\.(web|android|ios|common|all|mobile|test|spec)\.ts$/g, "").replace(/\.test\.ts$/, "");
|
|
59
|
+
testCases.push({ id, title: id });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (testCases.length === 0) {
|
|
66
|
+
testCases.push({
|
|
67
|
+
id: suiteName,
|
|
68
|
+
title: suiteName,
|
|
69
|
+
executionOrder: 1,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const outputJsonPath = options.output
|
|
74
|
+
? path.resolve(options.output)
|
|
75
|
+
: path.join(appDataPath, "reports/result.json");
|
|
76
|
+
|
|
77
|
+
console.log(`\x1b[36m[TestSpectra]\x1b[0m Starting execution on target \x1b[1m${platform}\x1b[0m...`);
|
|
78
|
+
console.log(`\x1b[36m[TestSpectra]\x1b[0m Suite: ${suiteName} (${testCases.length} case(s))\n`);
|
|
79
|
+
|
|
80
|
+
const reporter = new Reporter();
|
|
81
|
+
try {
|
|
82
|
+
const result = await RustCoreBridge.run(
|
|
83
|
+
{
|
|
84
|
+
baseDir: cwd,
|
|
85
|
+
appDataPath,
|
|
86
|
+
platform,
|
|
87
|
+
suite: suiteName,
|
|
88
|
+
testCases,
|
|
89
|
+
config,
|
|
90
|
+
targetDevice: options.device,
|
|
91
|
+
outputJsonPath,
|
|
92
|
+
},
|
|
93
|
+
reporter
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
console.log(`\n\x1b[36m[TestSpectra]\x1b[0m Run finished: \x1b[1m${result.status.toUpperCase()}\x1b[0m in ${result.duration}`);
|
|
97
|
+
console.log(`\x1b[36m[TestSpectra]\x1b[0m Report saved to: ${outputJsonPath}`);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error(`\x1b[31m[TestSpectra] Execution error:\x1b[0m`, e);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { TypeGenerator } from "../types/generator.js";
|
|
4
|
+
|
|
5
|
+
export function watchCommand(options: { once?: boolean } = {}) {
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
|
|
8
|
+
// Run initial generation
|
|
9
|
+
TypeGenerator.writeDeclarationFiles(cwd);
|
|
10
|
+
console.log(`\x1b[32m[TestSpectra]\x1b[0m Generated ambient types in .testspectra/types/`);
|
|
11
|
+
|
|
12
|
+
if (options.once) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log(`\x1b[34m[TestSpectra]\x1b[0m Watching for changes in page-objects/, actions/, steps/, fixtures/...`);
|
|
17
|
+
|
|
18
|
+
const watchDirs = ["page-objects", "pageobjects", "actions", "steps", "fixtures"];
|
|
19
|
+
let debounceTimeout: NodeJS.Timeout | null = null;
|
|
20
|
+
|
|
21
|
+
for (const dir of watchDirs) {
|
|
22
|
+
const fullPath = path.join(cwd, dir);
|
|
23
|
+
if (fs.existsSync(fullPath)) {
|
|
24
|
+
fs.watch(fullPath, { recursive: true }, (_eventType, filename) => {
|
|
25
|
+
if (!filename || filename.startsWith(".")) return;
|
|
26
|
+
if (debounceTimeout) clearTimeout(debounceTimeout);
|
|
27
|
+
debounceTimeout = setTimeout(() => {
|
|
28
|
+
TypeGenerator.writeDeclarationFiles(cwd);
|
|
29
|
+
console.log(`\x1b[32m[TestSpectra]\x1b[0m Updated ambient declarations (.testspectra/types/) due to ${filename}`);
|
|
30
|
+
}, 150);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { ConfigData, DEFAULT_CONFIG_DATA } from "./schema.js";
|
|
4
|
+
|
|
5
|
+
export class ConfigLoader {
|
|
6
|
+
static readonly CONFIG_FILE_NAMES = [
|
|
7
|
+
"testspectra.config.ts",
|
|
8
|
+
"spectra.config.ts",
|
|
9
|
+
"testspectra.config.js",
|
|
10
|
+
"spectra.config.js",
|
|
11
|
+
"testspectra.config.json",
|
|
12
|
+
".testspectrarc.json",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
static findConfigFile(cwd: string = process.cwd()): string | null {
|
|
16
|
+
for (const filename of this.CONFIG_FILE_NAMES) {
|
|
17
|
+
const fullPath = path.join(cwd, filename);
|
|
18
|
+
if (fs.existsSync(fullPath)) {
|
|
19
|
+
return fullPath;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static async loadConfig(cwd: string = process.cwd(), overrides?: Partial<ConfigData>): Promise<ConfigData> {
|
|
26
|
+
const configPath = this.findConfigFile(cwd);
|
|
27
|
+
let loaded: Partial<ConfigData> = {};
|
|
28
|
+
|
|
29
|
+
if (configPath) {
|
|
30
|
+
if (configPath.endsWith(".json")) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
33
|
+
loaded = JSON.parse(raw);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.warn(`[TestSpectra] Warning: Failed to parse ${configPath}:`, e);
|
|
36
|
+
}
|
|
37
|
+
} else if (configPath.endsWith(".ts") || configPath.endsWith(".js")) {
|
|
38
|
+
try {
|
|
39
|
+
const fileUrl = new URL(`file://${path.resolve(configPath)}`).href;
|
|
40
|
+
const mod = await import(fileUrl);
|
|
41
|
+
loaded = mod.default || mod.config || mod;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.warn(`[TestSpectra] Warning: Failed to import ${configPath}:`, e);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...DEFAULT_CONFIG_DATA,
|
|
50
|
+
...loaded,
|
|
51
|
+
...overrides,
|
|
52
|
+
webConfig: {
|
|
53
|
+
...DEFAULT_CONFIG_DATA.webConfig,
|
|
54
|
+
...(loaded.webConfig || {}),
|
|
55
|
+
...(overrides?.webConfig || {}),
|
|
56
|
+
},
|
|
57
|
+
androidConfig: {
|
|
58
|
+
...DEFAULT_CONFIG_DATA.androidConfig,
|
|
59
|
+
...(loaded.androidConfig || {}),
|
|
60
|
+
...(overrides?.androidConfig || {}),
|
|
61
|
+
},
|
|
62
|
+
iosConfig: {
|
|
63
|
+
...DEFAULT_CONFIG_DATA.iosConfig,
|
|
64
|
+
...(loaded.iosConfig || {}),
|
|
65
|
+
...(overrides?.iosConfig || {}),
|
|
66
|
+
},
|
|
67
|
+
executionConfig: {
|
|
68
|
+
networkMonitoringEnabled: overrides?.executionConfig?.networkMonitoringEnabled ?? loaded.executionConfig?.networkMonitoringEnabled ?? DEFAULT_CONFIG_DATA.executionConfig?.networkMonitoringEnabled ?? true,
|
|
69
|
+
fastResponseTime: overrides?.executionConfig?.fastResponseTime ?? loaded.executionConfig?.fastResponseTime ?? DEFAULT_CONFIG_DATA.executionConfig?.fastResponseTime ?? "200",
|
|
70
|
+
normalResponseTime: overrides?.executionConfig?.normalResponseTime ?? loaded.executionConfig?.normalResponseTime ?? DEFAULT_CONFIG_DATA.executionConfig?.normalResponseTime ?? "1000",
|
|
71
|
+
monitoredDomains: overrides?.executionConfig?.monitoredDomains ?? loaded.executionConfig?.monitoredDomains ?? DEFAULT_CONFIG_DATA.executionConfig?.monitoredDomains ?? [],
|
|
72
|
+
environmentVariables: overrides?.executionConfig?.environmentVariables ?? loaded.executionConfig?.environmentVariables ?? DEFAULT_CONFIG_DATA.executionConfig?.environmentVariables ?? [],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static saveConfig(cwd: string, config: ConfigData, filename = "testspectra.config.json"): string {
|
|
78
|
+
const target = path.join(cwd, filename);
|
|
79
|
+
fs.writeFileSync(target, JSON.stringify(config, null, 2), "utf-8");
|
|
80
|
+
return target;
|
|
81
|
+
}
|
|
82
|
+
}
|