@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.
Files changed (41) hide show
  1. package/CLI_IMPLEMENTATION_PLAN.md +369 -0
  2. package/README.md +167 -0
  3. package/bin/spectra.js +7 -0
  4. package/bin/testspectra-runner +0 -0
  5. package/dist/commands/devices.d.ts +3 -0
  6. package/dist/commands/devices.js +37 -0
  7. package/dist/commands/doctor.d.ts +3 -0
  8. package/dist/commands/doctor.js +54 -0
  9. package/dist/commands/init.d.ts +3 -0
  10. package/dist/commands/init.js +401 -0
  11. package/dist/commands/run.d.ts +8 -0
  12. package/dist/commands/run.js +82 -0
  13. package/dist/commands/watch.d.ts +3 -0
  14. package/dist/commands/watch.js +30 -0
  15. package/dist/config/loader.d.ts +7 -0
  16. package/dist/config/loader.js +79 -0
  17. package/dist/config/schema.d.ts +365 -0
  18. package/dist/config/schema.js +80 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +51 -0
  21. package/dist/runner/bridge.d.ts +20 -0
  22. package/dist/runner/bridge.js +122 -0
  23. package/dist/runner/reporter.d.ts +26 -0
  24. package/dist/runner/reporter.js +42 -0
  25. package/dist/types/generator.d.ts +7 -0
  26. package/dist/types/generator.js +195 -0
  27. package/package.json +32 -0
  28. package/src/commands/devices.ts +41 -0
  29. package/src/commands/doctor.ts +57 -0
  30. package/src/commands/init.ts +424 -0
  31. package/src/commands/run.ts +102 -0
  32. package/src/commands/watch.ts +34 -0
  33. package/src/config/loader.ts +82 -0
  34. package/src/config/schema.ts +489 -0
  35. package/src/index.ts +61 -0
  36. package/src/runner/bridge.ts +146 -0
  37. package/src/runner/reporter.ts +64 -0
  38. package/src/types/generator.ts +202 -0
  39. package/src/types/webdriverio.d.ts +46 -0
  40. package/testspectra-cli-1.0.0.tgz +0 -0
  41. 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
+ }