@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 +85 -19
- package/dist/commands/init.js +404 -58
- package/dist/commands/run.js +11 -1
- package/dist/config/loader.js +11 -4
- package/package.json +1 -1
- package/templates/nx/modules/catalog/e2e/spectra.config.ts +0 -61
- /package/templates/nx/{modules/auth/e2e/spectra.config.ts → spectra.config.ts} +0 -0
package/README.md
CHANGED
|
@@ -161,14 +161,14 @@ export default defineConfig({
|
|
|
161
161
|
|
|
162
162
|
| Command | Description |
|
|
163
163
|
| :--- | :--- |
|
|
164
|
-
| `spectra init`
|
|
165
|
-
| `spectra run` |
|
|
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
|
|
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/
|
|
193
|
-
│ └──
|
|
194
|
-
|
|
195
|
-
│
|
|
196
|
-
│ └──
|
|
197
|
-
├──
|
|
198
|
-
│
|
|
199
|
-
│
|
|
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.
|
|
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
|
-
#
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
npx tsc -b tsconfig.json
|
|
290
|
+
# Test interactive scaffolding
|
|
291
|
+
npx @testspectra/cli init
|
|
292
|
+
pnpm type-check
|
|
227
293
|
```
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
//
|
|
15
|
-
let
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
32
|
-
choices
|
|
33
|
-
|
|
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.
|
|
56
|
-
let cliVersion = "^1.0.
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
|
110
|
-
if (
|
|
111
|
-
|
|
221
|
+
const pkg = JSON.parse(content);
|
|
222
|
+
if (pkg.devDependencies && pkg.devDependencies["@testspectra/cli"]) {
|
|
223
|
+
pkg.devDependencies["@testspectra/cli"] = cliDepVersion;
|
|
112
224
|
}
|
|
113
|
-
if (
|
|
114
|
-
|
|
225
|
+
if (pkg.dependencies && pkg.dependencies["@testspectra/cli"]) {
|
|
226
|
+
pkg.dependencies["@testspectra/cli"] = cliDepVersion;
|
|
115
227
|
}
|
|
116
|
-
|
|
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
|
-
//
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/config/loader.js
CHANGED
|
@@ -11,11 +11,18 @@ export class ConfigLoader {
|
|
|
11
11
|
".testspectrarc.json",
|
|
12
12
|
];
|
|
13
13
|
static findConfigFile(cwd = process.cwd()) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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,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
|
-
});
|
|
File without changes
|