genat-mcp 2.3.2 → 2.3.4

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
@@ -212,7 +212,7 @@ When creating run-genat.mjs: Use webhook-genat-login, NOT webhook-genat. Prefer
212
212
 
213
213
  - **url** (string): Page URL to analyze for accessibility.
214
214
  - **parentProjectFolder** (string): Path to the project root (TypeScript, JavaScript, or Python Playwright).
215
- - **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.
215
+ - **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. New projects get **`step_defs/test_accessibility_steps.py`** when `accessibility_steps.py` is not already present.
216
216
  - **maxAssertions** (number, optional): Override default assertion count for complex pages (e.g. `25`).
217
217
  - **scopeToRegions** (string[], optional): Limit tests to specific regions (e.g. `["header", "main", "table"]`).
218
218
  - **analyzeStates** (boolean, optional): If `true`, run axe in expanded dropdown/combobox states and include state-specific violations. Increases analysis time.
@@ -225,8 +225,24 @@ Login credentials: provide in the prompt (loginUsername, loginPassword) or in pr
225
225
 
226
226
  Framework detection: language (TypeScript vs JavaScript vs Python), BDD (Cucumber, pytest-bdd, Behave), and Page Object pattern (e.g. `pages/` directory or `*Page.ts`, `*Page.js`, `*_page.py` files). Returns JSON with `testCode`, `featureFile`, `stepDefCode`, `pageObjectCode` (and `_written` paths when writing).
227
227
 
228
+ **Response fields for calling applications:** When `stepDefCode` is present, the MCP may set **`_stepDefIncomplete`** (boolean) and **`_stepDefIncompleteReason`** (string) using a lightweight heuristic (unclosed strings/delimiters, line continuation at EOF). Treat `true` as a signal to warn the user, retry generation, or block merge—do not assume the step file is runnable. Other optional fields from the workflow include **`usedLogin`** and **`loginReason`** when using login-capable workflows.
229
+
228
230
  Generated tests include keyboard accessibility checks (Tab order, focus) by default. For complex pages, the workflow uses page complexity (regions, tables, widgets) to scale assertion count.
229
231
 
