@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 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, default as tsPlugin } from "./plugin.js";
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, default as tsPlugin } from "./plugin.js";
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
@@ -10,6 +10,7 @@ declare function init(modules: {
10
10
  typescript: typeof ts;
11
11
  }): {
12
12
  create: (info: ts.server.PluginCreateInfo) => ts.LanguageService;
13
+ getExternalFiles: (project: ts.server.Project) => string[];
13
14
  };
14
15
  export default init;
15
16
  export { init };
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(projectDir);
17
- info.project.projectService.logger.info(`[TestSpectra TS Plugin] Initialized ambient declarations in ${projectDir}`);
55
+ TypeGenerator.writeDeclarationFiles(workspaceRoot);
56
+ log(`Generated ambient declarations on startup`);
18
57
  }
19
58
  catch (err) {
20
- info.project.projectService.logger.info(`[TestSpectra TS Plugin] Failed initial type generation: ${err?.message}`);
59
+ log(`Failed initial type generation: ${err?.message}`);
21
60
  }
22
- // 2. Watch for entity file changes inside TSServer
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 = (pathChanged) => {
27
- if (debounceTimer)
28
- clearTimeout(debounceTimer);
29
- debounceTimer = setTimeout(() => {
63
+ const triggerRegeneration = (reason, sync = false) => {
64
+ const doRegen = () => {
30
65
  try {
31
- TypeGenerator.writeDeclarationFiles(projectDir);
32
- info.project.projectService.logger.info(`[TestSpectra TS Plugin] Regenerated ambient declarations after change in ${pathChanged}`);
66
+ TypeGenerator.writeDeclarationFiles(workspaceRoot);
67
+ log(`Regenerated ambient declarations (${reason})`);
33
68
  }
34
69
  catch (err) {
35
- info.project.projectService.logger.info(`[TestSpectra TS Plugin] Error during regeneration: ${err?.message}`);
70
+ log(`Error during regeneration: ${err?.message}`);
36
71
  }
37
- }, 200);
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 isEntity = watchedFolders.some((f) => fileName.includes(f));
50
- if (isEntity) {
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
- return { create };
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 };
@@ -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 cand = path.join(classFolder, `${stem}.ts`);
169
- if (fs.existsSync(cand)) {
170
- matchedFile = path.relative(outputTypesDir, cand).replace(/\\/g, "/").replace(/\.ts$/, ".js");
171
- if (!matchedFile.startsWith("."))
172
- matchedFile = `./${matchedFile}`;
173
- break;
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}').default;\n`;
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 className = entry.name.split(".")[0];
192
+ const parts = entry.name.split(".");
193
+ const className = parts[0];
183
194
  if (declaredPOMs.has(className))
184
195
  continue;
185
- let rel = path.relative(outputTypesDir, path.join(poDir, entry.name)).replace(/\\/g, "/").replace(/\.ts$/, ".js");
186
- if (!rel.startsWith("."))
187
- rel = `./${rel}`;
188
- content += `declare const ${className}: typeof import('${rel}').default;\n`;
189
- declaredPOMs.add(className);
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
- for (const fixturesDir of fixturesDirs) {
311
- if (!fs.existsSync(fixturesDir))
312
- continue;
313
- const fixtureFiles = fs.readdirSync(fixturesDir);
314
- for (const file of fixtureFiles) {
315
- if (file.startsWith("."))
316
- continue;
317
- const parsed = path.parse(file);
318
- const varName = parsed.name.replace(/[^a-zA-Z0-9_$]/g, "_");
319
- if (declaredFixtures.has(varName))
320
- continue;
321
- content += ` readonly ${varName}: string;\n`;
322
- declaredFixtures.add(varName);
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.44",
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.44",
24
+ "@testspectra/matchers": "^1.0.46",
25
25
  "chalk": "^5.3.0",
26
26
  "commander": "^12.1.0",
27
27
  "dotenv": "^16.4.5",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "@testspectra/cli": "workspace:*",
12
- "@testspectra/matchers": "^1.0.44",
12
+ "@testspectra/matchers": "^1.0.46",
13
13
  "@types/node": "^20.14.0",
14
14
  "@wdio/globals": "^9.2.8",
15
15
  "@wdio/mocha-framework": "^9.2.8",
@@ -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",
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "devDependencies": {
13
13
  "@testspectra/cli": "workspace:*",
14
- "@testspectra/matchers": "^1.0.44",
14
+ "@testspectra/matchers": "^1.0.46",
15
15
  "@types/node": "^20.14.0",
16
16
  "@wdio/globals": "^9.2.8",
17
17
  "@wdio/mocha-framework": "^9.2.8",
@@ -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",