genat-mcp 2.2.7 → 2.2.8

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 CHANGED
@@ -4,6 +4,8 @@ MCP server that exposes a **GenAT** tool: generate accessibility tests for a URL
4
4
 
5
5
  Full setup (n8n workflow, services, usage) is in the [project README](../README.md).
6
6
 
7
+ **For teams:** See [GENAT_OVERVIEW.md](GENAT_OVERVIEW.md) for a concise, presentation-ready overview. Suitable as a source for NotebookLM or other AI tools to create slide decks.
8
+
7
9
  ## Install
8
10
 
9
11
  **If the package is published to npm** (otherwise you get 404):
@@ -49,6 +51,8 @@ GenAT detects **language** (TypeScript, JavaScript, Python), **BDD framework** (
49
51
 
50
52
  **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.
51
53
 
54
+ **pytest-bdd (Python):** When `scriptType` is Python and `bddFramework` is `pytest-bdd`, detection also scans `conftest.py` for common Playwright fixtures (`browser_page`, `page`, `base_url`, etc.) and prefers `features/` or `tests/features/` and `step_defs/` or `steps/` for layout hints. Those hints are sent as `projectLayoutHints` in the webhook body (and merged into `projectSummary`) so the n8n workflow can steer the model. Generated code uses **axe-playwright-python** (`from axe_playwright_python.sync_playwright import Axe`); ensure `pip install axe-playwright-python pytest-bdd pytest-playwright` (or your lockfile equivalent) in the project.
55
+
52
56
  ## Cursor MCP config
53
57
 
54
58
  **Using npx (local install):**
@@ -156,6 +160,7 @@ If the global binary is not on PATH, use the full path, e.g. `node $(npm root -g
156
160
 
157
161
  - **Node.js** 20+
158
162
  - **n8n** with a GenAT workflow imported and activated. Use [genat-accessibility-tests-with-login-jira.json](../workflows/genat-accessibility-tests-with-login-jira.json) (path `webhook-test/webhook-genat-login-jira`) for JIRA support, or [genat-accessibility-tests-with-login.json](../workflows/genat-accessibility-tests-with-login.json) (path `webhook-test/webhook-genat-login`). Set **N8N_WEBHOOK_URL** to the workflow's webhook URL.
163
+ - **Optional v2 pipeline (Python / pytest-bdd quality):** Import [GenAT Accessibility Tests (with Login + JIRA) v2.json](../workflows/GenAT%20Accessibility%20Tests%20(with%20Login%20+%20JIRA)%20v2.json) and set **N8N_WEBHOOK_URL** to `…/webhook-test/webhook-genat-login-jira-v2`. v2 adds stricter pytest-bdd + Python rules (no nested markdown in code, layout hints, fixture-aware steps) and can run alongside the original workflow. The original v1 workflow is unchanged.
159
164
  - **DOM Analyzer** and **Accessibility Analyzer** services running (see main README: `npm run services` from repo root)
160
165
 
161
166
  ## Standalone script: run-genat.mjs
@@ -206,7 +211,7 @@ When creating run-genat.mjs: Use webhook-genat-login, NOT webhook-genat. Prefer
206
211
 
207
212
  - **url** (string): Page URL to analyze for accessibility.
208
213
  - **parentProjectFolder** (string): Path to the project root (TypeScript, JavaScript, or Python Playwright).
209
- - **writeToProject** (boolean, optional): If `true`, write generated files under `tests/accessibility/` in the project folder.
214
+ - **writeToProject** (boolean, optional): If `true`, write generated files under `tests/accessibility/` by default; for **pytest-bdd** Python projects, files go under `features/` / `step_defs/` (or `tests/features`, `steps`, etc.) when those directories exist.
210
215
  - **maxAssertions** (number, optional): Override default assertion count for complex pages (e.g. `25`).
211
216
  - **scopeToRegions** (string[], optional): Limit tests to specific regions (e.g. `["header", "main", "table"]`).
212
217
  - **analyzeStates** (boolean, optional): If `true`, run axe in expanded dropdown/combobox states and include state-specific violations. Increases analysis time.
@@ -57,9 +57,61 @@ function findBestProjectRoot(root) {
57
57
  return best;
58
58
  }
59
59
 
60
+ /**
61
+ * Layout + conftest hints for pytest-bdd Python projects (empty string otherwise).
62
+ * @param {string} root
63
+ * @param {string} scriptType
64
+ * @param {string} bddFramework
65
+ * @returns {string}
66
+ */
67
+ function collectProjectLayoutHints(root, scriptType, bddFramework) {
68
+ if (scriptType !== 'python' || bddFramework !== 'pytest-bdd') return '';
69
+
70
+ const parts = [];
71
+ if (existsSync(join(root, 'features'))) {
72
+ parts.push('Put new .feature files under features/');
73
+ } else if (existsSync(join(root, 'tests', 'features'))) {
74
+ parts.push('Put new .feature files under tests/features/');
75
+ } else {
76
+ parts.push('Prefer features/ at project root for Gherkin (.feature) files');
77
+ }
78
+
79
+ if (existsSync(join(root, 'step_defs'))) {
80
+ parts.push('Put step definitions under step_defs/ (e.g. accessibility_steps.py)');
81
+ } else if (existsSync(join(root, 'steps'))) {
82
+ parts.push('Put step definitions under steps/');
83
+ } else if (existsSync(join(root, 'tests', 'step_defs'))) {
84
+ parts.push('Put step definitions under tests/step_defs/');
85
+ } else {
86
+ parts.push('Prefer step_defs/ for pytest-bdd step modules');
87
+ }
88
+
89
+ const conftestPaths = walkWithPaths(root, (f) => f === 'conftest.py');
90
+ const fixtureNames = new Set();
91
+ for (const p of conftestPaths.slice(0, 3)) {
92
+ try {
93
+ const content = readFileSync(p, 'utf8');
94
+ const re = /def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
95
+ let m;
96
+ while ((m = re.exec(content)) !== null) {
97
+ const name = m[1];
98
+ if (['page', 'browser_page', 'base_url', 'context', 'browser', 'playwright', 'request'].includes(name) || /_page$/.test(name)) {
99
+ fixtureNames.add(name);
100
+ }
101
+ }
102
+ } catch (_) {}
103
+ }
104
+ if (fixtureNames.size) {
105
+ parts.push(`Use these pytest fixtures from conftest (parameter names): ${[...fixtureNames].join(', ')}`);
106
+ }
107
+
108
+ parts.push('Dependencies: pip install axe-playwright-python pytest-bdd pytest-playwright');
109
+ return parts.join('; ');
110
+ }
111
+
60
112
  /**
61
113
  * @param {string} parentProjectFolder - absolute or relative path to project root
62
- * @returns {{ scriptType: 'typescript'|'javascript'|'python', bddFramework: string, pageObject: boolean, projectSummary: string }}
114
+ * @returns {{ scriptType: 'typescript'|'javascript'|'python', bddFramework: string, pageObject: boolean, projectSummary: string, projectLayoutHints?: string }}
63
115
  */
64
116
  export function detectFramework(parentProjectFolder) {
65
117
  let root = resolve(parentProjectFolder);
@@ -69,6 +121,7 @@ export function detectFramework(parentProjectFolder) {
69
121
  bddFramework: 'none',
70
122
  pageObject: false,
71
123
  projectSummary: `Folder not found or empty; using defaults. (resolved: ${root})`,
124
+ projectLayoutHints: '',
72
125
  };
73
126
  }
74
127
 
@@ -284,16 +337,18 @@ export function detectFramework(parentProjectFolder) {
284
337
  }
285
338
  } catch (_) {}
286
339
 
287
- const projectSummary = [
288
- ...(summaryParts.length ? summaryParts : ['No framework hints detected']),
289
- `(scanned: ${root})`,
290
- ].join('; ');
340
+ const projectLayoutHints = collectProjectLayoutHints(root, scriptType, bddFramework);
341
+ const summaryWithHints = [...(summaryParts.length ? summaryParts : ['No framework hints detected'])];
342
+ if (projectLayoutHints) summaryWithHints.push(projectLayoutHints);
343
+
344
+ const projectSummary = [...summaryWithHints, `(scanned: ${root})`].join('; ');
291
345
 
292
346
  return {
293
347
  scriptType,
294
348
  bddFramework: ALLOWED_BDD.includes(bddFramework) ? bddFramework : 'none',
295
349
  pageObject,
296
350
  projectSummary,
351
+ ...(projectLayoutHints ? { projectLayoutHints } : {}),
297
352
  };
298
353
  }
299
354
 
package/index.js CHANGED
@@ -55,7 +55,7 @@ function insecureHttpsFetch(url, { method = 'GET', headers = {}, body, signal })
55
55
  const server = new McpServer(
56
56
  {
57
57
  name: 'GenAT',
58
- version: '2.2.7',
58
+ version: '2.2.8',
59
59
  },
60
60
  {
61
61
  capabilities: {
@@ -158,6 +158,7 @@ server.registerTool(
158
158
  bddFramework: framework.bddFramework,
159
159
  pageObject: framework.pageObject,
160
160
  projectSummary: framework.projectSummary,
161
+ ...(framework.projectLayoutHints && { projectLayoutHints: framework.projectLayoutHints }),
161
162
  ...(SERVICE_BASE_URL && { serviceBaseUrl: SERVICE_BASE_URL }),
162
163
  ...(maxAssertionsResolved != null && !isNaN(maxAssertionsResolved) && { maxAssertions: maxAssertionsResolved }),
163
164
  ...(scopeToRegionsResolved?.length && { scopeToRegions: scopeToRegionsResolved }),
@@ -239,8 +240,9 @@ server.registerTool(
239
240
  }
240
241
 
241
242
  if (writeToProject && resolvedPath && data) {
243
+ if (data.bddFramework == null) data.bddFramework = framework.bddFramework;
242
244
  try {
243
- const written = writeGeneratedFiles(resolvedPath, data);
245
+ const written = await writeGeneratedFiles(resolvedPath, data);
244
246
  data._written = written;
245
247
  } catch (err) {
246
248
  data._writeError = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genat-mcp",
3
- "version": "2.2.7",
3
+ "version": "2.2.8",
4
4
  "mcpName": "io.github.asokans@oclc.org/genat",
5
5
  "description": "MCP server GenAT: generate accessibility tests via n8n workflow (url + project folder)",
6
6
  "type": "module",
package/run-genat.mjs CHANGED
@@ -66,7 +66,13 @@ async function main() {
66
66
  try {
67
67
  framework = detectFramework(parentProjectFolder);
68
68
  } catch (err) {
69
- framework = { scriptType: 'typescript', bddFramework: 'none', pageObject: false, projectSummary: '' };
69
+ framework = {
70
+ scriptType: 'typescript',
71
+ bddFramework: 'none',
72
+ pageObject: false,
73
+ projectSummary: '',
74
+ projectLayoutHints: '',
75
+ };
70
76
  }
71
77
 
72
78
  let login = {};
@@ -86,6 +92,7 @@ async function main() {
86
92
  bddFramework: framework.bddFramework,
87
93
  pageObject: framework.pageObject,
88
94
  projectSummary: framework.projectSummary,
95
+ ...(framework.projectLayoutHints && { projectLayoutHints: framework.projectLayoutHints }),
89
96
  ...(maxAssertions != null && !isNaN(maxAssertions) && { maxAssertions }),
90
97
  ...(scopeToRegions?.length && { scopeToRegions }),
91
98
  ...(analyzeStates && { analyzeStates: true }),
@@ -116,6 +123,7 @@ async function main() {
116
123
  }
117
124
 
118
125
  const data = await response.json();
126
+ if (data.bddFramework == null) data.bddFramework = framework.bddFramework;
119
127
  const written = await writeGeneratedFiles(parentProjectFolder, data);
120
128
  console.log('GenAT wrote:', written.join(', '));
121
129
  }
package/write-files.js CHANGED
@@ -2,14 +2,36 @@
2
2
  * Write generated accessibility test files into the project folder.
3
3
  * Uses suggested paths from the workflow response when present.
4
4
  */
5
+ import { existsSync } from 'fs';
5
6
  import { writeFile, mkdir } from 'fs/promises';
6
- import { join, resolve } from 'path';
7
+ import { dirname, join, resolve } from 'path';
7
8
 
8
9
  const DEFAULT_DIR = 'tests/accessibility';
9
10
 
11
+ /**
12
+ * @param {string} root
13
+ * @returns {{ featureRel: string, stepRel: string, specRel: string }}
14
+ */
15
+ function resolvePytestBddPaths(root) {
16
+ let featureRel;
17
+ if (existsSync(join(root, 'features'))) featureRel = join('features', 'accessibility.feature');
18
+ else if (existsSync(join(root, 'tests', 'features'))) featureRel = join('tests', 'features', 'accessibility.feature');
19
+ else featureRel = join('features', 'accessibility.feature');
20
+
21
+ let stepRel;
22
+ if (existsSync(join(root, 'step_defs'))) stepRel = join('step_defs', 'accessibility_steps.py');
23
+ else if (existsSync(join(root, 'steps'))) stepRel = join('steps', 'accessibility_steps.py');
24
+ else if (existsSync(join(root, 'tests', 'step_defs'))) stepRel = join('tests', 'step_defs', 'accessibility_steps.py');
25
+ else if (existsSync(join(root, 'tests', 'steps'))) stepRel = join('tests', 'steps', 'accessibility_steps.py');
26
+ else stepRel = join('step_defs', 'accessibility_steps.py');
27
+
28
+ const specRel = join(DEFAULT_DIR, 'accessibility_test.py');
29
+ return { featureRel, stepRel, specRel };
30
+ }
31
+
10
32
  /**
11
33
  * @param {string} parentProjectFolder - project root path
12
- * @param {{ testCode?: string, scriptType?: string, featureFile?: string, stepDefCode?: string, pageObjectCode?: string, suggestedPaths?: Record<string, string> }} data - workflow response
34
+ * @param {{ testCode?: string, scriptType?: string, bddFramework?: string, featureFile?: string, stepDefCode?: string, pageObjectCode?: string, suggestedPaths?: Record<string, string> }} data - workflow response
13
35
  * @returns {string[]} list of written file paths
14
36
  */
15
37
  export async function writeGeneratedFiles(parentProjectFolder, data) {
@@ -19,6 +41,8 @@ export async function writeGeneratedFiles(parentProjectFolder, data) {
19
41
 
20
42
  const written = [];
21
43
  const scriptType = (data.scriptType || 'typescript').toLowerCase();
44
+ const bddFramework = (data.bddFramework || 'none').toLowerCase();
45
+ const usePytestBddLayout = scriptType === 'python' && bddFramework === 'pytest-bdd';
22
46
 
23
47
  const defaultNames = {
24
48
  typescript: { spec: 'accessibility.spec.ts', steps: 'steps.ts', page: 'AccessibilityPage.ts' },
@@ -28,27 +52,92 @@ export async function writeGeneratedFiles(parentProjectFolder, data) {
28
52
  const names = defaultNames[scriptType] || defaultNames.typescript;
29
53
  const suggested = data.suggestedPaths || {};
30
54
 
55
+ if (usePytestBddLayout) {
56
+ const pb = resolvePytestBddPaths(root);
57
+ const dup =
58
+ data.testCode &&
59
+ data.stepDefCode &&
60
+ data.testCode.trim() === data.stepDefCode.trim();
61
+
62
+ if (data.featureFile) {
63
+ const relPath = suggested.feature || pb.featureRel;
64
+ const abs = join(root, relPath);
65
+ await mkdir(dirname(abs), { recursive: true });
66
+ await writeFile(abs, data.featureFile, 'utf8');
67
+ written.push(relPath);
68
+ }
69
+
70
+ if (dup) {
71
+ const relPath = suggested.stepDef || pb.stepRel;
72
+ const abs = join(root, relPath);
73
+ await mkdir(dirname(abs), { recursive: true });
74
+ await writeFile(abs, data.stepDefCode || data.testCode || '', 'utf8');
75
+ written.push(relPath);
76
+ } else {
77
+ if (data.stepDefCode) {
78
+ const relPath = suggested.stepDef || pb.stepRel;
79
+ const abs = join(root, relPath);
80
+ await mkdir(dirname(abs), { recursive: true });
81
+ await writeFile(abs, data.stepDefCode, 'utf8');
82
+ written.push(relPath);
83
+ }
84
+ if (data.testCode) {
85
+ if (data.stepDefCode) {
86
+ const relPath = suggested.spec || pb.specRel;
87
+ const abs = join(root, relPath);
88
+ await mkdir(dirname(abs), { recursive: true });
89
+ await writeFile(abs, data.testCode, 'utf8');
90
+ written.push(relPath);
91
+ } else {
92
+ const relPath = suggested.stepDef || pb.stepRel;
93
+ const abs = join(root, relPath);
94
+ await mkdir(dirname(abs), { recursive: true });
95
+ await writeFile(abs, data.testCode, 'utf8');
96
+ written.push(relPath);
97
+ }
98
+ }
99
+ }
100
+
101
+ if (data.pageObjectCode) {
102
+ const relPath = suggested.pageObject || join(DEFAULT_DIR, names.page);
103
+ const abs = join(root, relPath);
104
+ await mkdir(dirname(abs), { recursive: true });
105
+ await writeFile(abs, data.pageObjectCode, 'utf8');
106
+ written.push(relPath);
107
+ }
108
+
109
+ return written;
110
+ }
111
+
31
112
  if (data.testCode) {
32
113
  const relPath = suggested.spec || join(DEFAULT_DIR, names.spec);
33
- await writeFile(join(root, relPath), data.testCode, 'utf8');
114
+ const abs = join(root, relPath);
115
+ await mkdir(dirname(abs), { recursive: true });
116
+ await writeFile(abs, data.testCode, 'utf8');
34
117
  written.push(relPath);
35
118
  }
36
119
 
37
120
  if (data.featureFile) {
38
121
  const relPath = suggested.feature || join(DEFAULT_DIR, 'accessibility.feature');
39
- await writeFile(join(root, relPath), data.featureFile, 'utf8');
122
+ const abs = join(root, relPath);
123
+ await mkdir(dirname(abs), { recursive: true });
124
+ await writeFile(abs, data.featureFile, 'utf8');
40
125
  written.push(relPath);
41
126
  }
42
127
 
43
128
  if (data.stepDefCode) {
44
129
  const relPath = suggested.stepDef || join(DEFAULT_DIR, names.steps);
45
- await writeFile(join(root, relPath), data.stepDefCode, 'utf8');
130
+ const abs = join(root, relPath);
131
+ await mkdir(dirname(abs), { recursive: true });
132
+ await writeFile(abs, data.stepDefCode, 'utf8');
46
133
  written.push(relPath);
47
134
  }
48
135
 
49
136
  if (data.pageObjectCode) {
50
137
  const relPath = suggested.pageObject || join(DEFAULT_DIR, names.page);
51
- await writeFile(join(root, relPath), data.pageObjectCode, 'utf8');
138
+ const abs = join(root, relPath);
139
+ await mkdir(dirname(abs), { recursive: true });
140
+ await writeFile(abs, data.pageObjectCode, 'utf8');
52
141
  written.push(relPath);
53
142
  }
54
143