232
+ ### Feature ↔ step parity (consumer CI)
233
+
234
+ GenAT does not guarantee that every Gherkin line has an implemented step. For **pytest-bdd** projects, the calling application or CI can:
235
+
236
+ 1. Parse `*.feature` step lines (after `Given`/`When`/`Then`/`And`/`But`).
237
+ 2. Confirm `test_accessibility_steps.py` or `accessibility_steps.py` (your step module) defines matching `@given`/`@when`/`@then` patterns (or run `pytest --collect-only` / a dry run).
238
+
239
+ Keep this as an optional pre-merge check alongside `_stepDefIncomplete`.
240
+
241
+ ### pytest-bdd discovery and axe-playwright-python
242
+
243
+ - **Pytest** collects `test_*.py` by default. GenAT now prefers writing **`step_defs/test_accessibility_steps.py`** for new projects; if **`accessibility_steps.py`** already exists, that path is used. Ensure **`@scenario`** test functions are in a collected module (often the same file when named `test_*.py`) or import step defs from **`conftest.py`** / **`pytest_plugins`**.
244
+ - **axe-playwright-python (sync):** `from axe_playwright_python.sync_playwright import Axe`; `axe = Axe()`; `results = axe.run(page)`; read **`results.response`** (axe-core JSON) and **`results.violations_count`**. Do not invent other APIs—see [axe-playwright-python docs](https://pamelafox.github.io/axe-playwright-python/).
245
+
230
246
  ## Sample prompts
231
247
 
232
248
  See [prompts.md](prompts.md) for a full list of prompts including specific parent folders, login, JIRA, and combined options.
@@ -77,15 +77,21 @@ function collectProjectLayoutHints(root, scriptType, bddFramework) {
77
77
  }
78
78
 
79
79
  if (existsSync(join(root, 'step_defs'))) {
80
- parts.push('Put step definitions under step_defs/ (e.g. accessibility_steps.py)');
80
+ parts.push(
81
+ 'Put step definitions under step_defs/; prefer test_accessibility_steps.py (test_*.py) so pytest discovers @scenario tests, or keep accessibility_steps.py and import that module from conftest.py'
82
+ );
81
83
  } else if (existsSync(join(root, 'steps'))) {
82
- parts.push('Put step definitions under steps/');
84
+ parts.push('Put step definitions under steps/; prefer test_*.py naming for pytest collection or import via conftest.py');
83
85
  } else if (existsSync(join(root, 'tests', 'step_defs'))) {
84
- parts.push('Put step definitions under tests/step_defs/');
86
+ parts.push('Put step definitions under tests/step_defs/; prefer test_accessibility_steps.py or conftest import');
85
87
  } else {
86
- parts.push('Prefer step_defs/ for pytest-bdd step modules');
88
+ parts.push('Prefer step_defs/ for pytest-bdd; use test_accessibility_steps.py for pytest discovery of scenario tests');
87
89
  }
88
90
 
91
+ parts.push(
92
+ 'Pytest collects test_*.py by default; ensure BDD scenario functions (@scenario) live in a collected module or are pulled in via conftest/pytest_plugins'
93
+ );
94
+
89
95
  const conftestPaths = walkWithPaths(root, (f) => f === 'conftest.py');
90
96
  const fixtureNames = new Set();
91
97
  for (const p of conftestPaths.slice(0, 3)) {
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Lightweight heuristic: detect likely truncated or syntactically incomplete pytest-bdd step Python.
3
+ * For calling apps to surface _stepDefIncomplete and retry or warn.
4
+ */
5
+
6
+ /**
7
+ * @param {string} code
8
+ * @returns {{ incomplete: boolean, reason: string | null }}
9
+ */
10
+ export function detectStepDefIncomplete(code) {
11
+ if (code == null || typeof code !== 'string') {
12
+ return { incomplete: false, reason: null };
13
+ }
14
+ const t = code.replace(/\r\n/g, '\n').trimEnd();
15
+ if (t.length === 0) {
16
+ return { incomplete: true, reason: 'empty' };
17
+ }
18
+
19
+ const triples = (t.match(/"""/g) || []).length;
20
+ if (triples % 2 !== 0) {
21
+ return { incomplete: true, reason: 'unclosed_triple_double_quoted_string' };
22
+ }
23
+ const tripleSingle = (t.match(/'''/g) || []).length;
24
+ if (tripleSingle % 2 !== 0) {
25
+ return { incomplete: true, reason: 'unclosed_triple_single_quoted_string' };
26
+ }
27
+
28
+ const lines = t.split('\n');
29
+ const lastLine = lines[lines.length - 1];
30
+ if (/\\\s*$/.test(lastLine)) {
31
+ return { incomplete: true, reason: 'line_continuation_at_eof' };
32
+ }
33
+
34
+ const depth = bracketDepthAtEnd(t);
35
+ if (depth !== 0) {
36
+ return { incomplete: true, reason: 'unclosed_delimiters' };
37
+ }
38
+
39
+ return { incomplete: false, reason: null };
40
+ }
41
+
42
+ /**
43
+ * Rough balance of (), [], {} at end of file, skipping strings and comments.
44
+ * @param {string} s
45
+ * @returns {number} 0 if balanced, non-zero if likely incomplete
46
+ */
47
+ function bracketDepthAtEnd(s) {
48
+ let i = 0;
49
+ const stack = [];
50
+ let state = 'code';
51
+
52
+ while (i < s.length) {
53
+ const c = s[i];
54
+
55
+ if (state === 'code') {
56
+ if (c === '#') {
57
+ while (i < s.length && s[i] !== '\n') i++;
58
+ continue;
59
+ }
60
+ if (s.slice(i, i + 3) === '"""') {
61
+ state = 'triple_double';
62
+ i += 3;
63
+ continue;
64
+ }
65
+ if (s.slice(i, i + 3) === "'''") {
66
+ state = 'triple_single';
67
+ i += 3;
68
+ continue;
69
+ }
70
+ if (c === '"' || c === "'") {
71
+ state = c === '"' ? 'string_double' : 'string_single';
72
+ i++;
73
+ continue;
74
+ }
75
+ if (c === '(') stack.push('(');
76
+ else if (c === '[') stack.push('[');
77
+ else if (c === '{') stack.push('{');
78
+ else if (c === ')' || c === ']' || c === '}') {
79
+ const want = c === ')' ? '(' : c === ']' ? '[' : '{';
80
+ if (stack.length === 0 || stack[stack.length - 1] !== want) {
81
+ return -1;
82
+ }
83
+ stack.pop();
84
+ }
85
+ i++;
86
+ continue;
87
+ }
88
+
89
+ if (state === 'string_single') {
90
+ if (c === '\\') {
91
+ i += 2;
92
+ continue;
93
+ }
94
+ if (c === "'") {
95
+ state = 'code';
96
+ }
97
+ i++;
98
+ continue;
99
+ }
100
+
101
+ if (state === 'string_double') {
102
+ if (c === '\\') {
103
+ i += 2;
104
+ continue;
105
+ }
106
+ if (c === '"') {
107
+ state = 'code';
108
+ }
109
+ i++;
110
+ continue;
111
+ }
112
+
113
+ if (state === 'triple_double') {
114
+ if (s.slice(i, i + 3) === '"""') {
115
+ state = 'code';
116
+ i += 3;
117
+ continue;
118
+ }
119
+ i++;
120
+ continue;
121
+ }
122
+
123
+ if (state === 'triple_single') {
124
+ if (s.slice(i, i + 3) === "'''") {
125
+ state = 'code';
126
+ i += 3;
127
+ continue;
128
+ }
129
+ i++;
130
+ continue;
131
+ }
132
+
133
+ i++;
134
+ }
135
+
136
+ if (state !== 'code') {
137
+ return 1;
138
+ }
139
+ return stack.length;
140
+ }
package/index.js CHANGED
@@ -11,6 +11,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
11
11
  import { z } from 'zod';
12
12
  import { detectFramework } from './detect-framework.js';
13
13
  import { detectLogin } from './detect-login.js';
14
+ import { detectStepDefIncomplete } from './detect-incomplete-step-def.js';
14
15
  import { finalizePytestBddStepDef } from './finalize-pytest-bdd-steps.js';
15
16
  import { resolvePytestBddPaths } from './pytest-bdd-paths.js';
16
17
  import { writeGeneratedFiles } from './write-files.js';
@@ -58,7 +59,7 @@ function insecureHttpsFetch(url, { method = 'GET', headers = {}, body, signal })
58
59
  const server = new McpServer(
59
60
  {
60
61
  name: 'GenAT',
61
- version: '2.3.2',
62
+ version: '2.3.4',
62
63
  },
63
64
  {
64
65
  capabilities: {
@@ -283,6 +284,12 @@ server.registerTool(
283
284
  if (data.testCode) data.testCode = finalizePytestBddStepDef(resolvedPath, data.testCode, pathOverrides);
284
285
  }
285
286
 
287
+ if (data.stepDefCode && typeof data.stepDefCode === 'string') {
288
+ const inc = detectStepDefIncomplete(data.stepDefCode);
289
+ data._stepDefIncomplete = inc.incomplete;
290
+ if (inc.reason) data._stepDefIncompleteReason = inc.reason;
291
+ }
292
+
286
293
  if (writeToProject && resolvedPath && data) {
287
294
  if (data.bddFramework == null) data.bddFramework = framework.bddFramework;
288
295
  try {
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "genat-mcp",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
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",
7
7
  "main": "index.js",
8
8
  "bin": { "genat-mcp": "index.js" },
9
9
  "engines": { "node": ">=20" },
10
- "files": ["index.js", "detect-framework.js", "detect-login.js", "write-files.js", "strip-gherkin-from-python-steps.js", "finalize-pytest-bdd-steps.js", "fix-pytest-bdd-scenario-path.js", "pytest-bdd-paths.js", "oclc-context.js", "oclc-accessibility-confluence-context.md", "run-genat.mjs", "rules/genat-mcp.mdc"],
10
+ "files": ["index.js", "detect-framework.js", "detect-login.js", "detect-incomplete-step-def.js", "write-files.js", "strip-gherkin-from-python-steps.js", "finalize-pytest-bdd-steps.js", "fix-pytest-bdd-scenario-path.js", "pytest-bdd-paths.js", "oclc-context.js", "oclc-accessibility-confluence-context.md", "run-genat.mjs", "rules/genat-mcp.mdc"],
11
11
  "keywords": ["mcp", "accessibility", "playwright", "n8n", "model-context-protocol", "a11y", "testing"],
12
12
  "repository": {
13
13
  "type": "git",
@@ -1,11 +1,29 @@
1
1
  /**
2
2
  * Resolve pytest-bdd feature / step / spec paths from project layout (same rules as write-files).
3
+ * Step file: prefer legacy accessibility_steps.py if present; otherwise test_accessibility_steps.py
4
+ * so pytest discovers scenario tests (test_*.py).
3
5
  */
4
6
  import { existsSync } from 'fs';
5
7
  import { join } from 'path';
6
8
 
7
9
  const DEFAULT_DIR = 'tests/accessibility';
8
10
 
11
+ const LEGACY_STEP = 'accessibility_steps.py';
12
+ const TEST_STEP = 'test_accessibility_steps.py';
13
+
14
+ /**
15
+ * @param {string} root
16
+ * @param {string[]} dirSegments - e.g. ['step_defs']
17
+ * @returns {string} relative path to step file
18
+ */
19
+ function pickStepFileUnderDir(root, dirSegments) {
20
+ const legacy = join(...dirSegments, LEGACY_STEP);
21
+ const testNamed = join(...dirSegments, TEST_STEP);
22
+ if (existsSync(join(root, legacy))) return legacy;
23
+ if (existsSync(join(root, testNamed))) return testNamed;
24
+ return testNamed;
25
+ }
26
+
9
27
  /**
10
28
  * @param {string} root
11
29
  * @returns {{ featureRel: string, stepRel: string, specRel: string }}
@@ -17,11 +35,11 @@ export function resolvePytestBddPaths(root) {
17
35
  else featureRel = join('features', 'accessibility.feature');
18
36
 
19
37
  let stepRel;
20
- if (existsSync(join(root, 'step_defs'))) stepRel = join('step_defs', 'accessibility_steps.py');
21
- else if (existsSync(join(root, 'steps'))) stepRel = join('steps', 'accessibility_steps.py');
22
- else if (existsSync(join(root, 'tests', 'step_defs'))) stepRel = join('tests', 'step_defs', 'accessibility_steps.py');
23
- else if (existsSync(join(root, 'tests', 'steps'))) stepRel = join('tests', 'steps', 'accessibility_steps.py');
24
- else stepRel = join('step_defs', 'accessibility_steps.py');
38
+ if (existsSync(join(root, 'step_defs'))) stepRel = pickStepFileUnderDir(root, ['step_defs']);
39
+ else if (existsSync(join(root, 'steps'))) stepRel = pickStepFileUnderDir(root, ['steps']);
40
+ else if (existsSync(join(root, 'tests', 'step_defs'))) stepRel = pickStepFileUnderDir(root, ['tests', 'step_defs']);
41
+ else if (existsSync(join(root, 'tests', 'steps'))) stepRel = pickStepFileUnderDir(root, ['tests', 'steps']);
42
+ else stepRel = join('step_defs', TEST_STEP);
25
43
 
26
44
  const specRel = join(DEFAULT_DIR, 'accessibility_test.py');
27
45
  return { featureRel, stepRel, specRel };