genat-mcp 2.2.5 → 2.2.6
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/README.md +8 -0
- package/detect-framework.js +145 -28
- package/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,14 @@ npm install /path/to/n8n_playwright_tests/mcp-genat
|
|
|
40
40
|
| **N8N_ANALYZE_STATES** | No | (off) | Set to `1`, `true`, or `yes` to analyze dropdown/combobox expanded states and include state-specific violations in generated tests. Used as default when the tool is invoked without `analyzeStates`; also used by run-genat.mjs. Increases analysis time. |
|
|
41
41
|
| **GENAT_PROJECT_ROOT** | No | (cwd) | Base path for resolving `parentProjectFolder`. When set, `parentProjectFolder` is resolved relative to this path. Use when your project is outside the MCP's working directory. Example: if your pytest project is at `/Users/me/projects/ill_tests`, set `GENAT_PROJECT_ROOT=/Users/me/projects` and use `parentProjectFolder ill_tests` in the prompt. |
|
|
42
42
|
|
|
43
|
+
## Framework detection and auto-discovery
|
|
44
|
+
|
|
45
|
+
GenAT detects **language** (TypeScript, JavaScript, Python), **BDD framework** (Cucumber, pytest-bdd, Behave), and **Page Object pattern** from the project folder.
|
|
46
|
+
|
|
47
|
+
**Auto-discovery:** When you pass a folder that contains both tooling (e.g. `package.json` at root) and a test subfolder (`tests/`, `e2e/`, `tests/e2e/`, `tests/accessibility/`), GenAT scores each candidate and uses the folder with the most test-specific content (`.feature` files, `conftest.py`, `*_test.py`, `.spec.ts`, `pages/`, `steps/`, etc.). This ensures correct detection when tests live in subfolders.
|
|
48
|
+
|
|
49
|
+
**Path guidance:** Pass the project root (e.g. `.` or `/path/to/project`). GenAT will auto-discover the best folder when tests are in `tests/` or `e2e/`. For monorepos, pass the specific test project folder (e.g. `packages/e2e-tests`) when the root has mixed content.
|
|
50
|
+
|
|
43
51
|
## Cursor MCP config
|
|
44
52
|
|
|
45
53
|
**Using npx (local install):**
|
package/detect-framework.js
CHANGED
|
@@ -7,12 +7,62 @@ import { join, resolve } from 'path';
|
|
|
7
7
|
|
|
8
8
|
const ALLOWED_BDD = ['cucumber', 'pytest-bdd', 'behave', 'none'];
|
|
9
9
|
|
|
10
|
+
const SKIP_DIRS = ['node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build', 'coverage'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Score a directory for test-specific content (higher = more test content).
|
|
14
|
+
* @param {string} dir
|
|
15
|
+
* @param {(d: string, p: (f: string) => boolean) => string[]} w
|
|
16
|
+
* @returns {number}
|
|
17
|
+
*/
|
|
18
|
+
function scoreTestContent(dir, w) {
|
|
19
|
+
let score = 0;
|
|
20
|
+
if (existsSync(join(dir, 'pages'))) score += 2;
|
|
21
|
+
if (existsSync(join(dir, 'page_objects'))) score += 2;
|
|
22
|
+
if (existsSync(join(dir, 'steps'))) score += 2;
|
|
23
|
+
if (existsSync(join(dir, 'features'))) score += 2;
|
|
24
|
+
score += w(dir, (f) => f.endsWith('.feature')).length;
|
|
25
|
+
score += w(dir, (f) => f === 'conftest.py').length;
|
|
26
|
+
score += w(dir, (f) => f.endsWith('_test.py')).length;
|
|
27
|
+
score += w(dir, (f) => f.endsWith('.spec.ts')).length;
|
|
28
|
+
score += w(dir, (f) => f.endsWith('.spec.js')).length;
|
|
29
|
+
return score;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Auto-discover best project root when tests live in subfolders.
|
|
34
|
+
* @param {string} root - resolved path passed by user
|
|
35
|
+
* @returns {string} - root or subfolder with higher test content score
|
|
36
|
+
*/
|
|
37
|
+
function findBestProjectRoot(root) {
|
|
38
|
+
const candidates = [root];
|
|
39
|
+
const subdirs = ['tests', 'e2e', 'tests/e2e', 'tests/accessibility'];
|
|
40
|
+
for (const sub of subdirs) {
|
|
41
|
+
const p = join(root, sub);
|
|
42
|
+
if (existsSync(p)) candidates.push(p);
|
|
43
|
+
}
|
|
44
|
+
if (candidates.length === 1) return root;
|
|
45
|
+
|
|
46
|
+
const w = (d, pred) => walk(d, pred);
|
|
47
|
+
let best = root;
|
|
48
|
+
let bestScore = scoreTestContent(root, w);
|
|
49
|
+
for (const c of candidates) {
|
|
50
|
+
if (c === root) continue;
|
|
51
|
+
const s = scoreTestContent(c, w);
|
|
52
|
+
if (s > bestScore) {
|
|
53
|
+
bestScore = s;
|
|
54
|
+
best = c;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return best;
|
|
58
|
+
}
|
|
59
|
+
|
|
10
60
|
/**
|
|
11
61
|
* @param {string} parentProjectFolder - absolute or relative path to project root
|
|
12
62
|
* @returns {{ scriptType: 'typescript'|'javascript'|'python', bddFramework: string, pageObject: boolean, projectSummary: string }}
|
|
13
63
|
*/
|
|
14
64
|
export function detectFramework(parentProjectFolder) {
|
|
15
|
-
|
|
65
|
+
let root = resolve(parentProjectFolder);
|
|
16
66
|
if (!existsSync(root) || !readdirSync(root).length) {
|
|
17
67
|
return {
|
|
18
68
|
scriptType: 'typescript',
|
|
@@ -22,6 +72,8 @@ export function detectFramework(parentProjectFolder) {
|
|
|
22
72
|
};
|
|
23
73
|
}
|
|
24
74
|
|
|
75
|
+
root = findBestProjectRoot(root);
|
|
76
|
+
|
|
25
77
|
let scriptType = 'typescript';
|
|
26
78
|
let bddFramework = 'none';
|
|
27
79
|
let pageObject = false;
|
|
@@ -48,14 +100,39 @@ export function detectFramework(parentProjectFolder) {
|
|
|
48
100
|
|
|
49
101
|
// When both package.json and pyproject.toml exist, prefer Python if pyproject indicates Python project
|
|
50
102
|
let pyprojectIndicatesPython = false;
|
|
103
|
+
let playwrightInPython = false;
|
|
51
104
|
if (hasPyproject) {
|
|
52
105
|
try {
|
|
53
106
|
const pyproject = readFileSync(join(root, 'pyproject.toml'), 'utf8');
|
|
54
107
|
pyprojectIndicatesPython = /\[project\]/.test(pyproject) && (/pytest/i.test(pyproject) || /pytest-bdd/i.test(pyproject));
|
|
108
|
+
playwrightInPython = /playwright/i.test(pyproject);
|
|
109
|
+
} catch (_) {}
|
|
110
|
+
}
|
|
111
|
+
if (hasRequirements && !playwrightInPython) {
|
|
112
|
+
try {
|
|
113
|
+
playwrightInPython = /playwright/i.test(readFileSync(join(root, 'requirements.txt'), 'utf8'));
|
|
114
|
+
} catch (_) {}
|
|
115
|
+
}
|
|
116
|
+
for (const sub of ['tests', 'e2e']) {
|
|
117
|
+
const p = join(root, sub, 'requirements.txt');
|
|
118
|
+
if (existsSync(p) && !playwrightInPython) {
|
|
119
|
+
try {
|
|
120
|
+
playwrightInPython = /playwright/i.test(readFileSync(p, 'utf8'));
|
|
121
|
+
if (playwrightInPython) break;
|
|
122
|
+
} catch (_) {}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let playwrightInNode = false;
|
|
127
|
+
if (hasPackageJson) {
|
|
128
|
+
try {
|
|
129
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
130
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
131
|
+
playwrightInNode = Object.keys(deps).some((k) => /@playwright\/test|^playwright$/i.test(k));
|
|
55
132
|
} catch (_) {}
|
|
56
133
|
}
|
|
57
134
|
|
|
58
|
-
if ((hasPy || hasConftest || pyprojectIndicatesPython) && (hasRequirements || hasPyproject || hasPy || hasConftest)) {
|
|
135
|
+
if ((hasPy || hasConftest || pyprojectIndicatesPython) && (hasRequirements || hasPyproject || hasPy || hasConftest) && !(playwrightInNode && !playwrightInPython && !hasPy && !hasConftest)) {
|
|
59
136
|
scriptType = 'python';
|
|
60
137
|
summaryParts.push('Language: Python');
|
|
61
138
|
} else if (hasPackageJson || hasTsConfig || hasTs || hasJs) {
|
|
@@ -72,27 +149,58 @@ export function detectFramework(parentProjectFolder) {
|
|
|
72
149
|
}
|
|
73
150
|
|
|
74
151
|
// BDD: Cucumber (package.json), pytest-bdd, Behave
|
|
75
|
-
|
|
152
|
+
function hasCucumberInPackage(pkgPath) {
|
|
76
153
|
try {
|
|
77
|
-
const pkg = JSON.parse(readFileSync(
|
|
154
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
78
155
|
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
156
|
+
const keys = Object.keys(deps);
|
|
157
|
+
return keys.some((k) => /@cucumber\/cucumber|cucumber-js|cucumber/i.test(k));
|
|
158
|
+
} catch (_) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if ((scriptType === 'typescript' || scriptType === 'javascript') && hasPackageJson) {
|
|
163
|
+
if (hasCucumberInPackage(join(root, 'package.json'))) {
|
|
164
|
+
bddFramework = 'cucumber';
|
|
165
|
+
summaryParts.push('BDD: Cucumber');
|
|
166
|
+
} else {
|
|
167
|
+
for (const sub of ['tests', 'e2e', 'tests/e2e']) {
|
|
168
|
+
const p = join(root, sub, 'package.json');
|
|
169
|
+
if (existsSync(p) && hasCucumberInPackage(p)) {
|
|
170
|
+
bddFramework = 'cucumber';
|
|
171
|
+
summaryParts.push('BDD: Cucumber (subfolder)');
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
82
174
|
}
|
|
83
|
-
}
|
|
175
|
+
}
|
|
84
176
|
}
|
|
85
177
|
const hasFeatureFiles = hasExtension(root, '.feature');
|
|
86
178
|
if (scriptType === 'python') {
|
|
87
179
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
180
|
+
function readReq(path) {
|
|
181
|
+
try {
|
|
182
|
+
return readFileSync(path, 'utf8');
|
|
183
|
+
} catch (_) {
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const reqPaths = [join(root, 'requirements.txt')];
|
|
188
|
+
for (const sub of ['tests', 'e2e', 'tests/e2e']) {
|
|
189
|
+
const p = join(root, sub, 'requirements.txt');
|
|
190
|
+
if (existsSync(p)) reqPaths.push(p);
|
|
191
|
+
}
|
|
192
|
+
for (const reqPath of reqPaths) {
|
|
193
|
+
if (!existsSync(reqPath)) continue;
|
|
194
|
+
const req = readReq(reqPath);
|
|
90
195
|
if (/pytest-bdd/i.test(req)) {
|
|
91
196
|
bddFramework = 'pytest-bdd';
|
|
92
197
|
summaryParts.push('BDD: pytest-bdd');
|
|
93
|
-
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
if (/behave/i.test(req)) {
|
|
94
201
|
bddFramework = 'behave';
|
|
95
202
|
summaryParts.push('BDD: Behave');
|
|
203
|
+
break;
|
|
96
204
|
}
|
|
97
205
|
}
|
|
98
206
|
if (bddFramework === 'none' && hasPyproject) {
|
|
@@ -132,7 +240,8 @@ export function detectFramework(parentProjectFolder) {
|
|
|
132
240
|
} catch (_) {}
|
|
133
241
|
}
|
|
134
242
|
if ((scriptType === 'typescript' || scriptType === 'javascript') && hasFeatureFiles && bddFramework === 'none') {
|
|
135
|
-
|
|
243
|
+
bddFramework = 'cucumber';
|
|
244
|
+
summaryParts.push('BDD: Cucumber (from .feature files)');
|
|
136
245
|
}
|
|
137
246
|
|
|
138
247
|
// Page Object: pages/, page_objects/, or *Page.ts, *Page.js, *_page.py, *page.py
|
|
@@ -141,8 +250,8 @@ export function detectFramework(parentProjectFolder) {
|
|
|
141
250
|
try {
|
|
142
251
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
143
252
|
for (const e of entries) {
|
|
144
|
-
if (e.isDirectory() && (e.name === 'pages' || e.name === 'page_objects')) return true;
|
|
145
|
-
if (e.isDirectory() && !
|
|
253
|
+
if (e.isDirectory() && (e.name === 'pages' || e.name === 'page_objects' || e.name === 'support' || e.name === 'locators')) return true;
|
|
254
|
+
if (e.isDirectory() && !SKIP_DIRS.includes(e.name)) {
|
|
146
255
|
if (findPagesDir(join(dir, e.name))) return true;
|
|
147
256
|
}
|
|
148
257
|
}
|
|
@@ -155,16 +264,22 @@ export function detectFramework(parentProjectFolder) {
|
|
|
155
264
|
} else if (existsSync(join(root, 'page_objects'))) {
|
|
156
265
|
pageObject = true;
|
|
157
266
|
summaryParts.push('Page Object: page_objects/ directory');
|
|
267
|
+
} else if (existsSync(join(root, 'support'))) {
|
|
268
|
+
pageObject = true;
|
|
269
|
+
summaryParts.push('Page Object: support/ directory');
|
|
270
|
+
} else if (existsSync(join(root, 'locators'))) {
|
|
271
|
+
pageObject = true;
|
|
272
|
+
summaryParts.push('Page Object: locators/ directory');
|
|
158
273
|
} else if (hasPagesInSubdir) {
|
|
159
274
|
pageObject = true;
|
|
160
|
-
summaryParts.push('Page Object: pages/ or page_objects/ in subdir');
|
|
275
|
+
summaryParts.push('Page Object: pages/ or page_objects/ or support/ in subdir');
|
|
161
276
|
} else {
|
|
162
|
-
const pageTs = walk(root, (f) => f.endsWith('Page.ts') || f.endsWith('Page.tsx'));
|
|
163
|
-
const pageJs = walk(root, (f) => f.endsWith('Page.js') || f.endsWith('Page.jsx'));
|
|
277
|
+
const pageTs = walk(root, (f) => f.endsWith('Page.ts') || f.endsWith('Page.tsx') || f.endsWith('page.ts') || f.endsWith('page.tsx'));
|
|
278
|
+
const pageJs = walk(root, (f) => f.endsWith('Page.js') || f.endsWith('Page.jsx') || f.endsWith('page.js') || f.endsWith('page.jsx'));
|
|
164
279
|
const pagePy = walk(root, (f) => f.endsWith('_page.py') || f.endsWith('Page.py') || f.endsWith('page.py'));
|
|
165
280
|
if (pageTs.length || pageJs.length || pagePy.length) {
|
|
166
281
|
pageObject = true;
|
|
167
|
-
summaryParts.push('Page Object: *Page.ts / *
|
|
282
|
+
summaryParts.push('Page Object: *Page.ts / *page.ts / *_page.py files');
|
|
168
283
|
}
|
|
169
284
|
}
|
|
170
285
|
} catch (_) {}
|
|
@@ -195,7 +310,7 @@ function walkWithPaths(dir, predicate, acc = []) {
|
|
|
195
310
|
for (const e of entries) {
|
|
196
311
|
const full = join(dir, e.name);
|
|
197
312
|
if (e.isDirectory()) {
|
|
198
|
-
if (
|
|
313
|
+
if (!SKIP_DIRS.includes(e.name)) {
|
|
199
314
|
walkWithPaths(full, predicate, acc);
|
|
200
315
|
}
|
|
201
316
|
} else if (predicate(e.name)) {
|
|
@@ -213,16 +328,18 @@ function walkWithPaths(dir, predicate, acc = []) {
|
|
|
213
328
|
* @returns {string[]}
|
|
214
329
|
*/
|
|
215
330
|
function walk(dir, predicate, acc = []) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
if (e.
|
|
221
|
-
|
|
331
|
+
try {
|
|
332
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
333
|
+
for (const e of entries) {
|
|
334
|
+
const full = join(dir, e.name);
|
|
335
|
+
if (e.isDirectory()) {
|
|
336
|
+
if (!SKIP_DIRS.includes(e.name)) {
|
|
337
|
+
walk(full, predicate, acc);
|
|
338
|
+
}
|
|
339
|
+
} else if (predicate(e.name)) {
|
|
340
|
+
acc.push(e.name);
|
|
222
341
|
}
|
|
223
|
-
} else if (predicate(e.name)) {
|
|
224
|
-
acc.push(e.name);
|
|
225
342
|
}
|
|
226
|
-
}
|
|
343
|
+
} catch (_) {}
|
|
227
344
|
return acc;
|
|
228
345
|
}
|
package/index.js
CHANGED
package/package.json
CHANGED