@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,64 @@
1
+ export interface TestLog {
2
+ timestamp: string;
3
+ level: "INFO" | "SUCCESS" | "WARNING" | "ERROR" | "DEBUG" | string;
4
+ message: string;
5
+ }
6
+
7
+ export interface NetworkResource {
8
+ requestId: string;
9
+ url: string;
10
+ method: string;
11
+ status: number;
12
+ }
13
+
14
+ export interface TestRunResult {
15
+ status: "passed" | "failed" | "error" | string;
16
+ duration: string;
17
+ logs: TestLog[];
18
+ networkResources?: NetworkResource[];
19
+ }
20
+
21
+ export class Reporter {
22
+ private logs: TestLog[] = [];
23
+ private networkEvents: NetworkResource[] = [];
24
+
25
+ addLog(log: TestLog) {
26
+ this.logs.push(log);
27
+ const time = `\x1b[90m${log.timestamp}\x1b[0m`;
28
+ let prefix = `[${log.level}]`;
29
+ if (log.level === "SUCCESS" || log.level === "PASSED") {
30
+ prefix = `\x1b[32m${prefix}\x1b[0m`;
31
+ } else if (log.level === "ERROR" || log.level === "FAILED") {
32
+ prefix = `\x1b[31m${prefix}\x1b[0m`;
33
+ } else if (log.level === "WARN" || log.level === "WARNING") {
34
+ prefix = `\x1b[33m${prefix}\x1b[0m`;
35
+ } else if (log.level === "INFO") {
36
+ prefix = `\x1b[36m${prefix}\x1b[0m`;
37
+ } else {
38
+ prefix = `\x1b[90m${prefix}\x1b[0m`;
39
+ }
40
+
41
+ console.log(`${time} ${prefix} ${log.message}`);
42
+ }
43
+
44
+ addNetwork(res: NetworkResource) {
45
+ this.networkEvents.push(res);
46
+ }
47
+
48
+ getLogs(): TestLog[] {
49
+ return this.logs;
50
+ }
51
+
52
+ getNetworkEvents(): NetworkResource[] {
53
+ return this.networkEvents;
54
+ }
55
+
56
+ generateResult(status: string, duration: string): TestRunResult {
57
+ return {
58
+ status,
59
+ duration,
60
+ logs: this.logs,
61
+ networkResources: this.networkEvents,
62
+ };
63
+ }
64
+ }
@@ -0,0 +1,202 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export type PlatformTarget = "web" | "android" | "ios" | "mobile" | "common";
5
+
6
+ const PLATFORM_HIERARCHY: Record<PlatformTarget, string[]> = {
7
+ web: ["web", "common"],
8
+ ios: ["ios", "mobile", "common"],
9
+ android: ["android", "mobile", "common"],
10
+ mobile: ["mobile", "common"],
11
+ common: ["common"],
12
+ };
13
+
14
+ export class TypeGenerator {
15
+ static getPageObjectsDir(cwd: string): string {
16
+ const p1 = path.join(cwd, "page-objects");
17
+ if (fs.existsSync(p1)) return p1;
18
+ return path.join(cwd, "pageobjects");
19
+ }
20
+
21
+ static generateAmbientDeclarations(cwd: string): Record<string, string> {
22
+ const poDir = this.getPageObjectsDir(cwd);
23
+ const actionsDir = path.join(cwd, "actions");
24
+ const stepsDir = path.join(cwd, "steps");
25
+ const fixturesDir = path.join(cwd, "fixtures");
26
+
27
+ const platforms: PlatformTarget[] = ["web", "android", "ios", "mobile", "common"];
28
+ const results: Record<string, string> = {};
29
+
30
+ for (const platform of platforms) {
31
+ const hierarchy = PLATFORM_HIERARCHY[platform];
32
+ let content = `// Auto-generated ambient declarations for TestSpectra [${platform.toUpperCase()}]\n`;
33
+ content += `// Do not edit manually. Run 'spectra watch' or let the Language Service manage this.\n\n`;
34
+ content += `/// <reference types="@wdio/globals/types" />\n`;
35
+ content += `/// <reference types="@wdio/mocha-framework" />\n`;
36
+ content += `/// <reference path="./fixtures.d.ts" />\n\n`;
37
+
38
+ content += `declare namespace WebdriverIO {\n`;
39
+ content += ` export interface InterceptFixtureOptions {\n`;
40
+ content += ` statusCode?: number;\n`;
41
+ content += ` headers?: Record<string, string>;\n`;
42
+ content += ` }\n`;
43
+ content += ` export interface InterceptFixtureObject<TData = unknown> {\n`;
44
+ content += ` statusCode?: number;\n`;
45
+ content += ` body: TData;\n`;
46
+ content += ` headers?: Record<string, string>;\n`;
47
+ content += ` }\n`;
48
+ content += ` export type InterceptInput<TData = unknown> = InterceptFixtureObject<TData> | TData | string;\n`;
49
+ content += ` interface Mock {\n`;
50
+ content += ` respondWith<TData = unknown>(fixture: InterceptInput<TData>, options?: InterceptFixtureOptions): Promise<void>;\n`;
51
+ content += ` }\n`;
52
+ content += ` interface Browser {\n`;
53
+ content += ` intercept<TData = unknown>(path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', fixture?: InterceptInput<TData>, options?: InterceptFixtureOptions): Promise<Mock>;\n`;
54
+ content += ` }\n`;
55
+ content += `}\n\n`;
56
+
57
+ // 1. Page Objects Global Declarations (Hierarchical)
58
+
59
+ // 2. Page Objects Global Declarations (Hierarchical)
60
+ if (fs.existsSync(poDir)) {
61
+ const poEntries = fs.readdirSync(poDir, { withFileTypes: true });
62
+ for (const entry of poEntries) {
63
+ if (entry.name.startsWith(".")) continue;
64
+ if (entry.isDirectory()) {
65
+ const className = entry.name;
66
+ const classFolder = path.join(poDir, className);
67
+ // Search hierarchy
68
+ let matchedFile: string | null = null;
69
+ for (const stem of hierarchy) {
70
+ const cand = path.join(classFolder, `${stem}.ts`);
71
+ if (fs.existsSync(cand)) {
72
+ matchedFile = `../../page-objects/${className}/${stem}.js`;
73
+ break;
74
+ }
75
+ }
76
+ if (matchedFile) {
77
+ content += `declare const ${className}: typeof import('${matchedFile}').default;\n`;
78
+ }
79
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) {
80
+ const className = entry.name.split(".")[0];
81
+ content += `declare const ${className}: typeof import('../../page-objects/${entry.name.replace(".ts", ".js")}').default;\n`;
82
+ }
83
+ }
84
+ }
85
+
86
+ // 3. Actions Global Interface
87
+ content += `\ninterface TestSpectraActions {\n`;
88
+ if (fs.existsSync(actionsDir)) {
89
+ const actionEntries = fs.readdirSync(actionsDir, { withFileTypes: true });
90
+ for (const entry of actionEntries) {
91
+ if (entry.name.startsWith(".")) continue;
92
+ if (entry.isDirectory()) {
93
+ const actionName = entry.name;
94
+ const actionFolder = path.join(actionsDir, actionName);
95
+ let matchedFile: string | null = null;
96
+ for (const stem of hierarchy) {
97
+ const cand1 = path.join(actionFolder, `${stem}.action.ts`);
98
+ const cand2 = path.join(actionFolder, `${stem}.ts`);
99
+ if (fs.existsSync(cand1)) {
100
+ matchedFile = `../../actions/${actionName}/${stem}.action.js`;
101
+ break;
102
+ } else if (fs.existsSync(cand2)) {
103
+ matchedFile = `../../actions/${actionName}/${stem}.js`;
104
+ break;
105
+ }
106
+ }
107
+ if (matchedFile) {
108
+ content += ` ${actionName}: typeof import('${matchedFile}');\n`;
109
+ } else {
110
+ content += ` ${actionName}: (...args: any[]) => Promise<any>;\n`;
111
+ }
112
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) {
113
+ const actionName = entry.name.split(".")[0];
114
+ content += ` ${actionName}: typeof import('../../actions/${entry.name.replace(".ts", ".js")}');\n`;
115
+ }
116
+ }
117
+ }
118
+ content += `}\n`;
119
+ content += `declare const Spectra: TestSpectraActions;\n\n`;
120
+
121
+ // 4. Shared Steps Global Interface
122
+ content += `interface TestSpectraSteps {\n`;
123
+ if (fs.existsSync(stepsDir)) {
124
+ const stepEntries = fs.readdirSync(stepsDir, { withFileTypes: true });
125
+ for (const entry of stepEntries) {
126
+ if (entry.name.startsWith(".")) continue;
127
+ if (entry.isDirectory()) {
128
+ const stepName = entry.name;
129
+ const stepFolder = path.join(stepsDir, stepName);
130
+ let matchedFile: string | null = null;
131
+ for (const stem of hierarchy) {
132
+ const cand1 = path.join(stepFolder, `${stem}.step.ts`);
133
+ const cand2 = path.join(stepFolder, `${stem}.ts`);
134
+ if (fs.existsSync(cand1)) {
135
+ matchedFile = `../../steps/${stepName}/${stem}.step.js`;
136
+ break;
137
+ } else if (fs.existsSync(cand2)) {
138
+ matchedFile = `../../steps/${stepName}/${stem}.js`;
139
+ break;
140
+ }
141
+ }
142
+ if (matchedFile) {
143
+ content += ` ${stepName}: typeof import('${matchedFile}');\n`;
144
+ } else {
145
+ content += ` ${stepName}: (...args: any[]) => Promise<any>;\n`;
146
+ }
147
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) {
148
+ const stepName = entry.name.split(".")[0];
149
+ content += ` ${stepName}: typeof import('../../steps/${entry.name.replace(".ts", ".js")}');\n`;
150
+ }
151
+ }
152
+ }
153
+ content += `}\n`;
154
+ content += `declare const Step: TestSpectraSteps;\n\n`;
155
+
156
+ results[platform] = content;
157
+ }
158
+
159
+ return results;
160
+ }
161
+
162
+ static generateFixturesDeclaration(cwd: string): string {
163
+ const fixturesDir = path.join(cwd, "fixtures");
164
+ let content = `// Auto-generated ambient fixture declarations for TestSpectra\n`;
165
+ content += `// Do not edit manually. Run 'spectra watch' or let the Language Service manage this.\n\n`;
166
+ content += `interface TestSpectraFixtures {\n`;
167
+ if (fs.existsSync(fixturesDir)) {
168
+ const fixtureFiles = fs.readdirSync(fixturesDir);
169
+ for (const file of fixtureFiles) {
170
+ if (file.startsWith(".")) continue;
171
+ const parsed = path.parse(file);
172
+ const varName = parsed.name.replace(/[^a-zA-Z0-9_$]/g, "_");
173
+ content += ` readonly ${varName}: string;\n`;
174
+ }
175
+ }
176
+ content += `}\n`;
177
+ content += `declare const Fixture: TestSpectraFixtures;\n`;
178
+ return content;
179
+ }
180
+
181
+ static writeDeclarationFiles(cwd: string): void {
182
+ const typesDir = path.join(cwd, ".testspectra", "types");
183
+ if (!fs.existsSync(typesDir)) {
184
+ fs.mkdirSync(typesDir, { recursive: true });
185
+ }
186
+
187
+ // 1. Write single platform-agnostic fixtures.d.ts
188
+ const fixturesContent = this.generateFixturesDeclaration(cwd);
189
+ fs.writeFileSync(path.join(typesDir, "fixtures.d.ts"), fixturesContent, "utf-8");
190
+
191
+ // 2. Write per-platform ambient types
192
+ const declarations = this.generateAmbientDeclarations(cwd);
193
+ for (const [platform, content] of Object.entries(declarations)) {
194
+ const filePath = path.join(typesDir, `${platform}.d.ts`);
195
+ fs.writeFileSync(filePath, content, "utf-8");
196
+ }
197
+
198
+ // Also write a universal ambient index
199
+ const universalPath = path.join(typesDir, "index.d.ts");
200
+ fs.writeFileSync(universalPath, `/// <reference path="./common.d.ts" />\n`, "utf-8");
201
+ }
202
+ }
@@ -0,0 +1,46 @@
1
+ declare global {
2
+ namespace WebdriverIO {
3
+ export interface InterceptFixtureOptions {
4
+ statusCode?: number;
5
+ headers?: Record<string, string>;
6
+ }
7
+
8
+ export interface InterceptFixtureObject<TData = unknown> {
9
+ statusCode?: number;
10
+ body: TData;
11
+ headers?: Record<string, string>;
12
+ }
13
+
14
+ export type InterceptInput<TData = unknown> =
15
+ | InterceptFixtureObject<TData>
16
+ | TData
17
+ | string;
18
+
19
+ interface Mock {
20
+ /**
21
+ * Dynamically set or change the fixture response for this mock instance.
22
+ * Supports Fixture paths, `{ statusCode, body, headers }` objects, or raw payloads.
23
+ */
24
+ respondWith<TData = unknown>(
25
+ fixture: InterceptInput<TData>,
26
+ options?: InterceptFixtureOptions,
27
+ ): Promise<void>;
28
+ }
29
+
30
+ interface Browser {
31
+ /**
32
+ * Clean & strongly-typed API interceptor for WebdriverIO.
33
+ * Automatically resolves relative path against browser.options.baseUrl.
34
+ * Integrated directly with TestSpectra Fixtures.
35
+ */
36
+ intercept<TData = unknown>(
37
+ path: string,
38
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
39
+ fixture?: InterceptInput<TData>,
40
+ options?: InterceptFixtureOptions,
41
+ ): Promise<Mock>;
42
+ }
43
+ }
44
+ }
45
+
46
+ export {};
Binary file
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }