@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.
- package/dist/commands/init.js +1 -0
- package/dist/types/generator.d.ts +13 -4
- package/dist/types/generator.js +130 -100
- package/package.json +1 -1
- package/templates/nx/shared/testing/steps/loginAsAdmin/common.step.ts +2 -1
- package/templates/nx/tsconfig.spectra.android.json +1 -2
- package/templates/nx/tsconfig.spectra.ios.json +1 -2
- package/templates/nx/tsconfig.spectra.json +1 -0
- package/templates/nx/tsconfig.spectra.shared.json +21 -0
- package/templates/nx/tsconfig.spectra.web.json +1 -2
- package/templates/nx/modules/auth/e2e/actions/applyDiscount/mobile.action.ts +0 -6
- package/templates/nx/modules/auth/e2e/actions/applyDiscount/web.action.ts +0 -6
- package/templates/nx/modules/auth/e2e/fixtures/couponCode.json +0 -1
- package/templates/nx/modules/auth/e2e/fixtures/searchQuery.json +0 -1
- package/templates/nx/modules/auth/e2e/page-objects/ProductPage/mobile.ts +0 -39
- package/templates/nx/modules/auth/e2e/page-objects/ProductPage/web.ts +0 -38
- package/templates/nx/modules/auth/e2e/specs/Product/TC-PROD-01/mobile.test.ts +0 -8
- package/templates/nx/modules/auth/e2e/specs/Product/TC-PROD-01/web.test.ts +0 -8
- package/templates/nx/modules/auth/e2e/specs/Product/TC-PROD-02/mobile.test.ts +0 -6
- package/templates/nx/modules/auth/e2e/specs/Product/TC-PROD-02/web.test.ts +0 -6
- package/templates/nx/modules/auth/e2e/specs/Product/hooks/before.web.hook.ts +0 -3
- package/templates/nx/modules/auth/e2e/steps/searchProduct/mobile.step.ts +0 -5
- package/templates/nx/modules/auth/e2e/steps/searchProduct/web.step.ts +0 -5
- package/templates/nx/modules/catalog/e2e/actions/applyDiscount/mobile.action.ts +0 -6
- package/templates/nx/modules/catalog/e2e/actions/applyDiscount/web.action.ts +0 -6
- package/templates/nx/modules/catalog/e2e/actions/verifyOtp/mobile.action.ts +0 -6
- package/templates/nx/modules/catalog/e2e/actions/verifyOtp/web.action.ts +0 -6
- package/templates/nx/modules/catalog/e2e/fixtures/couponCode.json +0 -1
- package/templates/nx/modules/catalog/e2e/fixtures/searchQuery.json +0 -1
- package/templates/nx/modules/catalog/e2e/fixtures/userData.json +0 -1
- package/templates/nx/modules/catalog/e2e/global-hooks/before.web.hook.ts +0 -4
- package/templates/nx/modules/catalog/e2e/package.json +0 -18
- package/templates/nx/modules/catalog/e2e/page-objects/LoginPage/mobile.ts +0 -32
- package/templates/nx/modules/catalog/e2e/page-objects/LoginPage/web.ts +0 -32
- package/templates/nx/modules/catalog/e2e/page-objects/ProductPage/mobile.ts +0 -39
- package/templates/nx/modules/catalog/e2e/page-objects/ProductPage/web.ts +0 -38
- package/templates/nx/modules/catalog/e2e/page-objects/SettingsPage/common.ts +0 -26
- package/templates/nx/modules/catalog/e2e/project.json +0 -34
- package/templates/nx/modules/catalog/e2e/specs/Auth/TC-AUTH-01/android.test.ts +0 -6
- package/templates/nx/modules/catalog/e2e/specs/Auth/TC-AUTH-01/ios.test.ts +0 -6
- package/templates/nx/modules/catalog/e2e/specs/Auth/TC-AUTH-01/web.test.ts +0 -13
- package/templates/nx/modules/catalog/e2e/specs/Auth/TC-AUTH-02/android.test.ts +0 -5
- package/templates/nx/modules/catalog/e2e/specs/Auth/TC-AUTH-02/web.test.ts +0 -5
- package/templates/nx/modules/catalog/e2e/specs/Auth/hooks/before.android.hook.ts +0 -3
- package/templates/nx/modules/catalog/e2e/specs/Auth/hooks/before.ios.hook.ts +0 -3
- package/templates/nx/modules/catalog/e2e/specs/Auth/hooks/before.web.hook.ts +0 -3
- package/templates/nx/modules/catalog/e2e/specs/Product/TC-PROD-01/mobile.test.ts +0 -8
- package/templates/nx/modules/catalog/e2e/specs/Product/TC-PROD-01/web.test.ts +0 -8
- package/templates/nx/modules/catalog/e2e/specs/Product/TC-PROD-02/mobile.test.ts +0 -6
- package/templates/nx/modules/catalog/e2e/specs/Product/TC-PROD-02/web.test.ts +0 -6
- package/templates/nx/modules/catalog/e2e/specs/Product/hooks/before.web.hook.ts +0 -3
- package/templates/nx/modules/catalog/e2e/specs/Settings/TC-SET-01/common.test.ts +0 -8
- package/templates/nx/modules/catalog/e2e/steps/loginUser/mobile.step.ts +0 -4
- package/templates/nx/modules/catalog/e2e/steps/loginUser/web.step.ts +0 -4
- package/templates/nx/modules/catalog/e2e/steps/searchProduct/mobile.step.ts +0 -5
- package/templates/nx/modules/catalog/e2e/steps/searchProduct/web.step.ts +0 -5
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
}
|
package/dist/types/generator.js
CHANGED
|
@@ -8,82 +8,85 @@ const PLATFORM_HIERARCHY = {
|
|
|
8
8
|
common: ["common"],
|
|
9
9
|
};
|
|
10
10
|
export class TypeGenerator {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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 === "
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
49
|
+
scanForFeatures(path.join(dir, entry.name), depth - 1);
|
|
29
50
|
}
|
|
30
51
|
}
|
|
31
52
|
}
|
|
32
53
|
catch { }
|
|
33
54
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
74
|
+
static getEntityDirsForScope(entityType, scopeDir, sharedDir) {
|
|
48
75
|
const dirs = [];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
84
|
-
const poDirs = this.
|
|
85
|
-
const actionsDirs = this.
|
|
86
|
-
const stepsDirs = this.
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
323
|
-
const fixturesDirs = this.
|
|
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
|
|
349
|
-
if (!fs.existsSync(
|
|
350
|
-
fs.mkdirSync(
|
|
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
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
//
|
|
362
|
-
const universalPath = path.join(
|
|
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
|
@@ -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
|
+
}
|
|
@@ -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 +0,0 @@
|
|
|
1
|
-
"DISCOUNT50"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"Laptop Gaming"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"username": "tomsmith", "role": "admin"}
|
|
@@ -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,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,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,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
|
-
});
|