@testspectra/cli 1.0.17 → 1.0.19

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/README.md CHANGED
@@ -161,14 +161,14 @@ export default defineConfig({
161
161
 
162
162
  | Command | Description |
163
163
  | :--- | :--- |
164
- | `spectra init` | Scaffolds a complete multi-platform project from scratch with tsconfigs, specs, page objects, actions, steps, and fixtures. |
165
- | `spectra run` | Generates declaration types and invokes the native Rust test runner to execute the test suite. |
164
+ | `spectra init` / `npx @testspectra/cli init` | Interactive project setup. Auto-detects standalone or Nx Monorepos, discovers workspace packages from `pnpm-workspace.yaml`, and scaffolds centralized/distributed configurations. |
165
+ | `spectra run [spec]` | Automatically updates ambient declarations and invokes the native Rust test runner to execute the test suite. |
166
166
  | `spectra doctor` | Verifies local environment prerequisites (ADB, Java, Chrome, Bun, Node). |
167
167
  | `spectra devices` | Lists connected Android/iOS devices and local browsers. |
168
168
 
169
169
  ---
170
170
 
171
- ## 4. Directory Structure of a TestSpectra Project
171
+ ## 4. Standalone Project Directory Structure
172
172
 
173
173
  ```
174
174
  ├── spectra.config.ts # Central typed project configuration
@@ -189,19 +189,17 @@ export default defineConfig({
189
189
  │ └── loginUser/
190
190
  │ ├── web.step.ts
191
191
  │ └── mobile.step.ts
192
- ├── hooks/ # Suite lifecycle hooks
193
- │ └── default/
194
- ├── before.web.hook.ts
195
- ├── before.android.hook.ts
196
- │ └── before.ios.hook.ts
197
- ├── specs/ # Standalone zero-import test cases
198
- └── TC-LOGIN-01/
199
- ├── web.test.ts
200
- │ ├── android.test.ts
201
- │ └── ios.test.ts
192
+ ├── global-hooks/ # Global lifecycle hooks
193
+ │ └── before.web.hook.ts
194
+ ├── specs/ # Layered zero-import test cases
195
+ └── Auth/
196
+ │ └── TC-AUTH-01/
197
+ ├── web.test.ts
198
+ ├── android.test.ts
199
+ └── ios.test.ts
202
200
  ├── fixtures/ # Platform-agnostic test data
203
201
  │ └── userData.json
204
- └── .testspectra/ # Auto-generated ambient typings
202
+ └── .testspectra/ # Auto-generated ambient typings & cache
205
203
  └── types/
206
204
  ├── fixtures.d.ts
207
205
  ├── web.d.ts
@@ -213,15 +211,83 @@ export default defineConfig({
213
211
 
214
212
  ---
215
213
 
216
- ## 5. Development & Verification
214
+ ## 5. Enterprise Monorepo Architecture (Nx & PNPM Workspace)
215
+
216
+ TestSpectra features an enterprise-grade architecture for large monorepos: **"Centralized Configuration, Distributed Implementation"**.
217
+
218
+ ```
219
+ ┌───────────────────────────────────────────────────────────┐
220
+ │ Root Monorepo Directory │
221
+ │ │
222
+ │ ├── spectra.config.ts (Central Execution Config) │
223
+ │ ├── .testspectra/ (Central Cache, AppData, Log)│
224
+ │ ├── pnpm-workspace.yaml (Workspace Definition) │
225
+ │ ├── nx.json (Nx Target & Caching Rules) │
226
+ │ └── tsconfig.json (Solution References) │
227
+ └─────────────────────────────┬─────────────────────────────┘
228
+
229
+ ┌──────────────────────────────┴──────────────────────────────┐
230
+ ▼ ▼
231
+ ┌───────────────────────────────┐ ┌───────────────────────────────┐
232
+ │ shared/testing/ (Library) │ │ packages/features/auth/e2e/ │
233
+ ├───────────────────────────────┤ ├───────────────────────────────┤
234
+ │ - Shared Page Objects │ ◄─── Auto-Scanned Globals ──│ - Feature Specs (specs/) │
235
+ │ - Shared Steps (loginAsAdmin) │ (100% Zero-Import!) │ - Feature Page Objects (POMs) │
236
+ │ - Shared Actions (dismissBtn) │ │ - project.json (Nx Target) │
237
+ │ - Shared Fixtures (appConfig) │ │ - tsconfig.web/android/ios │
238
+ └───────────────────────────────┘ └───────────────────────────────┘
239
+ ```
240
+
241
+ ### Key Pillars of Monorepo Integration:
242
+
243
+ 1. **Interactive Workspace Auto-Discovery (`spectra init`)**:
244
+ - Parses `pnpm-workspace.yaml` (e.g. `packages/features/*`, `modules/**`).
245
+ - Scans sub-directories for existing `project.json` or `package.json`.
246
+ - Prompts the user with an interactive multi-select checkbox to choose which features will receive E2E testing.
247
+ - Prompts for customizable E2E folder names (e.g. `e2e`, `test/e2e`) and the shared testing library path (`shared/testing`).
248
+
249
+ 2. **Centralized Configuration (`spectra.config.ts`) & App Data (`.testspectra/`)**:
250
+ - `spectra.config.ts` lives exclusively at the root of the monorepo.
251
+ - CLI test runs executed from sub-feature folders automatically traverse up to find the root configuration.
252
+ - Driver caches, binaries, and logs reside in `<root>/.testspectra/`, preventing repository clutter across individual feature folders.
253
+
254
+ 3. **Global Zero-Import Consumption of Shared Library**:
255
+ - Entities placed inside `shared/testing/page-objects/`, `shared/testing/steps/`, `shared/testing/actions/`, and `shared/testing/fixtures/` are automatically aggregated into ambient declarations.
256
+ - Feature test specs consume shared Page Objects (`NavigationBar.goToHome()`), steps (`Step.loginAsAdmin()`), actions (`Spectra.dismissBanner()`), and fixtures (`Fixture.appConfig`) **without a single line of `import` statement**.
257
+
258
+ 4. **Nx Target Execution & Affected Caching**:
259
+ - Each feature E2E folder includes an Nx `project.json`:
260
+ ```json
261
+ {
262
+ "name": "auth-e2e",
263
+ "targets": {
264
+ "e2e": {
265
+ "executor": "nx:run-commands",
266
+ "options": { "command": "spectra run", "cwd": "modules/auth/e2e" },
267
+ "configurations": {
268
+ "android": { "command": "spectra run --target android" },
269
+ "ios": { "command": "spectra run --target ios" },
270
+ "headless": { "command": "spectra run --headless" }
271
+ }
272
+ }
273
+ }
274
+ }
275
+ ```
276
+ - Running `nx run-many -t e2e` executes all feature test suites in parallel.
277
+ 5. **Automatic Architecture Documentation (`ARCHITECTURE.md`)**:
278
+ - Every `spectra init` run produces a comprehensive, project-tailored `ARCHITECTURE.md` file in the root workspace.
279
+ - It documents the generated file map, exact distributed feature paths, shared library resolution, and zero-import mechanics for onboarding engineers.
280
+
281
+ ---
282
+
283
+ ## 6. Development & Verification
217
284
 
218
285
  To verify the CLI and example app:
219
286
  ```bash
220
287
  # Build CLI
221
288
  pnpm --filter @testspectra/cli build
222
289
 
223
- # Initialize and verify example app
224
- cd apps/example-app
225
- ../../cli/bin/spectra.js init
226
- npx tsc -b tsconfig.json
290
+ # Test interactive scaffolding
291
+ npx @testspectra/cli init
292
+ pnpm type-check
227
293
  ```
@@ -4,41 +4,152 @@ import { fileURLToPath } from "url";
4
4
  import { ConfigLoader } from "../config/loader.js";
5
5
  import { TypeGenerator } from "../types/generator.js";
6
6
  import inquirer from "inquirer";
7
+ function parsePnpmWorkspaceGlobs(content) {
8
+ const lines = content.split("\n");
9
+ const globs = [];
10
+ let inPackages = false;
11
+ for (const line of lines) {
12
+ const trimmed = line.trim();
13
+ if (trimmed.startsWith("packages:")) {
14
+ inPackages = true;
15
+ continue;
16
+ }
17
+ if (inPackages) {
18
+ if (trimmed.startsWith("-")) {
19
+ const item = trimmed.replace(/^-\s*['"]?/, "").replace(/['"]?\s*$/, "");
20
+ if (item)
21
+ globs.push(item);
22
+ }
23
+ else if (trimmed && !trimmed.startsWith("#")) {
24
+ break;
25
+ }
26
+ }
27
+ }
28
+ return globs;
29
+ }
30
+ function scanDirectoriesForProjects(cwd, patterns) {
31
+ const foundProjects = [];
32
+ const visited = new Set();
33
+ function search(dir, currentDepth, maxDepth) {
34
+ if (currentDepth > maxDepth || !fs.existsSync(dir))
35
+ return;
36
+ const base = path.basename(dir);
37
+ if (base === "node_modules" || base === "dist" || base === ".testspectra" || base === ".git" || base === "shared") {
38
+ return;
39
+ }
40
+ const hasPkg = fs.existsSync(path.join(dir, "package.json"));
41
+ const hasNx = fs.existsSync(path.join(dir, "project.json"));
42
+ if ((hasPkg || hasNx) && dir !== cwd) {
43
+ const rel = path.relative(cwd, dir);
44
+ if (!visited.has(rel)) {
45
+ visited.add(rel);
46
+ let name = path.basename(dir);
47
+ if (hasPkg) {
48
+ try {
49
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8"));
50
+ if (pkg.name)
51
+ name = pkg.name;
52
+ }
53
+ catch { }
54
+ }
55
+ else if (hasNx) {
56
+ try {
57
+ const nxJson = JSON.parse(fs.readFileSync(path.join(dir, "project.json"), "utf-8"));
58
+ if (nxJson.name)
59
+ name = nxJson.name;
60
+ }
61
+ catch { }
62
+ }
63
+ foundProjects.push({
64
+ name,
65
+ relPath: rel,
66
+ absPath: dir,
67
+ hasNxProject: hasNx,
68
+ });
69
+ }
70
+ return; // don't recurse into nested sub-packages unless needed
71
+ }
72
+ try {
73
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
74
+ for (const entry of entries) {
75
+ if (entry.isDirectory()) {
76
+ search(path.join(dir, entry.name), currentDepth + 1, maxDepth);
77
+ }
78
+ }
79
+ }
80
+ catch { }
81
+ }
82
+ // If patterns exist from pnpm-workspace.yaml, search those roots
83
+ if (patterns.length > 0) {
84
+ for (const pat of patterns) {
85
+ const cleanPat = pat.replace(/\/\*\*?$/, "").replace(/\/\*$/, "");
86
+ const searchRoot = path.join(cwd, cleanPat);
87
+ if (fs.existsSync(searchRoot)) {
88
+ const stat = fs.statSync(searchRoot);
89
+ if (stat.isDirectory()) {
90
+ const entries = fs.readdirSync(searchRoot, { withFileTypes: true });
91
+ for (const e of entries) {
92
+ if (e.isDirectory()) {
93
+ search(path.join(searchRoot, e.name), 1, 3);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ // Fallback search in standard directories if nothing found
101
+ if (foundProjects.length === 0) {
102
+ for (const fallback of ["modules", "packages", "apps", "libs", "features"]) {
103
+ const fbPath = path.join(cwd, fallback);
104
+ if (fs.existsSync(fbPath)) {
105
+ search(fbPath, 1, 3);
106
+ }
107
+ }
108
+ }
109
+ return foundProjects;
110
+ }
7
111
  export async function initCommand(options = {}) {
8
112
  const cwd = process.cwd();
9
113
  const existingConfig = ConfigLoader.findConfigFile(cwd);
10
- if (existingConfig && !options.force) {
114
+ if (existingConfig && existingConfig === path.join(cwd, path.basename(existingConfig)) && !options.force) {
11
115
  console.log(`\x1b[33m[TestSpectra]\x1b[0m Config already exists at ${path.basename(existingConfig)}. Use --force to overwrite.`);
12
116
  return;
13
117
  }
14
- // Interactive Prompt if template is not specified
15
- let selectedTemplate = options.template;
16
- if (!selectedTemplate) {
17
- // Detect if we are inside an Nx workspace
18
- let isNx = false;
19
- let checkNx = cwd;
20
- while (checkNx !== path.dirname(checkNx)) {
21
- if (fs.existsSync(path.join(checkNx, "nx.json"))) {
22
- isNx = true;
23
- break;
24
- }
25
- checkNx = path.dirname(checkNx);
118
+ // Detect monorepo environment
119
+ let isMonorepo = false;
120
+ let pnpmPatterns = [];
121
+ const pnpmWorkspacePath = path.join(cwd, "pnpm-workspace.yaml");
122
+ const nxJsonPath = path.join(cwd, "nx.json");
123
+ if (fs.existsSync(pnpmWorkspacePath)) {
124
+ isMonorepo = true;
125
+ try {
126
+ const content = fs.readFileSync(pnpmWorkspacePath, "utf-8");
127
+ pnpmPatterns = parsePnpmWorkspaceGlobs(content);
26
128
  }
129
+ catch { }
130
+ }
131
+ else if (fs.existsSync(nxJsonPath)) {
132
+ isMonorepo = true;
133
+ }
134
+ const detectedProjects = isMonorepo ? scanDirectoriesForProjects(cwd, pnpmPatterns) : [];
135
+ let selectedTemplate = options.template;
136
+ if (!selectedTemplate && !options.force) {
137
+ const choices = [
138
+ { name: "Standard (Standalone or single project)", value: "default" },
139
+ { name: "Nx Monorepo (Interactive multi-module feature discovery & centralized config)", value: "nx" },
140
+ ];
27
141
  const answers = await inquirer.prompt([
28
142
  {
29
143
  type: "list",
30
144
  name: "template",
31
- message: "Select TestSpectra project template:",
32
- choices: [
33
- { name: "Standard (Standalone or standard monorepo)", value: "default" },
34
- { name: "Nx Monorepo (Includes project.json with Nx targets & affected support)", value: "nx" },
35
- ],
36
- default: isNx ? "nx" : "default",
145
+ message: "Select TestSpectra setup mode:",
146
+ choices,
147
+ default: isMonorepo ? "nx" : "default",
37
148
  },
38
149
  ]);
39
150
  selectedTemplate = answers.template;
40
151
  }
41
- const templateName = selectedTemplate || "default";
152
+ const templateName = selectedTemplate || (isMonorepo ? "nx" : "default");
42
153
  // 1. Resolve template directory
43
154
  const __filename = fileURLToPath(import.meta.url);
44
155
  const __dirname = path.dirname(__filename);
@@ -52,8 +163,8 @@ export async function initCommand(options = {}) {
52
163
  if (!fs.existsSync(templateDir)) {
53
164
  throw new Error(`[TestSpectra] Scaffold template directory not found at: ${templateDir}`);
54
165
  }
55
- // 2. Dynamically resolve CLI version
56
- let cliVersion = "^1.0.6";
166
+ // 2. Resolve CLI dependency version
167
+ let cliVersion = "^1.0.17";
57
168
  try {
58
169
  const cliPackageJsonPath = path.resolve(__dirname, "../../package.json");
59
170
  if (fs.existsSync(cliPackageJsonPath)) {
@@ -64,7 +175,6 @@ export async function initCommand(options = {}) {
64
175
  }
65
176
  }
66
177
  catch { }
67
- // Check if cwd is inside the testspectra monorepo with a pnpm-workspace.yaml
68
178
  let isInternalWorkspace = false;
69
179
  let cur = cwd;
70
180
  while (cur !== path.dirname(cur)) {
@@ -80,10 +190,10 @@ export async function initCommand(options = {}) {
80
190
  }
81
191
  cur = path.dirname(cur);
82
192
  }
83
- const cliDepVersion = isInternalWorkspace ? "workspace:*" : cliVersion;
84
193
  const projectName = path.basename(cwd);
85
- // 3. Recursive copy function with variable replacement
86
- function copyRecursive(src, dest) {
194
+ const cliDepVersion = isInternalWorkspace ? "workspace:*" : cliVersion;
195
+ // Helper: Copy directory contents recursively
196
+ function copyRecursive(src, dest, replacements = {}) {
87
197
  const base = path.basename(src);
88
198
  if (base === "node_modules" || base === "dist" || base === ".testspectra" || base === ".git") {
89
199
  return;
@@ -95,41 +205,248 @@ export async function initCommand(options = {}) {
95
205
  }
96
206
  const entries = fs.readdirSync(src);
97
207
  for (const entry of entries) {
98
- copyRecursive(path.join(src, entry), path.join(dest, entry));
208
+ copyRecursive(path.join(src, entry), path.join(dest, entry), replacements);
99
209
  }
100
210
  }
101
211
  else {
102
- // If destination exists and force is not set, skip
103
212
  if (fs.existsSync(dest) && !options.force) {
104
213
  return;
105
214
  }
106
215
  let content = fs.readFileSync(src, "utf-8");
107
- if (src.endsWith("package.json")) {
216
+ for (const [k, v] of Object.entries(replacements)) {
217
+ content = content.replaceAll(k, v);
218
+ }
219
+ if (dest.endsWith("package.json")) {
108
220
  try {
109
- const pkgObj = JSON.parse(content);
110
- if (src === path.join(templateDir, "package.json")) {
111
- pkgObj.name = projectName;
221
+ const pkg = JSON.parse(content);
222
+ if (pkg.devDependencies && pkg.devDependencies["@testspectra/cli"]) {
223
+ pkg.devDependencies["@testspectra/cli"] = cliDepVersion;
112
224
  }
113
- if (pkgObj.devDependencies && pkgObj.devDependencies["@testspectra/cli"]) {
114
- pkgObj.devDependencies["@testspectra/cli"] = cliDepVersion;
225
+ if (pkg.dependencies && pkg.dependencies["@testspectra/cli"]) {
226
+ pkg.dependencies["@testspectra/cli"] = cliDepVersion;
115
227
  }
116
- if (pkgObj.dependencies && pkgObj.dependencies["@testspectra/cli"]) {
117
- pkgObj.dependencies["@testspectra/cli"] = cliDepVersion;
118
- }
119
- content = JSON.stringify(pkgObj, null, 2);
228
+ content = JSON.stringify(pkg, null, 2);
120
229
  }
121
230
  catch { }
122
231
  }
123
- else if (src.endsWith("project.json")) {
124
- content = content
125
- .replace(/\{\{PROJECT_NAME\}\}/g, projectName)
126
- .replace(/\{\{PROJECT_DIR\}\}/g, path.relative(cur, cwd) || ".");
127
- }
128
232
  fs.writeFileSync(dest, content, "utf-8");
129
233
  }
130
234
  }
235
+ // --- MONOREPO INTERACTIVE FLOW ---
236
+ if (templateName === "nx" && (detectedProjects.length > 0 || isMonorepo)) {
237
+ console.log(`\n\x1b[36m[TestSpectra Monorepo Discovery]\x1b[0m`);
238
+ console.log(`Found ${detectedProjects.length} workspace feature project(s).`);
239
+ let chosenProjects = [];
240
+ if (detectedProjects.length > 0) {
241
+ const projectAnswers = await inquirer.prompt([
242
+ {
243
+ type: "checkbox",
244
+ name: "selectedProjects",
245
+ message: "Select workspace features where TestSpectra should be initialized:",
246
+ choices: detectedProjects.map((p) => ({
247
+ name: `${p.relPath} (${p.name})${p.hasNxProject ? " [Nx project.json]" : ""}`,
248
+ value: p.relPath,
249
+ checked: true,
250
+ })),
251
+ },
252
+ ]);
253
+ chosenProjects = projectAnswers.selectedProjects;
254
+ }
255
+ const folderAnswers = await inquirer.prompt([
256
+ {
257
+ type: "input",
258
+ name: "e2eFolderName",
259
+ message: "Enter E2E test folder name for each selected feature:",
260
+ default: "e2e",
261
+ },
262
+ {
263
+ type: "input",
264
+ name: "sharedTestingPath",
265
+ message: "Enter path for Shared Testing library (POMs, Steps, Actions, Fixtures):",
266
+ default: "shared/testing",
267
+ },
268
+ ]);
269
+ const e2eFolderName = folderAnswers.e2eFolderName.trim() || "e2e";
270
+ const sharedTestingRelPath = folderAnswers.sharedTestingPath.trim() || "shared/testing";
271
+ const sharedTestingAbsPath = path.join(cwd, sharedTestingRelPath);
272
+ // 1. Centralized Root Config
273
+ const rootConfigSrc = path.join(templateDir, "spectra.config.ts");
274
+ const rootConfigDest = path.join(cwd, "spectra.config.ts");
275
+ if (fs.existsSync(rootConfigSrc) && (!fs.existsSync(rootConfigDest) || options.force)) {
276
+ fs.copyFileSync(rootConfigSrc, rootConfigDest);
277
+ console.log(`\x1b[32m[TestSpectra]\x1b[0m Created centralized configuration at ./spectra.config.ts`);
278
+ }
279
+ // 2. Centralized Root .testspectra folder for cache/logs
280
+ const rootTestDataDir = path.join(cwd, ".testspectra");
281
+ if (!fs.existsSync(rootTestDataDir)) {
282
+ fs.mkdirSync(rootTestDataDir, { recursive: true });
283
+ }
284
+ // 3. Centralized Shared Testing Library
285
+ const sharedTestingSrc = path.join(templateDir, "shared/testing");
286
+ if (fs.existsSync(sharedTestingSrc)) {
287
+ copyRecursive(sharedTestingSrc, sharedTestingAbsPath);
288
+ console.log(`\x1b[32m[TestSpectra]\x1b[0m Initialized shared testing library at ./${sharedTestingRelPath}`);
289
+ }
290
+ // 4. Distribute E2E Folders to Selected Feature Modules
291
+ const sampleE2eSrc = path.join(templateDir, "modules/auth/e2e");
292
+ const featureE2eDirs = [];
293
+ for (const projRel of chosenProjects) {
294
+ const targetE2eDir = path.join(cwd, projRel, e2eFolderName);
295
+ const featureName = path.basename(projRel);
296
+ const e2eProjectName = `${featureName}-${e2eFolderName}`;
297
+ copyRecursive(sampleE2eSrc, targetE2eDir, {
298
+ "auth-e2e": e2eProjectName,
299
+ "modules/auth/e2e": path.relative(cwd, targetE2eDir).replace(/\\/g, "/"),
300
+ "scope:auth": `scope:${featureName}`,
301
+ });
302
+ // Adjust project.json
303
+ const projJsonPath = path.join(targetE2eDir, "project.json");
304
+ if (fs.existsSync(projJsonPath)) {
305
+ try {
306
+ const pObj = JSON.parse(fs.readFileSync(projJsonPath, "utf-8"));
307
+ pObj.name = e2eProjectName;
308
+ pObj.sourceRoot = path.relative(cwd, targetE2eDir).replace(/\\/g, "/");
309
+ pObj.targets.e2e.options.cwd = path.relative(cwd, targetE2eDir).replace(/\\/g, "/");
310
+ pObj.targets["type-check"].options.cwd = path.relative(cwd, targetE2eDir).replace(/\\/g, "/");
311
+ pObj.tags = [`scope:${featureName}`, "type:e2e"];
312
+ fs.writeFileSync(projJsonPath, JSON.stringify(pObj, null, 2), "utf-8");
313
+ }
314
+ catch { }
315
+ }
316
+ // Ensure no local spectra.config.ts (centralized in root)
317
+ const localConfig = path.join(targetE2eDir, "spectra.config.ts");
318
+ if (fs.existsSync(localConfig))
319
+ fs.unlinkSync(localConfig);
320
+ featureE2eDirs.push(targetE2eDir);
321
+ console.log(`\x1b[32m[TestSpectra]\x1b[0m Initialized ${e2eProjectName} in ./${path.relative(cwd, targetE2eDir)}`);
322
+ }
323
+ // 5. Update Root Solution tsconfig.json references
324
+ const rootTsConfigPath = path.join(cwd, "tsconfig.json");
325
+ const references = [];
326
+ for (const e2eDir of featureE2eDirs) {
327
+ references.push({ path: `./${path.relative(cwd, e2eDir).replace(/\\/g, "/")}` });
328
+ }
329
+ references.push({ path: `./${sharedTestingRelPath.replace(/\\/g, "/")}` });
330
+ let rootTsConfig = { files: [], references: [] };
331
+ if (fs.existsSync(rootTsConfigPath)) {
332
+ try {
333
+ rootTsConfig = JSON.parse(fs.readFileSync(rootTsConfigPath, "utf-8"));
334
+ if (!rootTsConfig.references)
335
+ rootTsConfig.references = [];
336
+ for (const ref of references) {
337
+ if (!rootTsConfig.references.some((r) => r.path === ref.path)) {
338
+ rootTsConfig.references.push(ref);
339
+ }
340
+ }
341
+ }
342
+ catch { }
343
+ }
344
+ else {
345
+ rootTsConfig = { files: [], references };
346
+ }
347
+ // 6. Generate Ambient Types for Shared + All E2E Feature Directories
348
+ TypeGenerator.writeDeclarationFiles(cwd);
349
+ for (const e2eDir of featureE2eDirs) {
350
+ TypeGenerator.writeDeclarationFiles(e2eDir);
351
+ }
352
+ // 7. Generate ARCHITECTURE.md in root documenting the exact generated layout
353
+ const archDocContent = `# TestSpectra Enterprise Monorepo Architecture
354
+
355
+ This workspace uses TestSpectra's **"Centralized Configuration, Distributed Implementation"** model for automated cross-platform testing.
356
+
357
+ ---
358
+
359
+ ## 1. Directory & File Overview
360
+
361
+ ### Centralized Root Elements
362
+ - \`./spectra.config.ts\`: Single source of truth for runtime configurations (Base URL, Appium devices, browser targets, timeouts).
363
+ - \`./.testspectra/\`: Centralized app data folder containing driver caches (ChromeDriver, GeckoDriver, Appium), execution logs, and universal ambient types (\`.testspectra/types/\`).
364
+ - \`./nx.json\`: Nx target defaults with execution caching and inputs.
365
+ - \`./pnpm-workspace.yaml\`: Monorepo package glob definitions.
366
+ - \`./tsconfig.json\`: Solution-style TypeScript orchestrator linking all distributed E2E projects and shared libraries.
367
+
368
+ ### Centralized Shared Library (\`./${sharedTestingRelPath}\`)
369
+ Provides cross-feature reusable test entities with **100% zero-import global resolution**:
370
+ - \`page-objects/\`: Shared Page Object models (e.g. NavigationBar, AppHeader).
371
+ - \`steps/\`: Cross-feature business step flows (e.g. \`Step.loginAsAdmin()\`).
372
+ - \`actions/\`: Custom atomic actions (e.g. \`Spectra.dismissBanner()\`).
373
+ - \`fixtures/\`: Common test data and environment configurations (e.g. \`Fixture.appConfig\`).
374
+
375
+ ### Distributed Feature E2E Modules (${featureE2eDirs.length} Features)
376
+ ${featureE2eDirs.map((d) => {
377
+ const rel = path.relative(cwd, d);
378
+ const name = path.basename(path.dirname(d));
379
+ return `- \`./${rel}\` (**${name}**):
380
+ - \`specs/\`: Feature test cases (\`specs/<SuiteName>/<CaseId>/\` for web, android, ios).
381
+ - \`page-objects/\`: Feature-specific Page Objects (e.g. LoginPage, ProductPage).
382
+ - \`project.json\`: Nx targets (\`e2e\`, \`type-check\`) supporting configurations (\`--configuration=android\`, \`--configuration=ios\`, \`--configuration=headless\`).
383
+ - \`tsconfig.json\`: Solution TypeScript config reference.`;
384
+ }).join("\n")}
385
+
386
+ ---
387
+
388
+ ## 2. Zero-Import Consumption Flow
389
+
390
+ \`\`\`
391
+ ┌──────────────────────────────┐ ┌──────────────────────────────┐
392
+ │ shared/testing/page-objects │ │ modules/*/e2e/specs/ │
393
+ │ shared/testing/steps │ ───────► │ (Zero-Import Test Cases) │
394
+ │ shared/testing/actions │ │ - NavigationBar.goToHome() │
395
+ │ shared/testing/fixtures │ │ - Step.loginAsAdmin() │
396
+ └──────────────────────────────┘ │ - Spectra.dismissBanner() │
397
+ └──────────────────────────────┘
398
+ \`\`\`
399
+
400
+ ---
401
+
402
+ ## 3. Running Tests via Nx
403
+
404
+ \`\`\`bash
405
+ # Run all feature E2E suites in parallel
406
+ pnpm nx run-many -t e2e
407
+
408
+ # Run only E2E tests for features changed in PR
409
+ pnpm nx affected -t e2e
410
+
411
+ # Run E2E for a specific feature on Android
412
+ pnpm nx run auth-${e2eFolderName}:e2e --configuration=android
413
+
414
+ # Run type check across all E2E projects
415
+ pnpm nx run-many -t type-check
416
+ # Or standard TypeScript compilation
417
+ pnpm type-check
418
+ \`\`\`
419
+ `;
420
+ fs.writeFileSync(path.join(cwd, "ARCHITECTURE.md"), archDocContent, "utf-8");
421
+ // 8. Update root README.md
422
+ const rootReadmePath = path.join(cwd, "README.md");
423
+ const readmeContent = `# ${projectName} (TestSpectra Monorepo)
424
+
425
+ Automated cross-platform E2E testing powered by [TestSpectra](https://github.com/testspectra).
426
+
427
+ ## Architecture & Layout
428
+
429
+ This project follows TestSpectra's **Centralized Configuration, Distributed Implementation** architecture.
430
+ Detailed architecture, file locations, and zero-import mechanics are documented in:
431
+ 👉 **[ARCHITECTURE.md](./ARCHITECTURE.md)**
432
+
433
+ ## Commands
434
+
435
+ - **Run all E2E tests**: \`pnpm nx run-many -t e2e\`
436
+ - **Run affected E2E tests**: \`pnpm nx affected -t e2e\`
437
+ - **Type-check all suites**: \`pnpm type-check\`
438
+ `;
439
+ fs.writeFileSync(rootReadmePath, readmeContent, "utf-8");
440
+ console.log(`\n\x1b[32m[TestSpectra]\x1b[0m Enterprise Nx Monorepo initialized successfully!`);
441
+ console.log(`\x1b[36m- Centralized Configuration:\x1b[0m ./spectra.config.ts`);
442
+ console.log(`\x1b[36m- Centralized App Data/Cache:\x1b[0m ./.testspectra/`);
443
+ console.log(`\x1b[36m- Shared Testing Library:\x1b[0m ./${sharedTestingRelPath}`);
444
+ console.log(`\x1b[36m- Architecture Documentation:\x1b[0m ./ARCHITECTURE.md`);
445
+ console.log(`\x1b[36m- Initialized Features (${featureE2eDirs.length}):\x1b[0m\n ${featureE2eDirs.map((d) => path.relative(cwd, d)).join("\n ")}`);
446
+ return;
447
+ }
448
+ // --- STANDALONE / DEFAULT PROJECT FLOW ---
131
449
  copyRecursive(templateDir, cwd);
132
- // 4. Pre-configure pnpm allowBuilds for standalone projects
133
450
  if (!isInternalWorkspace) {
134
451
  const pnpmWorkspacePath = path.join(cwd, "pnpm-workspace.yaml");
135
452
  if (!fs.existsSync(pnpmWorkspacePath) || options.force) {
@@ -137,21 +454,50 @@ export async function initCommand(options = {}) {
137
454
  fs.writeFileSync(pnpmWorkspacePath, pnpmWsContent, "utf-8");
138
455
  }
139
456
  }
140
- // 5. Generate ambient declaration files
141
457
  TypeGenerator.writeDeclarationFiles(cwd);
142
- // If subprojects exist (e.g. in Nx monorepo template), generate types for them as well
143
- const modulesDir = path.join(cwd, "modules");
144
- if (fs.existsSync(modulesDir)) {
145
- const mods = fs.readdirSync(modulesDir, { withFileTypes: true });
146
- for (const m of mods) {
147
- if (m.isDirectory()) {
148
- const e2eDir = path.join(modulesDir, m.name, "e2e");
149
- if (fs.existsSync(e2eDir)) {
150
- TypeGenerator.writeDeclarationFiles(e2eDir);
151
- }
152
- }
153
- }
154
- }
458
+ // Standalone Architecture Documentation
459
+ const standaloneArchDoc = `# TestSpectra Project Architecture
460
+
461
+ This project is built with the TestSpectra testing framework.
462
+
463
+ ## Structure & File Map
464
+ - \`./spectra.config.ts\`: Typed runner configuration.
465
+ - \`./specs/\`: Layered test cases (\`specs/<Suite>/<CaseId>/\` with \`web.test.ts\`, \`android.test.ts\`, \`ios.test.ts\`).
466
+ - \`./page-objects/\`: Page Object models per platform.
467
+ - \`./steps/\`: Business flows accessible globally via \`Step.*\`.
468
+ - \`./actions/\`: Atomic actions accessible globally via \`Spectra.*\`.
469
+ - \`./fixtures/\`: JSON fixtures accessible globally via \`Fixture.*\`.
470
+ - \`./global-hooks/\`: Global suite lifecycle hooks.
471
+ - \`./.testspectra/\`: Auto-generated ambient typings and local runtime cache.
472
+
473
+ ## Execution
474
+ \`\`\`bash
475
+ # Run web test suite
476
+ pnpm test
477
+
478
+ # Run Android / iOS
479
+ pnpm spectra run --target android
480
+ pnpm spectra run --target ios
481
+
482
+ # Type check
483
+ pnpm type-check
484
+ \`\`\`
485
+ `;
486
+ fs.writeFileSync(path.join(cwd, "ARCHITECTURE.md"), standaloneArchDoc, "utf-8");
487
+ const standaloneReadme = `# ${projectName}
488
+
489
+ TestSpectra automated testing project.
490
+
491
+ ## Architecture & Layout
492
+ Detailed architecture and folder structure are documented in:
493
+ 👉 **[ARCHITECTURE.md](./ARCHITECTURE.md)**
494
+
495
+ ## Commands
496
+ - **Run tests**: \`pnpm test\`
497
+ - **Type-check**: \`pnpm type-check\`
498
+ `;
499
+ fs.writeFileSync(path.join(cwd, "README.md"), standaloneReadme, "utf-8");
155
500
  console.log(`\x1b[32m[TestSpectra]\x1b[0m Initialized project from template successfully!`);
501
+ console.log(`\x1b[32m[TestSpectra]\x1b[0m Generated documentation at ./ARCHITECTURE.md`);
156
502
  console.log(`\x1b[32m[TestSpectra]\x1b[0m Generated ambient multi-platform types in .testspectra/types/`);
157
503
  }
@@ -13,7 +13,17 @@ export async function runCommand(specPath, options = {}) {
13
13
  config.webConfig.headlessMode = options.headless;
14
14
  }
15
15
  const platform = options.target || "web";
16
- const appDataPath = options.workdir ? path.resolve(options.workdir) : path.join(cwd, ".testspectra");
16
+ // Find workspace root for centralized .testspectra
17
+ let workspaceRoot = cwd;
18
+ let cur = cwd;
19
+ while (cur !== path.dirname(cur)) {
20
+ if (fs.existsSync(path.join(cur, "pnpm-workspace.yaml")) || fs.existsSync(path.join(cur, "nx.json")) || fs.existsSync(path.join(cur, "spectra.config.ts")) || fs.existsSync(path.join(cur, "testspectra.config.ts"))) {
21
+ workspaceRoot = cur;
22
+ break;
23
+ }
24
+ cur = path.dirname(cur);
25
+ }
26
+ const appDataPath = options.workdir ? path.resolve(options.workdir) : path.join(workspaceRoot, ".testspectra");
17
27
  if (!fs.existsSync(appDataPath)) {
18
28
  fs.mkdirSync(appDataPath, { recursive: true });
19
29
  }
@@ -11,11 +11,18 @@ export class ConfigLoader {
11
11
  ".testspectrarc.json",
12
12
  ];
13
13
  static findConfigFile(cwd = process.cwd()) {
14
- for (const filename of this.CONFIG_FILE_NAMES) {
15
- const fullPath = path.join(cwd, filename);
16
- if (fs.existsSync(fullPath)) {
17
- return fullPath;
14
+ let cur = path.resolve(cwd);
15
+ while (true) {
16
+ for (const filename of this.CONFIG_FILE_NAMES) {
17
+ const fullPath = path.join(cur, filename);
18
+ if (fs.existsSync(fullPath)) {
19
+ return fullPath;
20
+ }
18
21
  }
22
+ const parent = path.dirname(cur);
23
+ if (parent === cur)
24
+ break;
25
+ cur = parent;
19
26
  }
20
27
  return null;
21
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testspectra/cli",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "TestSpectra Zero-Config Cross-Platform Test Runner CLI",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,61 +0,0 @@
1
- import { defineConfig } from "@testspectra/cli";
2
-
3
- export default defineConfig({
4
- webConfig: {
5
- baseUrl: "https://the-internet.herokuapp.com",
6
- maxConcurrentSessions: "1",
7
- headlessMode: true,
8
- implicitWait: "5000",
9
- pageLoadTimeout: "30000",
10
- scriptTimeout: "30000",
11
- parallelizationMode: "testcase",
12
- },
13
- browsers: [
14
- {
15
- id: "chrome-desktop",
16
- type: "chrome",
17
- mobileEmulation: false,
18
- },
19
- ],
20
- androidConfig: {
21
- appiumServer: "http://127.0.0.1:4723",
22
- platformName: "Android",
23
- platformVersion: "13",
24
- deviceName: "emulator-5554",
25
- automationName: "UiAutomator2",
26
- appPackage: "",
27
- appActivity: "",
28
- autoGrantPermissions: true,
29
- noReset: false,
30
- implicitWait: "10000",
31
- parallelizationMode: "suite",
32
- },
33
- iosConfig: {
34
- appiumServer: "http://127.0.0.1:4723",
35
- platformName: "iOS",
36
- platformVersion: "16.0",
37
- deviceName: "iPhone 14",
38
- automationName: "XCUITest",
39
- bundleId: "",
40
- udid: "auto",
41
- xcodeOrgId: "",
42
- xcodeSigningId: "iPhone Developer",
43
- autoAcceptAlerts: true,
44
- noReset: false,
45
- implicitWait: "10000",
46
- parallelizationMode: "suite",
47
- },
48
- loadConfig: {
49
- virtualUsers: "10",
50
- duration: "1m",
51
- },
52
- loadStages: [],
53
- thresholds: [],
54
- executionConfig: {
55
- networkMonitoringEnabled: true,
56
- fastResponseTime: "200",
57
- normalResponseTime: "1000",
58
- monitoredDomains: [],
59
- environmentVariables: [],
60
- },
61
- });