@testspectra/cli 1.0.43 → 1.0.45

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/plugin.js CHANGED
@@ -1,4 +1,23 @@
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
+ }
2
21
  /**
3
22
  * TypeScript Language Service Plugin Factory for TestSpectra.
4
23
  * Runs silently inside TSServer (VS Code, Cursor, WebStorm, Neovim, etc.).
@@ -11,31 +30,74 @@ function init(modules) {
11
30
  function create(info) {
12
31
  const project = info.project;
13
32
  const projectDir = project.getCurrentDirectory();
33
+ const workspaceRoot = findWorkspaceRoot(projectDir);
34
+ const log = (msg) => {
35
+ try {
36
+ info.project.projectService.logger.info(`[TestSpectra TS Plugin] ${msg}`);
37
+ }
38
+ catch { }
39
+ };
14
40
  // 1. Initial Generation on project load
15
41
  try {
16
- TypeGenerator.writeDeclarationFiles(projectDir);
17
- info.project.projectService.logger.info(`[TestSpectra TS Plugin] Initialized ambient declarations in ${projectDir}`);
42
+ TypeGenerator.writeDeclarationFiles(workspaceRoot);
43
+ log(`Initialized ambient declarations in workspace root: ${workspaceRoot}`);
18
44
  }
19
45
  catch (err) {
20
- info.project.projectService.logger.info(`[TestSpectra TS Plugin] Failed initial type generation: ${err?.message}`);
46
+ log(`Failed initial type generation: ${err?.message}`);
21
47
  }
22
- // 2. Watch for entity file changes inside TSServer
23
- const watchedFolders = ["page-objects", "pageobjects", "actions", "steps", "fixtures", "hooks"];
24
- // Debounce type regeneration
48
+ // Debounced regeneration handler
25
49
  let debounceTimer = null;
26
- const triggerRegeneration = (pathChanged) => {
27
- if (debounceTimer)
28
- clearTimeout(debounceTimer);
29
- debounceTimer = setTimeout(() => {
50
+ const triggerRegeneration = (reason, sync = false) => {
51
+ const doRegen = () => {
30
52
  try {
31
- TypeGenerator.writeDeclarationFiles(projectDir);
32
- info.project.projectService.logger.info(`[TestSpectra TS Plugin] Regenerated ambient declarations after change in ${pathChanged}`);
53
+ TypeGenerator.writeDeclarationFiles(workspaceRoot);
54
+ log(`Regenerated ambient declarations (${reason})`);
33
55
  }
34
56
  catch (err) {
35
- info.project.projectService.logger.info(`[TestSpectra TS Plugin] Error during regeneration: ${err?.message}`);
57
+ log(`Error during regeneration: ${err?.message}`);
36
58
  }
37
- }, 200);
59
+ };
60
+ if (sync) {
61
+ doRegen();
62
+ }
63
+ else {
64
+ if (debounceTimer)
65
+ clearTimeout(debounceTimer);
66
+ debounceTimer = setTimeout(doRegen, 80);
67
+ }
38
68
  };
69
+ // 2. Active File System Watcher on the entire workspace
70
+ try {
71
+ if (fs.existsSync(workspaceRoot)) {
72
+ const watcher = fs.watch(workspaceRoot, { recursive: true }, (eventType, filename) => {
73
+ if (!filename)
74
+ return;
75
+ const fn = filename.toString();
76
+ // Skip internal and build directories
77
+ if (fn.includes(".testspectra") ||
78
+ fn.includes("node_modules") ||
79
+ fn.includes(".git") ||
80
+ fn.includes("dist") ||
81
+ fn.includes(".nx")) {
82
+ return;
83
+ }
84
+ // Match relevant entity files
85
+ if (fn.includes("fixtures") ||
86
+ fn.includes("page-objects") ||
87
+ fn.includes("pageobjects") ||
88
+ fn.includes("actions") ||
89
+ fn.includes("steps") ||
90
+ fn.includes("hooks") ||
91
+ fn.includes("spectra.config")) {
92
+ triggerRegeneration(`fs.watch '${eventType}' on ${fn}`);
93
+ }
94
+ });
95
+ watcher.on("error", () => { });
96
+ }
97
+ }
98
+ catch (err) {
99
+ log(`Note: Recursive fs.watch not supported or failed: ${err?.message}`);
100
+ }
39
101
  // 3. Proxy language service methods to detect file modifications / completions
40
102
  const proxy = Object.create(null);
41
103
  for (const k of Object.keys(info.languageService)) {
@@ -46,9 +108,9 @@ function init(modules) {
46
108
  // Intercept getCompletionsAtPosition to ensure fresh types
47
109
  const originalGetCompletions = info.languageService.getCompletionsAtPosition;
48
110
  proxy.getCompletionsAtPosition = (fileName, position, options, formattingSettings) => {
49
- const isEntity = watchedFolders.some((f) => fileName.includes(f));
50
- if (isEntity) {
51
- triggerRegeneration(fileName);
111
+ const ext = path.extname(fileName);
112
+ if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".mjs") {
113
+ triggerRegeneration(`completions at ${path.basename(fileName)}`, false);
52
114
  }
53
115
  return originalGetCompletions.apply(info.languageService, [
54
116
  fileName,
@@ -57,6 +119,11 @@ function init(modules) {
57
119
  formattingSettings,
58
120
  ]);
59
121
  };
122
+ // Intercept getQuickInfoAtPosition for fresh hover info
123
+ const originalGetQuickInfo = info.languageService.getQuickInfoAtPosition;
124
+ proxy.getQuickInfoAtPosition = (fileName, position) => {
125
+ return originalGetQuickInfo.apply(info.languageService, [fileName, position]);
126
+ };
60
127
  return proxy;
61
128
  }
62
129
  return { create };
@@ -191,26 +191,6 @@ export class TypeGenerator {
191
191
  }
192
192
  }
193
193
  // 2. Built-in Spectra Static Methods & Custom Actions
194
- content += `\ninterface SpectraStaticInstance {\n`;
195
- content += ` get(target: any): import('@testspectra/matchers').SingleElementRunner;\n`;
196
- content += ` getAll(selector: string): import('@testspectra/matchers').MultiElementRunner;\n`;
197
- content += ` navigate(url: string): import('@testspectra/matchers').SingleElementRunner;\n`;
198
- content += ` back(): import('@testspectra/matchers').SingleElementRunner;\n`;
199
- content += ` refresh(): import('@testspectra/matchers').SingleElementRunner;\n`;
200
- content += ` click(target: any, textOrOptions?: string | import('@testspectra/matchers').ClickOptions): import('@testspectra/matchers').SingleElementRunner;\n`;
201
- content += ` doubleClick(target: any, textOrOptions?: string | import('@testspectra/matchers').ClickOptions): import('@testspectra/matchers').SingleElementRunner;\n`;
202
- content += ` longPress(target: any, options?: import('@testspectra/matchers').LongPressOptions | number): import('@testspectra/matchers').SingleElementRunner;\n`;
203
- content += ` type(target: any, value: string, options?: import('@testspectra/matchers').TypeOptions): import('@testspectra/matchers').SingleElementRunner;\n`;
204
- content += ` clear(target: any): import('@testspectra/matchers').SingleElementRunner;\n`;
205
- content += ` select(target: any, value: string): import('@testspectra/matchers').SingleElementRunner;\n`;
206
- content += ` hover(target: any): import('@testspectra/matchers').SingleElementRunner;\n`;
207
- content += ` pressKey(key: import('@testspectra/matchers').KeyOption | string): import('@testspectra/matchers').SingleElementRunner;\n`;
208
- content += ` dragDrop(sourceTarget: any, destTarget: any): import('@testspectra/matchers').SingleElementRunner;\n`;
209
- content += ` scroll(options?: import('@testspectra/matchers').ScrollOptions): import('@testspectra/matchers').SingleElementRunner;\n`;
210
- content += ` swipe(options: import('@testspectra/matchers').SwipeOptions): import('@testspectra/matchers').SingleElementRunner;\n`;
211
- content += ` wait(durationMs: number): import('@testspectra/matchers').SingleElementRunner;\n`;
212
- content += ` waitForElement(target: any, timeoutMs?: number): import('@testspectra/matchers').SingleElementRunner;\n`;
213
- content += `}\n\n`;
214
194
  content += `interface TestSpectraCustomActions {\n`;
215
195
  const declaredActions = new Set();
216
196
  for (const actionsDir of actionsDirs) {
@@ -262,7 +242,7 @@ export class TypeGenerator {
262
242
  }
263
243
  }
264
244
  content += `}\n`;
265
- content += `type TestSpectraActions = SpectraStaticInstance & TestSpectraCustomActions;\n`;
245
+ content += `type TestSpectraActions = import('@testspectra/matchers').SpectraStatic & TestSpectraCustomActions;\n`;
266
246
  content += `declare const Spectra: TestSpectraActions;\n\n`;
267
247
  // 3. Built-in Steps Declaration
268
248
  content += `interface TestSpectraSteps {\n`;
@@ -327,20 +307,31 @@ export class TypeGenerator {
327
307
  content += `// Managed automatically by TestSpectra Language Service Plugin.\n\n`;
328
308
  content += `interface TestSpectraFixtures {\n`;
329
309
  const declaredFixtures = new Set();
330
- for (const fixturesDir of fixturesDirs) {
331
- if (!fs.existsSync(fixturesDir))
332
- continue;
333
- const fixtureFiles = fs.readdirSync(fixturesDir);
334
- for (const file of fixtureFiles) {
335
- if (file.startsWith("."))
336
- continue;
337
- const parsed = path.parse(file);
338
- const varName = parsed.name.replace(/[^a-zA-Z0-9_$]/g, "_");
339
- if (declaredFixtures.has(varName))
340
- continue;
341
- content += ` readonly ${varName}: string;\n`;
342
- declaredFixtures.add(varName);
310
+ function scanFixtureDir(dir) {
311
+ if (!fs.existsSync(dir))
312
+ return;
313
+ try {
314
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
315
+ for (const entry of entries) {
316
+ if (entry.name.startsWith("."))
317
+ continue;
318
+ if (entry.isDirectory()) {
319
+ scanFixtureDir(path.join(dir, entry.name));
320
+ }
321
+ else if (entry.isFile()) {
322
+ const parsed = path.parse(entry.name);
323
+ const varName = parsed.name.replace(/[^a-zA-Z0-9_$]/g, "_");
324
+ if (declaredFixtures.has(varName))
325
+ continue;
326
+ content += ` readonly ${varName}: string;\n`;
327
+ declaredFixtures.add(varName);
328
+ }
329
+ }
343
330
  }
331
+ catch { }
332
+ }
333
+ for (const fixturesDir of fixturesDirs) {
334
+ scanFixtureDir(fixturesDir);
344
335
  }
345
336
  content += `}\n`;
346
337
  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.43",
3
+ "version": "1.0.45",
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.43",
24
+ "@testspectra/matchers": "^1.0.45",
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.43",
12
+ "@testspectra/matchers": "^1.0.45",
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.43",
14
+ "@testspectra/matchers": "^1.0.45",
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",