@testspectra/cli 1.0.32 → 1.0.34

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 (56) hide show
  1. package/dist/commands/init.js +1 -0
  2. package/dist/types/generator.d.ts +13 -4
  3. package/dist/types/generator.js +130 -100
  4. package/package.json +1 -1
  5. package/templates/nx/shared/testing/steps/loginAsAdmin/common.step.ts +2 -1
  6. package/templates/nx/tsconfig.spectra.android.json +1 -2
  7. package/templates/nx/tsconfig.spectra.ios.json +1 -2
  8. package/templates/nx/tsconfig.spectra.json +1 -0
  9. package/templates/nx/tsconfig.spectra.shared.json +21 -0
  10. package/templates/nx/tsconfig.spectra.web.json +1 -2
  11. package/templates/nx/modules/auth/e2e/actions/applyDiscount/mobile.action.ts +0 -6
  12. package/templates/nx/modules/auth/e2e/actions/applyDiscount/web.action.ts +0 -6
  13. package/templates/nx/modules/auth/e2e/fixtures/couponCode.json +0 -1
  14. package/templates/nx/modules/auth/e2e/fixtures/searchQuery.json +0 -1
  15. package/templates/nx/modules/auth/e2e/page-objects/ProductPage/mobile.ts +0 -39
  16. package/templates/nx/modules/auth/e2e/page-objects/ProductPage/web.ts +0 -38
  17. package/templates/nx/modules/auth/e2e/specs/Product/TC-PROD-01/mobile.test.ts +0 -8
  18. package/templates/nx/modules/auth/e2e/specs/Product/TC-PROD-01/web.test.ts +0 -8
  19. package/templates/nx/modules/auth/e2e/specs/Product/TC-PROD-02/mobile.test.ts +0 -6
  20. package/templates/nx/modules/auth/e2e/specs/Product/TC-PROD-02/web.test.ts +0 -6
  21. package/templates/nx/modules/auth/e2e/specs/Product/hooks/before.web.hook.ts +0 -3
  22. package/templates/nx/modules/auth/e2e/steps/searchProduct/mobile.step.ts +0 -5
  23. package/templates/nx/modules/auth/e2e/steps/searchProduct/web.step.ts +0 -5
  24. package/templates/nx/modules/catalog/e2e/actions/applyDiscount/mobile.action.ts +0 -6
  25. package/templates/nx/modules/catalog/e2e/actions/applyDiscount/web.action.ts +0 -6
  26. package/templates/nx/modules/catalog/e2e/actions/verifyOtp/mobile.action.ts +0 -6
  27. package/templates/nx/modules/catalog/e2e/actions/verifyOtp/web.action.ts +0 -6
  28. package/templates/nx/modules/catalog/e2e/fixtures/couponCode.json +0 -1
  29. package/templates/nx/modules/catalog/e2e/fixtures/searchQuery.json +0 -1
  30. package/templates/nx/modules/catalog/e2e/fixtures/userData.json +0 -1
  31. package/templates/nx/modules/catalog/e2e/global-hooks/before.web.hook.ts +0 -4
  32. package/templates/nx/modules/catalog/e2e/package.json +0 -18
  33. package/templates/nx/modules/catalog/e2e/page-objects/LoginPage/mobile.ts +0 -32
  34. package/templates/nx/modules/catalog/e2e/page-objects/LoginPage/web.ts +0 -32
  35. package/templates/nx/modules/catalog/e2e/page-objects/ProductPage/mobile.ts +0 -39
  36. package/templates/nx/modules/catalog/e2e/page-objects/ProductPage/web.ts +0 -38
  37. package/templates/nx/modules/catalog/e2e/page-objects/SettingsPage/common.ts +0 -26
  38. package/templates/nx/modules/catalog/e2e/project.json +0 -34
  39. package/templates/nx/modules/catalog/e2e/specs/Auth/TC-AUTH-01/android.test.ts +0 -6
  40. package/templates/nx/modules/catalog/e2e/specs/Auth/TC-AUTH-01/ios.test.ts +0 -6
  41. package/templates/nx/modules/catalog/e2e/specs/Auth/TC-AUTH-01/web.test.ts +0 -13
  42. package/templates/nx/modules/catalog/e2e/specs/Auth/TC-AUTH-02/android.test.ts +0 -5
  43. package/templates/nx/modules/catalog/e2e/specs/Auth/TC-AUTH-02/web.test.ts +0 -5
  44. package/templates/nx/modules/catalog/e2e/specs/Auth/hooks/before.android.hook.ts +0 -3
  45. package/templates/nx/modules/catalog/e2e/specs/Auth/hooks/before.ios.hook.ts +0 -3
  46. package/templates/nx/modules/catalog/e2e/specs/Auth/hooks/before.web.hook.ts +0 -3
  47. package/templates/nx/modules/catalog/e2e/specs/Product/TC-PROD-01/mobile.test.ts +0 -8
  48. package/templates/nx/modules/catalog/e2e/specs/Product/TC-PROD-01/web.test.ts +0 -8
  49. package/templates/nx/modules/catalog/e2e/specs/Product/TC-PROD-02/mobile.test.ts +0 -6
  50. package/templates/nx/modules/catalog/e2e/specs/Product/TC-PROD-02/web.test.ts +0 -6
  51. package/templates/nx/modules/catalog/e2e/specs/Product/hooks/before.web.hook.ts +0 -3
  52. package/templates/nx/modules/catalog/e2e/specs/Settings/TC-SET-01/common.test.ts +0 -8
  53. package/templates/nx/modules/catalog/e2e/steps/loginUser/mobile.step.ts +0 -4
  54. package/templates/nx/modules/catalog/e2e/steps/loginUser/web.step.ts +0 -4
  55. package/templates/nx/modules/catalog/e2e/steps/searchProduct/mobile.step.ts +0 -5
  56. package/templates/nx/modules/catalog/e2e/steps/searchProduct/web.step.ts +0 -5
