@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,42 @@
1
+ export class Reporter {
2
+ logs = [];
3
+ networkEvents = [];
4
+ addLog(log) {
5
+ this.logs.push(log);
6
+ const time = `\x1b[90m${log.timestamp}\x1b[0m`;
7
+ let prefix = `[${log.level}]`;
8
+ if (log.level === "SUCCESS" || log.level === "PASSED") {
9
+ prefix = `\x1b[32m${prefix}\x1b[0m`;
10
+ }
11
+ else if (log.level === "ERROR" || log.level === "FAILED") {
12
+ prefix = `\x1b[31m${prefix}\x1b[0m`;
13
+ }
14
+ else if (log.level === "WARN" || log.level === "WARNING") {
15
+ prefix = `\x1b[33m${prefix}\x1b[0m`;
16
+ }
17
+ else if (log.level === "INFO") {
18
+ prefix = `\x1b[36m${prefix}\x1b[0m`;
19
+ }
20
+ else {
21
+ prefix = `\x1b[90m${prefix}\x1b[0m`;
22
+ }
23
+ console.log(`${time} ${prefix} ${log.message}`);
24
+ }
25
+ addNetwork(res) {
26
+ this.networkEvents.push(res);
27
+ }
28
+ getLogs() {
29
+ return this.logs;
30
+ }
31
+ getNetworkEvents() {
32
+ return this.networkEvents;
33
+ }
34
+ generateResult(status, duration) {
35
+ return {
36
+ status,
37
+ duration,
38
+ logs: this.logs,
39
+ networkResources: this.networkEvents,
40
+ };
41
+ }
42
+ }
@@ -0,0 +1,7 @@
1
+ export type PlatformTarget = "web" | "android" | "ios" | "mobile" | "common";
2
+ export declare class TypeGenerator {
3
+ static getPageObjectsDir(cwd: string): string;
4
+ static generateAmbientDeclarations(cwd: string): Record<string, string>;
5
+ static generateFixturesDeclaration(cwd: string): string;
6
+ static writeDeclarationFiles(cwd: string): void;
7
+ }
@@ -0,0 +1,195 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ const PLATFORM_HIERARCHY = {
4
+ web: ["web", "common"],
5
+ ios: ["ios", "mobile", "common"],
6
+ android: ["android", "mobile", "common"],
7
+ mobile: ["mobile", "common"],
8
+ common: ["common"],
9
+ };
10
+ export class TypeGenerator {
11
+ static getPageObjectsDir(cwd) {
12
+ const p1 = path.join(cwd, "page-objects");
13
+ if (fs.existsSync(p1))
14
+ return p1;
15
+ return path.join(cwd, "pageobjects");
16
+ }
17
+ static generateAmbientDeclarations(cwd) {
18
+ const poDir = this.getPageObjectsDir(cwd);
19
+ const actionsDir = path.join(cwd, "actions");
20
+ const stepsDir = path.join(cwd, "steps");
21
+ const fixturesDir = path.join(cwd, "fixtures");
22
+ const platforms = ["web", "android", "ios", "mobile", "common"];
23
+ const results = {};
24
+ for (const platform of platforms) {
25
+ const hierarchy = PLATFORM_HIERARCHY[platform];
26
+ let content = `// Auto-generated ambient declarations for TestSpectra [${platform.toUpperCase()}]\n`;
27
+ content += `// Do not edit manually. Run 'spectra watch' or let the Language Service manage this.\n\n`;
28
+ content += `/// <reference types="@wdio/globals/types" />\n`;
29
+ content += `/// <reference types="@wdio/mocha-framework" />\n`;
30
+ content += `/// <reference path="./fixtures.d.ts" />\n\n`;
31
+ content += `declare namespace WebdriverIO {\n`;
32
+ content += ` export interface InterceptFixtureOptions {\n`;
33
+ content += ` statusCode?: number;\n`;
34
+ content += ` headers?: Record<string, string>;\n`;
35
+ content += ` }\n`;
36
+ content += ` export interface InterceptFixtureObject<TData = unknown> {\n`;
37
+ content += ` statusCode?: number;\n`;
38
+ content += ` body: TData;\n`;
39
+ content += ` headers?: Record<string, string>;\n`;
40
+ content += ` }\n`;
41
+ content += ` export type InterceptInput<TData = unknown> = InterceptFixtureObject<TData> | TData | string;\n`;
42
+ content += ` interface Mock {\n`;
43
+ content += ` respondWith<TData = unknown>(fixture: InterceptInput<TData>, options?: InterceptFixtureOptions): Promise<void>;\n`;
44
+ content += ` }\n`;
45
+ content += ` interface Browser {\n`;
46
+ content += ` intercept<TData = unknown>(path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', fixture?: InterceptInput<TData>, options?: InterceptFixtureOptions): Promise<Mock>;\n`;
47
+ content += ` }\n`;
48
+ content += `}\n\n`;
49
+ // 1. Page Objects Global Declarations (Hierarchical)
50
+ // 2. Page Objects Global Declarations (Hierarchical)
51
+ if (fs.existsSync(poDir)) {
52
+ const poEntries = fs.readdirSync(poDir, { withFileTypes: true });
53
+ for (const entry of poEntries) {
54
+ if (entry.name.startsWith("."))
55
+ continue;
56
+ if (entry.isDirectory()) {
57
+ const className = entry.name;
58
+ const classFolder = path.join(poDir, className);
59
+ // Search hierarchy
60
+ let matchedFile = null;
61
+ for (const stem of hierarchy) {
62
+ const cand = path.join(classFolder, `${stem}.ts`);
63
+ if (fs.existsSync(cand)) {
64
+ matchedFile = `../../page-objects/${className}/${stem}.js`;
65
+ break;
66
+ }
67
+ }
68
+ if (matchedFile) {
69
+ content += `declare const ${className}: typeof import('${matchedFile}').default;\n`;
70
+ }
71
+ }
72
+ else if (entry.isFile() && entry.name.endsWith(".ts")) {
73
+ const className = entry.name.split(".")[0];
74
+ content += `declare const ${className}: typeof import('../../page-objects/${entry.name.replace(".ts", ".js")}').default;\n`;
75
+ }
76
+ }
77
+ }
78
+ // 3. Actions Global Interface
79
+ content += `\ninterface TestSpectraActions {\n`;
80
+ if (fs.existsSync(actionsDir)) {
81
+ const actionEntries = fs.readdirSync(actionsDir, { withFileTypes: true });
82
+ for (const entry of actionEntries) {
83
+ if (entry.name.startsWith("."))
84
+ continue;
85
+ if (entry.isDirectory()) {
86
+ const actionName = entry.name;
87
+ const actionFolder = path.join(actionsDir, actionName);
88
+ let matchedFile = null;
89
+ for (const stem of hierarchy) {
90
+ const cand1 = path.join(actionFolder, `${stem}.action.ts`);
91
+ const cand2 = path.join(actionFolder, `${stem}.ts`);
92
+ if (fs.existsSync(cand1)) {
93
+ matchedFile = `../../actions/${actionName}/${stem}.action.js`;
94
+ break;
95
+ }
96
+ else if (fs.existsSync(cand2)) {
97
+ matchedFile = `../../actions/${actionName}/${stem}.js`;
98
+ break;
99
+ }
100
+ }
101
+ if (matchedFile) {
102
+ content += ` ${actionName}: typeof import('${matchedFile}');\n`;
103
+ }
104
+ else {
105
+ content += ` ${actionName}: (...args: any[]) => Promise<any>;\n`;
106
+ }
107
+ }
108
+ else if (entry.isFile() && entry.name.endsWith(".ts")) {
109
+ const actionName = entry.name.split(".")[0];
110
+ content += ` ${actionName}: typeof import('../../actions/${entry.name.replace(".ts", ".js")}');\n`;
111
+ }
112
+ }
113
+ }
114
+ content += `}\n`;
115
+ content += `declare const Spectra: TestSpectraActions;\n\n`;
116
+ // 4. Shared Steps Global Interface
117
+ content += `interface TestSpectraSteps {\n`;
118
+ if (fs.existsSync(stepsDir)) {
119
+ const stepEntries = fs.readdirSync(stepsDir, { withFileTypes: true });
120
+ for (const entry of stepEntries) {
121
+ if (entry.name.startsWith("."))
122
+ continue;
123
+ if (entry.isDirectory()) {
124
+ const stepName = entry.name;
125
+ const stepFolder = path.join(stepsDir, stepName);
126
+ let matchedFile = null;
127
+ for (const stem of hierarchy) {
128
+ const cand1 = path.join(stepFolder, `${stem}.step.ts`);
129
+ const cand2 = path.join(stepFolder, `${stem}.ts`);
130
+ if (fs.existsSync(cand1)) {
131
+ matchedFile = `../../steps/${stepName}/${stem}.step.js`;
132
+ break;
133
+ }
134
+ else if (fs.existsSync(cand2)) {
135
+ matchedFile = `../../steps/${stepName}/${stem}.js`;
136
+ break;
137
+ }
138
+ }
139
+ if (matchedFile) {
140
+ content += ` ${stepName}: typeof import('${matchedFile}');\n`;
141
+ }
142
+ else {
143
+ content += ` ${stepName}: (...args: any[]) => Promise<any>;\n`;
144
+ }
145
+ }
146
+ else if (entry.isFile() && entry.name.endsWith(".ts")) {
147
+ const stepName = entry.name.split(".")[0];
148
+ content += ` ${stepName}: typeof import('../../steps/${entry.name.replace(".ts", ".js")}');\n`;
149
+ }
150
+ }
151
+ }
152
+ content += `}\n`;
153
+ content += `declare const Step: TestSpectraSteps;\n\n`;
154
+ results[platform] = content;
155
+ }
156
+ return results;
157
+ }
158
+ static generateFixturesDeclaration(cwd) {
159
+ const fixturesDir = path.join(cwd, "fixtures");
160
+ let content = `// Auto-generated ambient fixture declarations for TestSpectra\n`;
161
+ content += `// Do not edit manually. Run 'spectra watch' or let the Language Service manage this.\n\n`;
162
+ content += `interface TestSpectraFixtures {\n`;
163
+ if (fs.existsSync(fixturesDir)) {
164
+ const fixtureFiles = fs.readdirSync(fixturesDir);
165
+ for (const file of fixtureFiles) {
166
+ if (file.startsWith("."))
167
+ continue;
168
+ const parsed = path.parse(file);
169
+ const varName = parsed.name.replace(/[^a-zA-Z0-9_$]/g, "_");
170
+ content += ` readonly ${varName}: string;\n`;
171
+ }
172
+ }
173
+ content += `}\n`;
174
+ content += `declare const Fixture: TestSpectraFixtures;\n`;
175
+ return content;
176
+ }
177
+ static writeDeclarationFiles(cwd) {
178
+ const typesDir = path.join(cwd, ".testspectra", "types");
179
+ if (!fs.existsSync(typesDir)) {
180
+ fs.mkdirSync(typesDir, { recursive: true });
181
+ }
182
+ // 1. Write single platform-agnostic fixtures.d.ts
183
+ const fixturesContent = this.generateFixturesDeclaration(cwd);
184
+ fs.writeFileSync(path.join(typesDir, "fixtures.d.ts"), fixturesContent, "utf-8");
185
+ // 2. Write per-platform ambient types
186
+ const declarations = this.generateAmbientDeclarations(cwd);
187
+ for (const [platform, content] of Object.entries(declarations)) {
188
+ const filePath = path.join(typesDir, `${platform}.d.ts`);
189
+ fs.writeFileSync(filePath, content, "utf-8");
190
+ }
191
+ // Also write a universal ambient index
192
+ const universalPath = path.join(typesDir, "index.d.ts");
193
+ fs.writeFileSync(universalPath, `/// <reference path="./common.d.ts" />\n`, "utf-8");
194
+ }
195
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@testspectra/cli",
3
+ "version": "1.0.0",
4
+ "description": "TestSpectra Zero-Config Cross-Platform Test Runner CLI",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "spectra": "./bin/spectra.js",
9
+ "testspectra": "./bin/spectra.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "watch": "tsc -w",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "dependencies": {
17
+ "chalk": "^5.3.0",
18
+ "chokidar": "^3.6.0",
19
+ "commander": "^12.1.0",
20
+ "dotenv": "^16.4.5",
21
+ "inquirer": "^9.3.2",
22
+ "ora": "^8.0.1",
23
+ "zod": "^3.23.8"
24
+ },
25
+ "devDependencies": {
26
+ "@types/inquirer": "^9.0.7",
27
+ "@types/node": "^20.14.0",
28
+ "typescript": "^5.4.5"
29
+ },
30
+ "type": "module",
31
+ "license": "MIT"
32
+ }
@@ -0,0 +1,41 @@
1
+ import { execSync } from "child_process";
2
+
3
+ export async function devicesCommand(options: { target?: "android" | "ios" | "web" | "all" }) {
4
+ const target = options.target || "all";
5
+ console.log(`\x1b[36m[TestSpectra Devices]\x1b[0m Listing connected test targets (scope: ${target})...\n`);
6
+
7
+ if (target === "all" || target === "android") {
8
+ console.log("\x1b[1mAndroid Devices (ADB):\x1b[0m");
9
+ try {
10
+ const adbOutput = execSync("adb devices -l", { stdio: "pipe" }).toString();
11
+ const lines = adbOutput.split("\n").slice(1);
12
+ let found = false;
13
+ for (const line of lines) {
14
+ if (line.trim()) {
15
+ found = true;
16
+ console.log(` \x1b[32m•\x1b[0m ${line.trim()}`);
17
+ }
18
+ }
19
+ if (!found) {
20
+ console.log(" \x1b[90mNo Android devices/emulators connected via ADB.\x1b[0m");
21
+ }
22
+ } catch {
23
+ console.log(" \x1b[31mADB command failed or platform-tools not in PATH.\x1b[0m");
24
+ }
25
+ }
26
+
27
+ if (target === "all" || target === "web") {
28
+ console.log("\n\x1b[1mWeb Browsers:\x1b[0m");
29
+ try {
30
+ const chromeVer = execSync(
31
+ process.platform === "darwin"
32
+ ? '"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --version'
33
+ : "google-chrome --version",
34
+ { stdio: "pipe" }
35
+ ).toString().trim();
36
+ console.log(` \x1b[32m•\x1b[0m Chrome: ${chromeVer}`);
37
+ } catch {
38
+ console.log(" \x1b[90m• Chrome: Not installed\x1b[0m");
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,57 @@
1
+ import { execSync } from "child_process";
2
+
3
+ export async function doctorCommand(options: { fix?: boolean }) {
4
+ console.log("\x1b[36m[TestSpectra Doctor]\x1b[0m Checking development and runtime dependencies...\n");
5
+
6
+ const checks = [
7
+ {
8
+ name: "Node.js",
9
+ command: "node -v",
10
+ required: true,
11
+ },
12
+ {
13
+ name: "Bun",
14
+ command: "bun -v",
15
+ required: false,
16
+ },
17
+ {
18
+ name: "ADB (Android Debug Bridge)",
19
+ command: "adb version",
20
+ required: false,
21
+ },
22
+ {
23
+ name: "Java JDK",
24
+ command: "java -version",
25
+ required: false,
26
+ },
27
+ {
28
+ name: "Google Chrome",
29
+ command: process.platform === "darwin"
30
+ ? '"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --version'
31
+ : "google-chrome --version",
32
+ required: false,
33
+ },
34
+ ];
35
+
36
+ let missingCount = 0;
37
+
38
+ for (const check of checks) {
39
+ try {
40
+ const out = execSync(check.command, { stdio: "pipe" }).toString().trim();
41
+ const firstLine = out.split("\n")[0];
42
+ console.log(` \x1b[32m✓\x1b[0m ${check.name.padEnd(30)} \x1b[90m(${firstLine})\x1b[0m`);
43
+ } catch {
44
+ console.log(` \x1b[31m✗\x1b[0m ${check.name.padEnd(30)} \x1b[31m(Not found)\x1b[0m`);
45
+ if (check.required) {
46
+ missingCount++;
47
+ }
48
+ }
49
+ }
50
+
51
+ console.log("\n------------------------------------------------------------");
52
+ if (missingCount === 0) {
53
+ console.log("\x1b[32m[Doctor]\x1b[0m System ready for local test execution!");
54
+ } else {
55
+ console.log(`\x1b[33m[Doctor]\x1b[0m Found ${missingCount} missing required dependencies.`);
56
+ }
57
+ }