@testspectra/cli 1.0.44 → 1.0.46
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/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +126 -18
- package/dist/types/generator.js +60 -26
- package/package.json +2 -2
- package/templates/default/package.json +1 -1
- package/templates/default/tsconfig.spectra.android.json +4 -1
- package/templates/default/tsconfig.spectra.ios.json +4 -1
- package/templates/default/tsconfig.spectra.web.json +4 -1
- package/templates/nx/package.json +1 -1
- package/templates/nx/tsconfig.spectra.android.json +4 -1
- package/templates/nx/tsconfig.spectra.ios.json +4 -1
- package/templates/nx/tsconfig.spectra.shared.json +4 -1
- package/templates/nx/tsconfig.spectra.web.json +4 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { init } from "./plugin.js";
|
|
2
3
|
export * from "./config/schema.js";
|
|
3
4
|
export * from "./config/loader.js";
|
|
4
5
|
export * from "./types/generator.js";
|
|
5
6
|
export * from "@testspectra/matchers";
|
|
6
|
-
export { init as initTsPlugin,
|
|
7
|
+
export { init as initTsPlugin, init };
|
|
8
|
+
export default init;
|
|
7
9
|
export declare function createCliProgram(): Command;
|
|
8
10
|
export declare function runCli(args?: string[]): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -4,11 +4,13 @@ import { devicesCommand } from "./commands/devices.js";
|
|
|
4
4
|
import { doctorCommand } from "./commands/doctor.js";
|
|
5
5
|
import { initCommand } from "./commands/init.js";
|
|
6
6
|
import { runCommand } from "./commands/run.js";
|
|
7
|
+
import { init } from "./plugin.js";
|
|
7
8
|
export * from "./config/schema.js";
|
|
8
9
|
export * from "./config/loader.js";
|
|
9
10
|
export * from "./types/generator.js";
|
|
10
11
|
export * from "@testspectra/matchers";
|
|
11
|
-
export { init as initTsPlugin,
|
|
12
|
+
export { init as initTsPlugin, init };
|
|
13
|
+
export default init;
|
|
12
14
|
export function createCliProgram() {
|
|
13
15
|
const program = new Command();
|
|
14
16
|
program
|
package/dist/plugin.d.ts
CHANGED
package/dist/plugin.js
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
1
3
|
import { TypeGenerator } from "./types/generator.js";
|
|
4
|
+
/**
|
|
5
|
+
* Traverses upwards from `startDir` to locate the TestSpectra workspace root.
|
|
6
|
+
* Looks for indicators: `spectra.config.ts`, `.testspectra`, `pnpm-workspace.yaml`, or `nx.json`.
|
|
7
|
+
*/
|
|
8
|
+
function findWorkspaceRoot(startDir) {
|
|
9
|
+
let cur = path.resolve(startDir);
|
|
10
|
+
while (cur !== path.dirname(cur)) {
|
|
11
|
+
if (fs.existsSync(path.join(cur, "spectra.config.ts")) ||
|
|
12
|
+
fs.existsSync(path.join(cur, ".testspectra")) ||
|
|
13
|
+
fs.existsSync(path.join(cur, "pnpm-workspace.yaml")) ||
|
|
14
|
+
fs.existsSync(path.join(cur, "nx.json"))) {
|
|
15
|
+
return cur;
|
|
16
|
+
}
|
|
17
|
+
cur = path.dirname(cur);
|
|
18
|
+
}
|
|
19
|
+
return startDir;
|
|
20
|
+
}
|
|
21
|
+
function appendLog(workspaceRoot, message) {
|
|
22
|
+
try {
|
|
23
|
+
const logDir = path.join(workspaceRoot, ".testspectra", "logs");
|
|
24
|
+
if (!fs.existsSync(logDir))
|
|
25
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
26
|
+
const logFile = path.join(logDir, "plugin.log");
|
|
27
|
+
const timestamp = new Date().toISOString();
|
|
28
|
+
fs.appendFileSync(logFile, `[${timestamp}] ${message}\n`, "utf-8");
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
2
32
|
/**
|
|
3
33
|
* TypeScript Language Service Plugin Factory for TestSpectra.
|
|
4
34
|
* Runs silently inside TSServer (VS Code, Cursor, WebStorm, Neovim, etc.).
|
|
@@ -11,31 +41,78 @@ function init(modules) {
|
|
|
11
41
|
function create(info) {
|
|
12
42
|
const project = info.project;
|
|
13
43
|
const projectDir = project.getCurrentDirectory();
|
|
44
|
+
const workspaceRoot = findWorkspaceRoot(projectDir);
|
|
45
|
+
const log = (msg) => {
|
|
46
|
+
try {
|
|
47
|
+
info.project.projectService.logger.info(`[TestSpectra TS Plugin] ${msg}`);
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
appendLog(workspaceRoot, msg);
|
|
51
|
+
};
|
|
52
|
+
log(`Plugin initialized on project: ${projectDir} (resolved root: ${workspaceRoot})`);
|
|
14
53
|
// 1. Initial Generation on project load
|
|
15
54
|
try {
|
|
16
|
-
TypeGenerator.writeDeclarationFiles(
|
|
17
|
-
|
|
55
|
+
TypeGenerator.writeDeclarationFiles(workspaceRoot);
|
|
56
|
+
log(`Generated ambient declarations on startup`);
|
|
18
57
|
}
|
|
19
58
|
catch (err) {
|
|
20
|
-
|
|
59
|
+
log(`Failed initial type generation: ${err?.message}`);
|
|
21
60
|
}
|
|
22
|
-
//
|
|
23
|
-
const watchedFolders = ["page-objects", "pageobjects", "actions", "steps", "fixtures", "hooks"];
|
|
24
|
-
// Debounce type regeneration
|
|
61
|
+
// Debounced regeneration handler
|
|
25
62
|
let debounceTimer = null;
|
|
26
|
-
const triggerRegeneration = (
|
|
27
|
-
|
|
28
|
-
clearTimeout(debounceTimer);
|
|
29
|
-
debounceTimer = setTimeout(() => {
|
|
63
|
+
const triggerRegeneration = (reason, sync = false) => {
|
|
64
|
+
const doRegen = () => {
|
|
30
65
|
try {
|
|
31
|
-
TypeGenerator.writeDeclarationFiles(
|
|
32
|
-
|
|
66
|
+
TypeGenerator.writeDeclarationFiles(workspaceRoot);
|
|
67
|
+
log(`Regenerated ambient declarations (${reason})`);
|
|
33
68
|
}
|
|
34
69
|
catch (err) {
|
|
35
|
-
|
|
70
|
+
log(`Error during regeneration: ${err?.message}`);
|
|
36
71
|
}
|
|
37
|
-
}
|
|
72
|
+
};
|
|
73
|
+
if (sync) {
|
|
74
|
+
doRegen();
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
if (debounceTimer)
|
|
78
|
+
clearTimeout(debounceTimer);
|
|
79
|
+
debounceTimer = setTimeout(doRegen, 80);
|
|
80
|
+
}
|
|
38
81
|
};
|
|
82
|
+
// 2. Active File System Watcher on the entire workspace
|
|
83
|
+
try {
|
|
84
|
+
if (fs.existsSync(workspaceRoot)) {
|
|
85
|
+
const watcher = fs.watch(workspaceRoot, { recursive: true }, (eventType, filename) => {
|
|
86
|
+
if (!filename)
|
|
87
|
+
return;
|
|
88
|
+
const fn = filename.toString();
|
|
89
|
+
// Skip internal and build directories
|
|
90
|
+
if (fn.includes(".testspectra") ||
|
|
91
|
+
fn.includes("node_modules") ||
|
|
92
|
+
fn.includes(".git") ||
|
|
93
|
+
fn.includes("dist") ||
|
|
94
|
+
fn.includes(".nx")) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Match relevant entity files
|
|
98
|
+
if (fn.includes("fixtures") ||
|
|
99
|
+
fn.includes("page-objects") ||
|
|
100
|
+
fn.includes("pageobjects") ||
|
|
101
|
+
fn.includes("actions") ||
|
|
102
|
+
fn.includes("steps") ||
|
|
103
|
+
fn.includes("hooks") ||
|
|
104
|
+
fn.includes("spectra.config")) {
|
|
105
|
+
log(`Detected file change [${eventType}]: ${fn}`);
|
|
106
|
+
triggerRegeneration(`fs.watch '${eventType}' on ${fn}`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
watcher.on("error", () => { });
|
|
110
|
+
log(`Attached recursive fs.watch on ${workspaceRoot}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
log(`Note: Recursive fs.watch not supported or failed: ${err?.message}`);
|
|
115
|
+
}
|
|
39
116
|
// 3. Proxy language service methods to detect file modifications / completions
|
|
40
117
|
const proxy = Object.create(null);
|
|
41
118
|
for (const k of Object.keys(info.languageService)) {
|
|
@@ -46,9 +123,9 @@ function init(modules) {
|
|
|
46
123
|
// Intercept getCompletionsAtPosition to ensure fresh types
|
|
47
124
|
const originalGetCompletions = info.languageService.getCompletionsAtPosition;
|
|
48
125
|
proxy.getCompletionsAtPosition = (fileName, position, options, formattingSettings) => {
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
triggerRegeneration(fileName);
|
|
126
|
+
const ext = path.extname(fileName);
|
|
127
|
+
if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".mjs") {
|
|
128
|
+
triggerRegeneration(`completions at ${path.basename(fileName)}`, false);
|
|
52
129
|
}
|
|
53
130
|
return originalGetCompletions.apply(info.languageService, [
|
|
54
131
|
fileName,
|
|
@@ -57,9 +134,40 @@ function init(modules) {
|
|
|
57
134
|
formattingSettings,
|
|
58
135
|
]);
|
|
59
136
|
};
|
|
137
|
+
// Intercept getQuickInfoAtPosition for fresh hover info
|
|
138
|
+
const originalGetQuickInfo = info.languageService.getQuickInfoAtPosition;
|
|
139
|
+
proxy.getQuickInfoAtPosition = (fileName, position) => {
|
|
140
|
+
return originalGetQuickInfo.apply(info.languageService, [fileName, position]);
|
|
141
|
+
};
|
|
60
142
|
return proxy;
|
|
61
143
|
}
|
|
62
|
-
|
|
144
|
+
function getExternalFiles(project) {
|
|
145
|
+
try {
|
|
146
|
+
const workspaceRoot = findWorkspaceRoot(project.getCurrentDirectory());
|
|
147
|
+
const typesDir = path.join(workspaceRoot, ".testspectra", "types");
|
|
148
|
+
const files = [];
|
|
149
|
+
function collectDts(dir) {
|
|
150
|
+
if (!fs.existsSync(dir))
|
|
151
|
+
return;
|
|
152
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
const fullPath = path.join(dir, entry.name);
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
collectDts(fullPath);
|
|
157
|
+
}
|
|
158
|
+
else if (entry.isFile() && entry.name.endsWith(".d.ts")) {
|
|
159
|
+
files.push(fullPath);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
collectDts(typesDir);
|
|
164
|
+
return files;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { create, getExternalFiles };
|
|
63
171
|
}
|
|
64
172
|
export default init;
|
|
65
173
|
export { init };
|
package/dist/types/generator.js
CHANGED
|
@@ -165,28 +165,51 @@ export class TypeGenerator {
|
|
|
165
165
|
const classFolder = path.join(poDir, className);
|
|
166
166
|
let matchedFile = null;
|
|
167
167
|
for (const stem of hierarchy) {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
168
|
+
const candidates = [
|
|
169
|
+
path.join(classFolder, `${stem}.ts`),
|
|
170
|
+
path.join(classFolder, `${stem}.page.ts`),
|
|
171
|
+
path.join(classFolder, `${stem}.po.ts`),
|
|
172
|
+
path.join(classFolder, "index.ts"),
|
|
173
|
+
path.join(classFolder, "common.ts"),
|
|
174
|
+
];
|
|
175
|
+
for (const cand of candidates) {
|
|
176
|
+
if (fs.existsSync(cand)) {
|
|
177
|
+
matchedFile = path.relative(outputTypesDir, cand).replace(/\\/g, "/").replace(/\.ts$/, ".js");
|
|
178
|
+
if (!matchedFile.startsWith("."))
|
|
179
|
+
matchedFile = `./${matchedFile}`;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
174
182
|
}
|
|
183
|
+
if (matchedFile)
|
|
184
|
+
break;
|
|
175
185
|
}
|
|
176
186
|
if (matchedFile) {
|
|
177
|
-
content += `declare const ${className}: typeof import('${matchedFile}')
|
|
187
|
+
content += `declare const ${className}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
|
|
178
188
|
declaredPOMs.add(className);
|
|
179
189
|
}
|
|
180
190
|
}
|
|
181
191
|
else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
182
|
-
const
|
|
192
|
+
const parts = entry.name.split(".");
|
|
193
|
+
const className = parts[0];
|
|
183
194
|
if (declaredPOMs.has(className))
|
|
184
195
|
continue;
|
|
185
|
-
let
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
196
|
+
let isMatch = false;
|
|
197
|
+
if (parts.length === 2) {
|
|
198
|
+
// E.g. ProfilePage.ts (applies to all platforms)
|
|
199
|
+
isMatch = true;
|
|
200
|
+
}
|
|
201
|
+
else if (parts.length > 2) {
|
|
202
|
+
// E.g. ProfilePage.web.ts or ProfilePage.android.ts
|
|
203
|
+
const stem = parts[1];
|
|
204
|
+
isMatch = hierarchy.includes(stem);
|
|
205
|
+
}
|
|
206
|
+
if (isMatch) {
|
|
207
|
+
let rel = path.relative(outputTypesDir, path.join(poDir, entry.name)).replace(/\\/g, "/").replace(/\.ts$/, ".js");
|
|
208
|
+
if (!rel.startsWith("."))
|
|
209
|
+
rel = `./${rel}`;
|
|
210
|
+
content += `declare const ${className}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
|
|
211
|
+
declaredPOMs.add(className);
|
|
212
|
+
}
|
|
190
213
|
}
|
|
191
214
|
}
|
|
192
215
|
}
|
|
@@ -307,20 +330,31 @@ export class TypeGenerator {
|
|
|
307
330
|
content += `// Managed automatically by TestSpectra Language Service Plugin.\n\n`;
|
|
308
331
|
content += `interface TestSpectraFixtures {\n`;
|
|
309
332
|
const declaredFixtures = new Set();
|
|
310
|
-
|
|
311
|
-
if (!fs.existsSync(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
333
|
+
function scanFixtureDir(dir) {
|
|
334
|
+
if (!fs.existsSync(dir))
|
|
335
|
+
return;
|
|
336
|
+
try {
|
|
337
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
338
|
+
for (const entry of entries) {
|
|
339
|
+
if (entry.name.startsWith("."))
|
|
340
|
+
continue;
|
|
341
|
+
if (entry.isDirectory()) {
|
|
342
|
+
scanFixtureDir(path.join(dir, entry.name));
|
|
343
|
+
}
|
|
344
|
+
else if (entry.isFile()) {
|
|
345
|
+
const parsed = path.parse(entry.name);
|
|
346
|
+
const varName = parsed.name.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
347
|
+
if (declaredFixtures.has(varName))
|
|
348
|
+
continue;
|
|
349
|
+
content += ` readonly ${varName}: string;\n`;
|
|
350
|
+
declaredFixtures.add(varName);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
323
353
|
}
|
|
354
|
+
catch { }
|
|
355
|
+
}
|
|
356
|
+
for (const fixturesDir of fixturesDirs) {
|
|
357
|
+
scanFixtureDir(fixturesDir);
|
|
324
358
|
}
|
|
325
359
|
content += `}\n`;
|
|
326
360
|
content += `declare const Fixture: TestSpectraFixtures;\n`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testspectra/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.46",
|
|
4
4
|
"description": "TestSpectra Zero-Config Cross-Platform Test Runner CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@clack/prompts": "^1.7.0",
|
|
24
|
-
"@testspectra/matchers": "^1.0.
|
|
24
|
+
"@testspectra/matchers": "^1.0.46",
|
|
25
25
|
"chalk": "^5.3.0",
|
|
26
26
|
"commander": "^12.1.0",
|
|
27
27
|
"dotenv": "^16.4.5",
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
"noEmit": true,
|
|
8
8
|
"skipLibCheck": true,
|
|
9
9
|
"strict": true,
|
|
10
|
-
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"]
|
|
10
|
+
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
|
|
11
|
+
"plugins": [
|
|
12
|
+
{ "name": "@testspectra/cli" }
|
|
13
|
+
]
|
|
11
14
|
},
|
|
12
15
|
"include": [
|
|
13
16
|
".testspectra/types/android.d.ts",
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
"noEmit": true,
|
|
8
8
|
"skipLibCheck": true,
|
|
9
9
|
"strict": true,
|
|
10
|
-
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"]
|
|
10
|
+
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
|
|
11
|
+
"plugins": [
|
|
12
|
+
{ "name": "@testspectra/cli" }
|
|
13
|
+
]
|
|
11
14
|
},
|
|
12
15
|
"include": [
|
|
13
16
|
".testspectra/types/ios.d.ts",
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
"noEmit": true,
|
|
8
8
|
"skipLibCheck": true,
|
|
9
9
|
"strict": true,
|
|
10
|
-
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"]
|
|
10
|
+
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
|
|
11
|
+
"plugins": [
|
|
12
|
+
{ "name": "@testspectra/cli" }
|
|
13
|
+
]
|
|
11
14
|
},
|
|
12
15
|
"include": [
|
|
13
16
|
".testspectra/types/web.d.ts",
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
"noEmit": true,
|
|
8
8
|
"skipLibCheck": true,
|
|
9
9
|
"strict": true,
|
|
10
|
-
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"]
|
|
10
|
+
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
|
|
11
|
+
"plugins": [
|
|
12
|
+
{ "name": "@testspectra/cli" }
|
|
13
|
+
]
|
|
11
14
|
},
|
|
12
15
|
"include": [
|
|
13
16
|
".testspectra/types/android.d.ts",
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
"noEmit": true,
|
|
8
8
|
"skipLibCheck": true,
|
|
9
9
|
"strict": true,
|
|
10
|
-
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"]
|
|
10
|
+
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
|
|
11
|
+
"plugins": [
|
|
12
|
+
{ "name": "@testspectra/cli" }
|
|
13
|
+
]
|
|
11
14
|
},
|
|
12
15
|
"include": [
|
|
13
16
|
".testspectra/types/ios.d.ts",
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
"noEmit": true,
|
|
8
8
|
"skipLibCheck": true,
|
|
9
9
|
"strict": true,
|
|
10
|
-
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"]
|
|
10
|
+
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
|
|
11
|
+
"plugins": [
|
|
12
|
+
{ "name": "@testspectra/cli" }
|
|
13
|
+
]
|
|
11
14
|
},
|
|
12
15
|
"include": [
|
|
13
16
|
".testspectra/types/shared/common.d.ts",
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
"noEmit": true,
|
|
8
8
|
"skipLibCheck": true,
|
|
9
9
|
"strict": true,
|
|
10
|
-
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"]
|
|
10
|
+
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
|
|
11
|
+
"plugins": [
|
|
12
|
+
{ "name": "@testspectra/cli" }
|
|
13
|
+
]
|
|
11
14
|
},
|
|
12
15
|
"include": [
|
|
13
16
|
".testspectra/types/web.d.ts",
|