@@ -345,6 +345,7 @@ export async function initCommand(options = {}) {
345
345
  const rootConfigFiles = [
346
346
  "spectra.config.ts",
347
347
  "tsconfig.spectra.json",
348
+ "tsconfig.spectra.shared.json",
348
349
  "tsconfig.spectra.web.json",
349
350
  "tsconfig.spectra.android.json",
350
351
  "tsconfig.spectra.ios.json",
@@ -1,8 +1,17 @@
1
1
  export type PlatformTarget = "web" | "android" | "ios" | "mobile" | "common";
2
2
  export declare class TypeGenerator {
3
- static getPageObjectsDirs(cwd: string): string[];
4
- static getEntityDirs(cwd: string, entityType: "actions" | "steps" | "fixtures"): string[];
5
- static generateAmbientDeclarations(cwd: string): Record<string, string>;
6
- static generateFixturesDeclaration(cwd: string): string;
3
+ /**
4
+ * Discovers all feature directories and shared library directories.
5
+ * Returns a map of scope -> base directory (e.g. { "shared": ".../shared/testing", "auth": ".../packages/features/auth/e2e" })
6
+ */
7
+ static getEntityScopes(cwd: string): {
8
+ name: string;
9
+ dir: string;
10
+ isShared: boolean;
11
+ }[];
12
+ static getPageObjectsDirsForScope(scopeDir: string, sharedDir?: string): string[];
13
+ static getEntityDirsForScope(entityType: "actions" | "steps" | "fixtures", scopeDir: string, sharedDir?: string): string[];
14
+ static generateDeclarationsForScope(cwd: string, outputTypesDir: string, scopeDir: string, sharedDir?: string): Record<string, string>;
15
+ static generateFixturesDeclarationForScope(scopeDir: string, sharedDir?: string): string;
7
16
  static writeDeclarationFiles(cwd: string): void;
8
17
  }
@@ -8,82 +8,85 @@ const PLATFORM_HIERARCHY = {
8
8
  common: ["common"],
9
9
  };
10
10
  export class TypeGenerator {
11
- static getPageObjectsDirs(cwd) {
12
- const dirs = [];
13
- function scanDir(dir, maxDepth) {
14
- if (maxDepth <= 0 || !fs.existsSync(dir))
11
+ /**
12
+ * Discovers all feature directories and shared library directories.
13
+ * Returns a map of scope -> base directory (e.g. { "shared": ".../shared/testing", "auth": ".../packages/features/auth/e2e" })
14
+ */
15
+ static getEntityScopes(cwd) {
16
+ const scopes = [];
17
+ // 1. Scan for shared directories
18
+ const sharedCandidates = [
19
+ path.join(cwd, "shared", "testing"),
20
+ path.join(cwd, "shared"),
21
+ path.join(cwd, "testing"),
22
+ ];
23
+ for (const cand of sharedCandidates) {
24
+ if (fs.existsSync(cand) && (fs.existsSync(path.join(cand, "page-objects")) || fs.existsSync(path.join(cand, "steps")) || fs.existsSync(path.join(cand, "actions")))) {
25
+ if (!scopes.some((s) => s.dir === cand)) {
26
+ scopes.push({ name: "shared", dir: cand, isShared: true });
27
+ }
28
+ }
29
+ }
30
+ // 2. Scan recursively for feature e2e directories
31
+ function scanForFeatures(dir, depth) {
32
+ if (depth <= 0 || !fs.existsSync(dir))
15
33
  return;
16
34
  const base = path.basename(dir);
17
35
  if (base === "node_modules" || base === "dist" || base === ".git" || base === ".testspectra")
18
36
  return;
19
- if (base === "page-objects" || base === "pageobjects") {
20
- if (!dirs.includes(dir))
21
- dirs.push(dir);
37
+ if (base === "e2e" || base === "tests" || base === "specs") {
38
+ const featureDir = path.dirname(dir);
39
+ const featureName = path.basename(featureDir);
40
+ if (!scopes.some((s) => s.dir === dir)) {
41
+ scopes.push({ name: featureName, dir: dir, isShared: false });
42
+ }
22
43
  return;
23
44
  }
24
45
  try {
25
46
  const entries = fs.readdirSync(dir, { withFileTypes: true });
26
47
  for (const entry of entries) {
27
48
  if (entry.isDirectory()) {
28
- scanDir(path.join(dir, entry.name), maxDepth - 1);
49
+ scanForFeatures(path.join(dir, entry.name), depth - 1);
29
50
  }
30
51
  }
31
52
  }
32
53
  catch { }
33
54
  }
34
- // 1. Scan from cwd downwards (finds all feature page-objects and shared page-objects)
35
- scanDir(cwd, 6);
36
- // 2. Also look upwards to monorepo root if called from a subfolder
37
- let cur = cwd;
38
- while (cur !== path.dirname(cur)) {
39
- if (fs.existsSync(path.join(cur, "pnpm-workspace.yaml")) || fs.existsSync(path.join(cur, "nx.json")) || fs.existsSync(path.join(cur, "spectra.config.ts"))) {
40
- scanDir(cur, 6);
41
- break;
42
- }
43
- cur = path.dirname(cur);
55
+ scanForFeatures(cwd, 6);
56
+ return scopes;
57
+ }
58
+ static getPageObjectsDirsForScope(scopeDir, sharedDir) {
59
+ const dirs = [];
60
+ const candidates = [
61
+ path.join(scopeDir, "page-objects"),
62
+ path.join(scopeDir, "pageobjects"),
63
+ ];
64
+ if (sharedDir) {
65
+ candidates.push(path.join(sharedDir, "page-objects"));
66
+ candidates.push(path.join(sharedDir, "pageobjects"));
67
+ }
68
+ for (const c of candidates) {
69
+ if (fs.existsSync(c) && !dirs.includes(c))
70
+ dirs.push(c);
44
71
  }
45
72
  return dirs;
46
73
  }
47
- static getEntityDirs(cwd, entityType) {
74
+ static getEntityDirsForScope(entityType, scopeDir, sharedDir) {
48
75
  const dirs = [];
49
- function scanDir(dir, maxDepth) {
50
- if (maxDepth <= 0 || !fs.existsSync(dir))
51
- return;
52
- const base = path.basename(dir);
53
- if (base === "node_modules" || base === "dist" || base === ".git" || base === ".testspectra")
54
- return;
55
- if (base === entityType) {
56
- if (!dirs.includes(dir))
57
- dirs.push(dir);
58
- return;
59
- }
60
- try {
61
- const entries = fs.readdirSync(dir, { withFileTypes: true });
62
- for (const entry of entries) {
63
- if (entry.isDirectory()) {
64
- scanDir(path.join(dir, entry.name), maxDepth - 1);
65
- }
66
- }
67
- }
68
- catch { }
76
+ const candidates = [path.join(scopeDir, entityType)];
77
+ if (sharedDir) {
78
+ candidates.push(path.join(sharedDir, entityType));
69
79
  }
70
- // 1. Scan from cwd downwards
71
- scanDir(cwd, 6);
72
- // 2. Look upwards to monorepo root if called from a subfolder
73
- let cur = cwd;
74
- while (cur !== path.dirname(cur)) {
75
- if (fs.existsSync(path.join(cur, "pnpm-workspace.yaml")) || fs.existsSync(path.join(cur, "nx.json")) || fs.existsSync(path.join(cur, "spectra.config.ts"))) {
76
- scanDir(cur, 6);
77
- break;
78
- }
79
- cur = path.dirname(cur);
80
+ for (const c of candidates) {
81
+ if (fs.existsSync(c) && !dirs.includes(c))
82
+ dirs.push(c);
80
83
  }
81
84
  return dirs;
82
85
  }
83
- static generateAmbientDeclarations(cwd) {
84
- const poDirs = this.getPageObjectsDirs(cwd);
85
- const actionsDirs = this.getEntityDirs(cwd, "actions");
86
- const stepsDirs = this.getEntityDirs(cwd, "steps");
86
+ static generateDeclarationsForScope(cwd, outputTypesDir, scopeDir, sharedDir) {
87
+ const poDirs = this.getPageObjectsDirsForScope(scopeDir, sharedDir);
88
+ const actionsDirs = this.getEntityDirsForScope("actions", scopeDir, sharedDir);
89
+ const stepsDirs = this.getEntityDirsForScope("steps", scopeDir, sharedDir);
87
90
  const platforms = ["web", "android", "ios", "mobile", "common"];
88
91
  const results = {};
89
92
  for (const platform of platforms) {
@@ -147,7 +150,7 @@ export class TypeGenerator {
147
150
  content += ` shouldHaveClass(this: Promise<WebdriverIO.Element>, className: string): Promise<WebdriverIO.Element>;\n`;
148
151
  content += ` shouldHaveAttribute(this: Promise<WebdriverIO.Element>, attributeName: string, expectedValue?: string): Promise<WebdriverIO.Element>;\n`;
149
152
  content += `}\n\n`;
150
- // 1. Page Objects Global Declarations (Local + Shared Hierarchical)
153
+ // 1. Page Objects Global Declarations
151
154
  const declaredPOMs = new Set();
152
155
  for (const poDir of poDirs) {
153
156
  if (!fs.existsSync(poDir))
@@ -165,7 +168,7 @@ export class TypeGenerator {
165
168
  for (const stem of hierarchy) {
166
169
  const cand = path.join(classFolder, `${stem}.ts`);
167
170
  if (fs.existsSync(cand)) {
168
- matchedFile = path.relative(path.join(cwd, ".testspectra/types"), cand).replace(/\\/g, "/").replace(/\.ts$/, ".js");
171
+ matchedFile = path.relative(outputTypesDir, cand).replace(/\\/g, "/").replace(/\.ts$/, ".js");
169
172
  if (!matchedFile.startsWith("."))
170
173
  matchedFile = `./${matchedFile}`;
171
174
  break;
@@ -180,7 +183,7 @@ export class TypeGenerator {
180
183
  const className = entry.name.split(".")[0];
181
184
  if (declaredPOMs.has(className))
182
185
  continue;
183
- let rel = path.relative(path.join(cwd, ".testspectra/types"), path.join(poDir, entry.name)).replace(/\\/g, "/").replace(/\.ts$/, ".js");
186
+ let rel = path.relative(outputTypesDir, path.join(poDir, entry.name)).replace(/\\/g, "/").replace(/\.ts$/, ".js");
184
187
  if (!rel.startsWith("."))
185
188
  rel = `./${rel}`;
186
189
  content += `declare const ${className}: typeof import('${rel}').default;\n`;
@@ -228,11 +231,11 @@ export class TypeGenerator {
228
231
  const cand1 = path.join(actionFolder, `${stem}.action.ts`);
229
232
  const cand2 = path.join(actionFolder, `${stem}.ts`);
230
233
  if (fs.existsSync(cand1)) {
231
- matchedFile = path.relative(path.join(cwd, ".testspectra/types"), cand1).replace(/\\/g, "/").replace(/\.ts$/, ".js");
234
+ matchedFile = path.relative(outputTypesDir, cand1).replace(/\\/g, "/").replace(/\.ts$/, ".js");
232
235
  break;
233
236
  }
234
237
  else if (fs.existsSync(cand2)) {
235
- matchedFile = path.relative(path.join(cwd, ".testspectra/types"), cand2).replace(/\\/g, "/").replace(/\.ts$/, ".js");
238
+ matchedFile = path.relative(outputTypesDir, cand2).replace(/\\/g, "/").replace(/\.ts$/, ".js");
236
239
  break;
237
240
  }
238
241
  }
@@ -242,27 +245,27 @@ export class TypeGenerator {
242
245
  content += ` ${actionName}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
243
246
  declaredActions.add(actionName);
244
247
  }
245
- else {
246
- content += ` ${actionName}: (...args: any[]) => Promise<any>;\n`;
247
- declaredActions.add(actionName);
248
- }
249
248
  }
250
249
  else if (entry.isFile() && entry.name.endsWith(".ts")) {
251
- const actionName = entry.name.split(".")[0];
252
- if (declaredActions.has(actionName))
253
- continue;
254
- let rel = path.relative(path.join(cwd, ".testspectra/types"), path.join(actionsDir, entry.name)).replace(/\\/g, "/").replace(/\.ts$/, ".js");
255
- if (!rel.startsWith("."))
256
- rel = `./${rel}`;
257
- content += ` ${actionName}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
258
- declaredActions.add(actionName);
250
+ const parts = entry.name.split(".");
251
+ const stem = parts.length > 2 ? parts[parts.length - 2] : parts[0];
252
+ const actionName = parts[0];
253
+ if (hierarchy.includes(stem) || hierarchy.includes(parts[1])) {
254
+ if (declaredActions.has(actionName))
255
+ continue;
256
+ let rel = path.relative(outputTypesDir, path.join(actionsDir, entry.name)).replace(/\\/g, "/").replace(/\.ts$/, ".js");
257
+ if (!rel.startsWith("."))
258
+ rel = `./${rel}`;
259
+ content += ` ${actionName}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
260
+ declaredActions.add(actionName);
261
+ }
259
262
  }
260
263
  }
261
264
  }
262
265
  content += `}\n`;
263
266
  content += `type TestSpectraActions = SpectraStaticInstance & TestSpectraCustomActions;\n`;
264
267
  content += `declare const Spectra: TestSpectraActions;\n\n`;
265
- // 3. Shared Steps Global Interface (Local + Shared)
268
+ // 3. Built-in Steps Declaration
266
269
  content += `interface TestSpectraSteps {\n`;
267
270
  const declaredSteps = new Set();
268
271
  for (const stepsDir of stepsDirs) {
@@ -282,11 +285,11 @@ export class TypeGenerator {
282
285
  const cand1 = path.join(stepFolder, `${stem}.step.ts`);
283
286
  const cand2 = path.join(stepFolder, `${stem}.ts`);
284
287
  if (fs.existsSync(cand1)) {
285
- matchedFile = path.relative(path.join(cwd, ".testspectra/types"), cand1).replace(/\\/g, "/").replace(/\.ts$/, ".js");
288
+ matchedFile = path.relative(outputTypesDir, cand1).replace(/\\/g, "/").replace(/\.ts$/, ".js");
286
289
  break;
287
290
  }
288
291
  else if (fs.existsSync(cand2)) {
289
- matchedFile = path.relative(path.join(cwd, ".testspectra/types"), cand2).replace(/\\/g, "/").replace(/\.ts$/, ".js");
292
+ matchedFile = path.relative(outputTypesDir, cand2).replace(/\\/g, "/").replace(/\.ts$/, ".js");
290
293
  break;
291
294
  }
292
295
  }
@@ -296,20 +299,20 @@ export class TypeGenerator {
296
299
  content += ` ${stepName}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
297
300
  declaredSteps.add(stepName);
298
301
  }
299
- else {
300
- content += ` ${stepName}: (...args: any[]) => Promise<any>;\n`;
301
- declaredSteps.add(stepName);
302
- }
303
302
  }
304
303
  else if (entry.isFile() && entry.name.endsWith(".ts")) {
305
- const stepName = entry.name.split(".")[0];
306
- if (declaredSteps.has(stepName))
307
- continue;
308
- let rel = path.relative(path.join(cwd, ".testspectra/types"), path.join(stepsDir, entry.name)).replace(/\\/g, "/").replace(/\.ts$/, ".js");
309
- if (!rel.startsWith("."))
310
- rel = `./${rel}`;
311
- content += ` ${stepName}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
312
- declaredSteps.add(stepName);
304
+ const parts = entry.name.split(".");
305
+ const stem = parts.length > 2 ? parts[parts.length - 2] : parts[0];
306
+ const stepName = parts[0];
307
+ if (hierarchy.includes(stem) || hierarchy.includes(parts[1])) {
308
+ if (declaredSteps.has(stepName))
309
+ continue;
310
+ let rel = path.relative(outputTypesDir, path.join(stepsDir, entry.name)).replace(/\\/g, "/").replace(/\.ts$/, ".js");
311
+ if (!rel.startsWith("."))
312
+ rel = `./${rel}`;
313
+ content += ` ${stepName}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
314
+ declaredSteps.add(stepName);
315
+ }
313
316
  }
314
317
  }
315
318
  }
@@ -319,8 +322,8 @@ export class TypeGenerator {
319
322
  }
320
323
  return results;
321
324
  }
322
- static generateFixturesDeclaration(cwd) {
323
- const fixturesDirs = this.getEntityDirs(cwd, "fixtures");
325
+ static generateFixturesDeclarationForScope(scopeDir, sharedDir) {
326
+ const fixturesDirs = this.getEntityDirsForScope("fixtures", scopeDir, sharedDir);
324
327
  let content = `// Auto-generated ambient fixture declarations for TestSpectra\n`;
325
328
  content += `// Managed automatically by TestSpectra Language Service Plugin.\n\n`;
326
329
  content += `interface TestSpectraFixtures {\n`;
@@ -345,21 +348,48 @@ export class TypeGenerator {
345
348
  return content;
346
349
  }
347
350
  static writeDeclarationFiles(cwd) {
348
- const typesDir = path.join(cwd, ".testspectra", "types");
349
- if (!fs.existsSync(typesDir)) {
350
- fs.mkdirSync(typesDir, { recursive: true });
351
+ const rootTypesDir = path.join(cwd, ".testspectra", "types");
352
+ if (!fs.existsSync(rootTypesDir)) {
353
+ fs.mkdirSync(rootTypesDir, { recursive: true });
354
+ }
355
+ const scopes = this.getEntityScopes(cwd);
356
+ const sharedScope = scopes.find((s) => s.isShared);
357
+ // 1. Generate Global / Standalone Root Types (or fallback)
358
+ const rootFixtures = this.generateFixturesDeclarationForScope(cwd);
359
+ fs.writeFileSync(path.join(rootTypesDir, "fixtures.d.ts"), rootFixtures, "utf-8");
360
+ const rootDeclarations = this.generateDeclarationsForScope(cwd, rootTypesDir, cwd);
361
+ for (const [platform, content] of Object.entries(rootDeclarations)) {
362
+ fs.writeFileSync(path.join(rootTypesDir, `${platform}.d.ts`), content, "utf-8");
351
363
  }
352
- // 1. Write single platform-agnostic fixtures.d.ts
353
- const fixturesContent = this.generateFixturesDeclaration(cwd);
354
- fs.writeFileSync(path.join(typesDir, "fixtures.d.ts"), fixturesContent, "utf-8");
355
- // 2. Write per-platform ambient types
356
- const declarations = this.generateAmbientDeclarations(cwd);
357
- for (const [platform, content] of Object.entries(declarations)) {
358
- const filePath = path.join(typesDir, `${platform}.d.ts`);
359
- fs.writeFileSync(filePath, content, "utf-8");
364
+ // 2. Generate Scoped Types for Shared Library: .testspectra/types/shared/
365
+ if (sharedScope) {
366
+ const sharedTypesDir = path.join(rootTypesDir, "shared");
367
+ if (!fs.existsSync(sharedTypesDir))
368
+ fs.mkdirSync(sharedTypesDir, { recursive: true });
369
+ const sharedFixtures = this.generateFixturesDeclarationForScope(sharedScope.dir);
370
+ fs.writeFileSync(path.join(sharedTypesDir, "fixtures.d.ts"), sharedFixtures, "utf-8");
371
+ // Shared scope has NO feature dependencies (only shared itself)
372
+ const sharedDeclarations = this.generateDeclarationsForScope(cwd, sharedTypesDir, sharedScope.dir);
373
+ for (const [platform, content] of Object.entries(sharedDeclarations)) {
374
+ fs.writeFileSync(path.join(sharedTypesDir, `${platform}.d.ts`), content, "utf-8");
375
+ }
376
+ }
377
+ // 3. Generate Scoped Types for Feature Modules: .testspectra/types/features/<featureName>/
378
+ const featureScopes = scopes.filter((s) => !s.isShared);
379
+ for (const feature of featureScopes) {
380
+ const featureTypesDir = path.join(rootTypesDir, "features", feature.name);
381
+ if (!fs.existsSync(featureTypesDir))
382
+ fs.mkdirSync(featureTypesDir, { recursive: true });
383
+ const featureFixtures = this.generateFixturesDeclarationForScope(feature.dir, sharedScope?.dir);
384
+ fs.writeFileSync(path.join(featureTypesDir, "fixtures.d.ts"), featureFixtures, "utf-8");
385
+ // Feature scope gets (feature + shared) ONLY
386
+ const featureDeclarations = this.generateDeclarationsForScope(cwd, featureTypesDir, feature.dir, sharedScope?.dir);
387
+ for (const [platform, content] of Object.entries(featureDeclarations)) {
388
+ fs.writeFileSync(path.join(featureTypesDir, `${platform}.d.ts`), content, "utf-8");
389
+ }
360
390
  }
361
- // Also write a universal ambient index
362
- const universalPath = path.join(typesDir, "index.d.ts");
391
+ // Universal ambient index
392
+ const universalPath = path.join(rootTypesDir, "index.d.ts");
363
393
  fs.writeFileSync(universalPath, `/// <reference path="./common.d.ts" />\n`, "utf-8");
364
394
  }
365
395
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testspectra/cli",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
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,3 +1,4 @@
1
1
  export default async function loginAsAdmin(): Promise<void> {
2
- await Step.loginUser("admin", "SuperAdminSecretKey!");
2
+ // Shared business step directly interacting with shared navigation
3
+ await NavigationBar.goToHome();
3
4
  }
@@ -14,8 +14,7 @@
14
14
  ".testspectra/types/fixtures.d.ts",
15
15
  "spectra.config.ts",
16
16
  "modules/**/e2e/**/*.ts",
17
- "packages/**/e2e/**/*.ts",
18
- "shared/testing/**/*.ts"
17
+ "packages/**/e2e/**/*.ts"
19
18
  ],
20
19
  "exclude": [
21
20
  "node_modules",
@@ -14,8 +14,7 @@
14
14
  ".testspectra/types/fixtures.d.ts",
15
15
  "spectra.config.ts",
16
16
  "modules/**/e2e/**/*.ts",
17
- "packages/**/e2e/**/*.ts",
18
- "shared/testing/**/*.ts"
17
+ "packages/**/e2e/**/*.ts"
19
18
  ],
20
19
  "exclude": [
21
20
  "node_modules",
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "files": [],
14
14
  "references": [
15
+ { "path": "./tsconfig.spectra.shared.json" },
15
16
  { "path": "./tsconfig.spectra.web.json" },
16
17
  { "path": "./tsconfig.spectra.android.json" },
17
18
  { "path": "./tsconfig.spectra.ios.json" }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "composite": true,
7
+ "noEmit": true,
8
+ "skipLibCheck": true,
9
+ "strict": true,
10
+ "types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"]
11
+ },
12
+ "include": [
13
+ ".testspectra/types/shared/common.d.ts",
14
+ ".testspectra/types/shared/fixtures.d.ts",
15
+ "shared/testing/**/*.ts"
16
+ ],
17
+ "exclude": [
18
+ "node_modules",
19
+ "dist"
20
+ ]
21
+ }
@@ -14,8 +14,7 @@
14
14
  ".testspectra/types/fixtures.d.ts",
15
15
  "spectra.config.ts",
16
16
  "modules/**/e2e/**/*.ts",
17
- "packages/**/e2e/**/*.ts",
18
- "shared/testing/**/*.ts"
17
+ "packages/**/e2e/**/*.ts"
19
18
  ],
20
19
  "exclude": [
21
20
  "node_modules",
@@ -1,6 +0,0 @@
1
- export default async function applyDiscount(couponCode: string): Promise<void> {
2
- const couponInput = await $('~coupon_input');
3
- await couponInput.setValue(couponCode);
4
- const applyBtn = await $('~btn_apply');
5
- await applyBtn.click();
6
- }
@@ -1,6 +0,0 @@
1
- export default async function applyDiscount(couponCode: string): Promise<void> {
2
- const couponInput = await $('#coupon-code');
3
- await couponInput.setValue(couponCode);
4
- const applyBtn = await $('#apply-coupon-btn');
5
- await applyBtn.click();
6
- }
@@ -1 +0,0 @@
1
- "DISCOUNT50"
@@ -1 +0,0 @@
1
- "Laptop Gaming"
@@ -1,39 +0,0 @@
1
- export default class ProductPage {
2
- static get searchInput(): ChainablePromiseElement {
3
- return $('~search_input');
4
- }
5
-
6
- static get categorySelect(): ChainablePromiseElement {
7
- return $('~category_picker');
8
- }
9
-
10
- static get productList(): ChainablePromiseElement {
11
- return $('~product_list');
12
- }
13
-
14
- static get firstProductCard(): ChainablePromiseElement {
15
- return $('~product_item_0');
16
- }
17
-
18
- static get addToCartButton(): ChainablePromiseElement {
19
- return $('~btn_add_to_cart');
20
- }
21
-
22
- static get cartBadge(): ChainablePromiseElement {
23
- return $('~cart_badge');
24
- }
25
-
26
- static async open() {
27
- // Mobile in-app navigation to products tab
28
- }
29
-
30
- static async filterByCategory(category: string) {
31
- await Spectra.click(this.categorySelect);
32
- await Spectra.click(`~option_${category}`);
33
- }
34
-
35
- static async addFirstProductToCart() {
36
- await Spectra.swipe({ direction: "down", distance: 400 });
37
- await Spectra.click(this.addToCartButton);
38
- }
39
- }
@@ -1,38 +0,0 @@
1
- export default class ProductPage {
2
- static get searchInput(): ChainablePromiseElement {
3
- return $('#search-input');
4
- }
5
-
6
- static get categorySelect(): ChainablePromiseElement {
7
- return $('#category-select');
8
- }
9
-
10
- static get productList(): ChainablePromiseElement {
11
- return $('#product-grid');
12
- }
13
-
14
- static get firstProductCard(): ChainablePromiseElement {
15
- return $('.product-card');
16
- }
17
-
18
- static get addToCartButton(): ChainablePromiseElement {
19
- return $('#add-to-cart');
20
- }
21
-
22
- static get cartBadge(): ChainablePromiseElement {
23
- return $('#cart-count');
24
- }
25
-
26
- static async open() {
27
- await Spectra.navigate('/products');
28
- }
29
-
30
- static async filterByCategory(category: string) {
31
- await Spectra.select(this.categorySelect, category);
32
- }
33
-
34
- static async addFirstProductToCart() {
35
- await Spectra.scroll({ selector: this.firstProductCard });
36
- await Spectra.click(this.addToCartButton);
37
- }
38
- }
@@ -1,8 +0,0 @@
1
- it("should filter products by category and add to cart on Mobile (Android & iOS)", async () => {
2
- await ProductPage.open();
3
- await ProductPage.filterByCategory("Electronics");
4
- await ProductPage.addFirstProductToCart();
5
-
6
- await ProductPage.cartBadge.shouldBeVisible();
7
- await ProductPage.cartBadge.shouldHaveText("1");
8
- });
@@ -1,8 +0,0 @@
1
- it("should filter products by category and add to cart on Web", async () => {
2
- await ProductPage.open();
3
- await ProductPage.filterByCategory("Electronics");
4
- await ProductPage.addFirstProductToCart();
5
-
6
- await ProductPage.cartBadge.shouldBeVisible();
7
- await ProductPage.cartBadge.shouldHaveText("1");
8
- });
@@ -1,6 +0,0 @@
1
- it("should search products and apply promotional discount coupon on Mobile", async () => {
2
- await Step.searchProduct(Fixture.searchQuery);
3
- await Spectra.applyDiscount(Fixture.couponCode);
4
-
5
- await ProductPage.firstProductCard.shouldBeVisible();
6
- });
@@ -1,6 +0,0 @@
1
- it("should search products and apply promotional discount coupon on Web", async () => {
2
- await Step.searchProduct(Fixture.searchQuery);
3
- await Spectra.applyDiscount(Fixture.couponCode);
4
-
5
- await ProductPage.firstProductCard.shouldBeVisible();
6
- });
@@ -1,3 +0,0 @@
1
- export default async function (): Promise<void> {
2
- // Suite-specific setup for Product catalog suite
3
- }
@@ -1,5 +0,0 @@
1
- export default async function searchProduct(query: string): Promise<void> {
2
- await ProductPage.open();
3
- await Spectra.type(ProductPage.searchInput, query);
4
- await Spectra.pressKey("Enter");
5
- }
@@ -1,5 +0,0 @@
1
- export default async function searchProduct(query: string): Promise<void> {
2
- await ProductPage.open();
3
- await Spectra.type(ProductPage.searchInput, query);
4
- await Spectra.pressKey("Enter");
5
- }
@@ -1,6 +0,0 @@
1
- export default async function applyDiscount(couponCode: string): Promise<void> {
2
- const couponInput = await $('~coupon_input');
3
- await couponInput.setValue(couponCode);
4
- const applyBtn = await $('~btn_apply');
5
- await applyBtn.click();
6
- }
@@ -1,6 +0,0 @@
1
- export default async function applyDiscount(couponCode: string): Promise<void> {
2
- const couponInput = await $('#coupon-code');
3
- await couponInput.setValue(couponCode);
4
- const applyBtn = await $('#apply-coupon-btn');
5
- await applyBtn.click();
6
- }
@@ -1,6 +0,0 @@
1
- export default async function verifyOtp(otp: string): Promise<void> {
2
- const otpInput = await $('~otp_input');
3
- await otpInput.setValue(otp);
4
- const verifyBtn = await $('~verify_btn');
5
- await verifyBtn.click();
6
- }
@@ -1,6 +0,0 @@
1
- export default async function verifyOtp(otp: string): Promise<void> {
2
- const otpInput = await $('#otp');
3
- await otpInput.setValue(otp);
4
- const verifyBtn = await $('#verify-btn');
5
- await verifyBtn.click();
6
- }
@@ -1 +0,0 @@
1
- "Laptop Gaming"
@@ -1 +0,0 @@
1
- {"username": "tomsmith", "role": "admin"}
@@ -1,4 +0,0 @@
1
- export default async function (): Promise<void> {
2
- // Global before setup (executed once before all suites)
3
- await browser.maximizeWindow();
4
- }
@@ -1,18 +0,0 @@
1
- {
2
- "name": "catalog-e2e",
3
- "version": "1.0.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "test": "spectra run",
8
- "type-check": "tsc -b"
9
- },
10
- "devDependencies": {
11
- "@testspectra/cli": "workspace:*",
12
- "@types/node": "^20.14.0",
13
- "@wdio/globals": "^9.2.8",
14
- "@wdio/mocha-framework": "^9.2.8",
15
- "webdriverio": "^9.2.8",
16
- "typescript": "^5.4.5"
17
- }
18
- }
@@ -1,32 +0,0 @@
1
- export default class LoginPage {
2
- static get usernameInput(): ChainablePromiseElement {
3
- return $('~username_input');
4
- }
5
-
6
- static get passwordInput(): ChainablePromiseElement {
7
- return $('~password_input');
8
- }
9
-
10
- static get rememberMeCheckbox(): ChainablePromiseElement {
11
- return $('~remember_me_checkbox');
12
- }
13
-
14
- static get submitButton(): ChainablePromiseElement {
15
- return $('~login_button');
16
- }
17
-
18
- static get welcomeText(): ChainablePromiseElement {
19
- return $('~welcome_text');
20
- }
21
-
22
- static async open() {
23
- // Mobile app launch handled by Appium capabilities
24
- }
25
-
26
- static async login(username: string, pass: string) {
27
- await Spectra.waitForElement(this.usernameInput);
28
- await Spectra.type(this.usernameInput, username, { clearFirst: true });
29
- await Spectra.type(this.passwordInput, pass, { clearFirst: true });
30
- await Spectra.click(this.submitButton);
31
- }
32
- }
@@ -1,32 +0,0 @@
1
- export default class LoginPage {
2
- static get usernameInput(): ChainablePromiseElement {
3
- return $('#username');
4
- }
5
-
6
- static get passwordInput(): ChainablePromiseElement {
7
- return $('#password');
8
- }
9
-
10
- static get rememberMeCheckbox(): ChainablePromiseElement {
11
- return $('#remember-me');
12
- }
13
-
14
- static get submitButton(): ChainablePromiseElement {
15
- return $('button[type="submit"]');
16
- }
17
-
18
- static get flashAlert(): ChainablePromiseElement {
19
- return $('#flash');
20
- }
21
-
22
- static async open() {
23
- await Spectra.navigate('/login');
24
- }
25
-
26
- static async login(username: string, pass: string) {
27
- await Spectra.waitForElement(this.usernameInput);
28
- await Spectra.type(this.usernameInput, username, { clearFirst: true });
29
- await Spectra.type(this.passwordInput, pass, { clearFirst: true });
30
- await Spectra.click(this.submitButton);
31
- }
32
- }
@@ -1,39 +0,0 @@
1
- export default class ProductPage {
2
- static get searchInput(): ChainablePromiseElement {
3
- return $('~search_input');
4
- }
5
-
6
- static get categorySelect(): ChainablePromiseElement {
7
- return $('~category_picker');
8
- }
9
-
10
- static get productList(): ChainablePromiseElement {
11
- return $('~product_list');
12
- }
13
-
14
- static get firstProductCard(): ChainablePromiseElement {
15
- return $('~product_item_0');
16
- }
17
-
18
- static get addToCartButton(): ChainablePromiseElement {
19
- return $('~btn_add_to_cart');
20
- }
21
-
22
- static get cartBadge(): ChainablePromiseElement {
23
- return $('~cart_badge');
24
- }
25
-
26
- static async open() {
27
- // Mobile in-app navigation to products tab
28
- }
29
-
30
- static async filterByCategory(category: string) {
31
- await Spectra.click(this.categorySelect);
32
- await Spectra.click(`~option_${category}`);
33
- }
34
-
35
- static async addFirstProductToCart() {
36
- await Spectra.swipe({ direction: "down", distance: 400 });
37
- await Spectra.click(this.addToCartButton);
38
- }
39
- }
@@ -1,38 +0,0 @@
1
- export default class ProductPage {
2
- static get searchInput(): ChainablePromiseElement {
3
- return $('#search-input');
4
- }
5
-
6
- static get categorySelect(): ChainablePromiseElement {
7
- return $('#category-select');
8
- }
9
-
10
- static get productList(): ChainablePromiseElement {
11
- return $('#product-grid');
12
- }
13
-
14
- static get firstProductCard(): ChainablePromiseElement {
15
- return $('.product-card');
16
- }
17
-
18
- static get addToCartButton(): ChainablePromiseElement {
19
- return $('#add-to-cart');
20
- }
21
-
22
- static get cartBadge(): ChainablePromiseElement {
23
- return $('#cart-count');
24
- }
25
-
26
- static async open() {
27
- await Spectra.navigate('/products');
28
- }
29
-
30
- static async filterByCategory(category: string) {
31
- await Spectra.select(this.categorySelect, category);
32
- }
33
-
34
- static async addFirstProductToCart() {
35
- await Spectra.scroll({ selector: this.firstProductCard });
36
- await Spectra.click(this.addToCartButton);
37
- }
38
- }
@@ -1,26 +0,0 @@
1
- export default class SettingsPage {
2
- static get darkModeToggle(): ChainablePromiseElement {
3
- return $('[data-testid="dark-mode-toggle"]');
4
- }
5
-
6
- static get languageDropdown(): ChainablePromiseElement {
7
- return $('[data-testid="language-select"]');
8
- }
9
-
10
- static get saveButton(): ChainablePromiseElement {
11
- return $('[data-testid="save-settings"]');
12
- }
13
-
14
- static get statusMessage(): ChainablePromiseElement {
15
- return $('[data-testid="status-message"]');
16
- }
17
-
18
- static async open() {
19
- await Spectra.navigate('/settings');
20
- }
21
-
22
- static async toggleDarkMode() {
23
- await Spectra.click(this.darkModeToggle);
24
- await Spectra.click(this.saveButton);
25
- }
26
- }
@@ -1,34 +0,0 @@
1
- {
2
- "name": "catalog-e2e",
3
- "$schema": "../../../node_modules/nx/schemas/project-schema.json",
4
- "projectType": "application",
5
- "sourceRoot": "modules/catalog/e2e",
6
- "targets": {
7
- "e2e": {
8
- "executor": "nx:run-commands",
9
- "options": {
10
- "command": "spectra run",
11
- "cwd": "modules/catalog/e2e"
12
- },
13
- "configurations": {
14
- "android": {
15
- "command": "spectra run --target android"
16
- },
17
- "ios": {
18
- "command": "spectra run --target ios"
19
- },
20
- "headless": {
21
- "command": "spectra run --headless"
22
- }
23
- }
24
- },
25
- "type-check": {
26
- "executor": "nx:run-commands",
27
- "options": {
28
- "command": "tsc -b tsconfig.spectra.json",
29
- "cwd": "."
30
- }
31
- }
32
- },
33
- "tags": ["scope:catalog", "type:e2e"]
34
- }
@@ -1,6 +0,0 @@
1
- it("should authenticate user on Android Appium device", async () => {
2
- await Step.loginUser("tomsmith", "SuperSecretPassword!");
3
-
4
- await LoginPage.welcomeText.shouldBeVisible();
5
- await LoginPage.welcomeText.shouldContainText("Welcome");
6
- });
@@ -1,6 +0,0 @@
1
- it("should authenticate user on iOS Appium device", async () => {
2
- await Step.loginUser("tomsmith", "SuperSecretPassword!");
3
-
4
- await LoginPage.welcomeText.shouldBeVisible();
5
- await LoginPage.welcomeText.shouldContainText("Welcome");
6
- });
@@ -1,13 +0,0 @@
1
- it("should authenticate user and verify flash message across specific platforms", async () => {
2
- await browser.intercept("/api/status", "GET", Fixture.userData);
3
-
4
- // 1. Navigation via Page Object
5
- await LoginPage.open();
6
-
7
- // 2. High-level business flow via Shared Step & Page Object
8
- await Step.loginUser("tomsmith", "SuperSecretPassword!");
9
-
10
- // 3. Direct callable assertion on Page Object element
11
- await LoginPage.flashAlert.shouldBeVisible();
12
- await LoginPage.flashAlert.shouldContainText("You logged into a secure area!");
13
- });
@@ -1,5 +0,0 @@
1
- it("should toggle remember me checkbox on Android", async () => {
2
- await LoginPage.open();
3
- await LoginPage.rememberMeCheckbox.shouldBeVisible();
4
- await Spectra.click(LoginPage.rememberMeCheckbox);
5
- });
@@ -1,5 +0,0 @@
1
- it("should toggle remember me checkbox on Web", async () => {
2
- await LoginPage.open();
3
- await LoginPage.rememberMeCheckbox.shouldBeVisible();
4
- await Spectra.click(LoginPage.rememberMeCheckbox);
5
- });
@@ -1,3 +0,0 @@
1
- export default async function (): Promise<void> {
2
- // Setup Android Appium capabilities
3
- }
@@ -1,3 +0,0 @@
1
- export default async function (): Promise<void> {
2
- // Setup iOS Appium capabilities
3
- }
@@ -1,3 +0,0 @@
1
- export default async function (): Promise<void> {
2
- // Suite-specific setup before test cases in Auth suite run
3
- }
@@ -1,8 +0,0 @@
1
- it("should filter products by category and add to cart on Mobile (Android & iOS)", async () => {
2
- await ProductPage.open();
3
- await ProductPage.filterByCategory("Electronics");
4
- await ProductPage.addFirstProductToCart();
5
-
6
- await ProductPage.cartBadge.shouldBeVisible();
7
- await ProductPage.cartBadge.shouldHaveText("1");
8
- });
@@ -1,8 +0,0 @@
1
- it("should filter products by category and add to cart on Web", async () => {
2
- await ProductPage.open();
3
- await ProductPage.filterByCategory("Electronics");
4
- await ProductPage.addFirstProductToCart();
5
-
6
- await ProductPage.cartBadge.shouldBeVisible();
7
- await ProductPage.cartBadge.shouldHaveText("1");
8
- });
@@ -1,6 +0,0 @@
1
- it("should search products and apply promotional discount coupon on Mobile", async () => {
2
- await Step.searchProduct(Fixture.searchQuery);
3
- await Spectra.applyDiscount(Fixture.couponCode);
4
-
5
- await ProductPage.firstProductCard.shouldBeVisible();
6
- });
@@ -1,6 +0,0 @@
1
- it("should search products and apply promotional discount coupon on Web", async () => {
2
- await Step.searchProduct(Fixture.searchQuery);
3
- await Spectra.applyDiscount(Fixture.couponCode);
4
-
5
- await ProductPage.firstProductCard.shouldBeVisible();
6
- });
@@ -1,3 +0,0 @@
1
- export default async function (): Promise<void> {
2
- // Suite-specific setup for Product catalog suite
3
- }
@@ -1,8 +0,0 @@
1
- it("should toggle dark mode setting across all platforms (Common)", async () => {
2
- await SettingsPage.open();
3
- await SettingsPage.darkModeToggle.shouldBeVisible();
4
- await SettingsPage.toggleDarkMode();
5
-
6
- await SettingsPage.statusMessage.shouldBeVisible();
7
- await SettingsPage.statusMessage.shouldContainText("Settings saved");
8
- });
@@ -1,4 +0,0 @@
1
- export default async function loginUser(u: string, p: string): Promise<void> {
2
- await LoginPage.open();
3
- await LoginPage.login(u, p);
4
- }
@@ -1,4 +0,0 @@
1
- export default async function loginUser(u: string, p: string): Promise<void> {
2
- await LoginPage.open();
3
- await LoginPage.login(u, p);
4
- }
@@ -1,5 +0,0 @@
1
- export default async function searchProduct(query: string): Promise<void> {
2
- await ProductPage.open();
3
- await Spectra.type(ProductPage.searchInput, query);
4
- await Spectra.pressKey("Enter");
5
- }
@@ -1,5 +0,0 @@
1
- export default async function searchProduct(query: string): Promise<void> {
2
- await ProductPage.open();
3
- await Spectra.type(ProductPage.searchInput, query);
4
- await Spectra.pressKey("Enter");
5
- }