genat-mcp 2.2.7 → 2.3.0
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 -2
- package/detect-framework.js +60 -5
- package/index.js +7 -2
- package/oclc-accessibility-confluence-context.md +51 -0
- package/oclc-context.js +24 -0
- package/package.json +2 -2
- package/run-genat.mjs +13 -1
- package/write-files.js +95 -6
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
MCP server that exposes a **GenAT** tool: generate accessibility tests for a URL based on your project folder. It detects framework (TypeScript, JavaScript, or Python, BDD, Page Object), calls an n8n GenAT workflow, and returns (or writes) generated test files.
|
|
4
4
|
|
|
5
|
-
Full setup (n8n workflow, services, usage) is in the [project README](../README.md).
|
|
5
|
+
Full setup (n8n workflow, services, usage) is in the [project README](../README.md). GenAT workflows also instruct the AI to align with **OCLC internal Confluence** pages ([Accessibility Guidelines and Resources](https://confluence.oclc.org/spaces/UAD/pages/103062358/Accessibility+Guidelines+and+Resources), [OCLC Accessibility Release Blockers](https://confluence.oclc.org/spaces/UXUI/pages/119232333/OCLC+Accessibility+Release+Blockers)); the MCP sends optional extra context from [oclc-accessibility-confluence-context.md](oclc-accessibility-confluence-context.md) or `GENAT_OCLC_ACCESSIBILITY_CONTEXT`.
|
|
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.
|
|
6
8
|
|
|
7
9
|
## Install
|
|
8
10
|
|
|
@@ -40,6 +42,7 @@ npm install /path/to/n8n_playwright_tests/mcp-genat
|
|
|
40
42
|
| **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
43
|
| **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
44
|
| **GENAT_FETCH_TIMEOUT_MS** | No | 600000 (10 min) | Max time in milliseconds to wait for the n8n webhook response. Increase if GenAT times out before the workflow completes (e.g. complex pages with JIRA + AI Agent). Set in MCP env, e.g. `GENAT_FETCH_TIMEOUT_MS=900000` for 15 minutes. |
|
|
45
|
+
| **GENAT_OCLC_ACCESSIBILITY_CONTEXT** | No | (bundled file) | Optional multiline text sent as `oclcAccessibilityContext` in the webhook body. Overrides the default [oclc-accessibility-confluence-context.md](oclc-accessibility-confluence-context.md) (OCLC UAD guidelines + UXUI release blockers links and notes). Use to paste updated Confluence excerpts without republishing the package. |
|
|
43
46
|
|
|
44
47
|
## Framework detection and auto-discovery
|
|
45
48
|
|
|
@@ -49,6 +52,8 @@ GenAT detects **language** (TypeScript, JavaScript, Python), **BDD framework** (
|
|
|
49
52
|
|
|
50
53
|
**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
54
|
|
|
55
|
+
**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.
|
|
56
|
+
|
|
52
57
|
## Cursor MCP config
|
|
53
58
|
|
|
54
59
|
**Using npx (local install):**
|
|
@@ -156,6 +161,7 @@ If the global binary is not on PATH, use the full path, e.g. `node $(npm root -g
|
|
|
156
161
|
|
|
157
162
|
- **Node.js** 20+
|
|
158
163
|
- **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.
|
|
164
|
+
- **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
165
|
- **DOM Analyzer** and **Accessibility Analyzer** services running (see main README: `npm run services` from repo root)
|
|
160
166
|
|
|
161
167
|
## Standalone script: run-genat.mjs
|
|
@@ -206,7 +212,7 @@ When creating run-genat.mjs: Use webhook-genat-login, NOT webhook-genat. Prefer
|
|
|
206
212
|
|
|
207
213
|
- **url** (string): Page URL to analyze for accessibility.
|
|
208
214
|
- **parentProjectFolder** (string): Path to the project root (TypeScript, JavaScript, or Python Playwright).
|
|
209
|
-
- **writeToProject** (boolean, optional): If `true`, write generated files under `tests/accessibility/`
|
|
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.
|
|
210
216
|
- **maxAssertions** (number, optional): Override default assertion count for complex pages (e.g. `25`).
|
|
211
217
|
- **scopeToRegions** (string[], optional): Limit tests to specific regions (e.g. `["header", "main", "table"]`).
|
|
212
218
|
- **analyzeStates** (boolean, optional): If `true`, run axe in expanded dropdown/combobox states and include state-specific violations. Increases analysis time.
|
package/detect-framework.js
CHANGED
|
@@ -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
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
@@ -12,6 +12,7 @@ import { z } from 'zod';
|
|
|
12
12
|
import { detectFramework } from './detect-framework.js';
|
|
13
13
|
import { detectLogin } from './detect-login.js';
|
|
14
14
|
import { writeGeneratedFiles } from './write-files.js';
|
|
15
|
+
import { loadOclcAccessibilityContext } from './oclc-context.js';
|
|
15
16
|
|
|
16
17
|
const N8N_WEBHOOK_URL = process.env.N8N_WEBHOOK_URL || 'http://localhost:5678/webhook-test/webhook-genat-login';
|
|
17
18
|
const SERVICE_BASE_URL = process.env.SERVICE_BASE_URL || null;
|
|
@@ -55,7 +56,7 @@ function insecureHttpsFetch(url, { method = 'GET', headers = {}, body, signal })
|
|
|
55
56
|
const server = new McpServer(
|
|
56
57
|
{
|
|
57
58
|
name: 'GenAT',
|
|
58
|
-
version: '2.
|
|
59
|
+
version: '2.3.0',
|
|
59
60
|
},
|
|
60
61
|
{
|
|
61
62
|
capabilities: {
|
|
@@ -151,6 +152,7 @@ server.registerTool(
|
|
|
151
152
|
const maxAssertionsResolved = maxAssertions ?? (process.env.N8N_MAX_ASSERTIONS ? parseInt(process.env.N8N_MAX_ASSERTIONS, 10) : null);
|
|
152
153
|
const scopeToRegionsResolved = scopeToRegions?.length ? scopeToRegions : (process.env.N8N_SCOPE_TO_REGIONS ? process.env.N8N_SCOPE_TO_REGIONS.split(',').map((s) => s.trim()).filter(Boolean) : null);
|
|
153
154
|
const analyzeStatesResolved = analyzeStates || /^1|true|yes$/i.test(process.env.N8N_ANALYZE_STATES || '');
|
|
155
|
+
const oclcAccessibilityContext = loadOclcAccessibilityContext();
|
|
154
156
|
|
|
155
157
|
const body = {
|
|
156
158
|
url,
|
|
@@ -158,6 +160,8 @@ server.registerTool(
|
|
|
158
160
|
bddFramework: framework.bddFramework,
|
|
159
161
|
pageObject: framework.pageObject,
|
|
160
162
|
projectSummary: framework.projectSummary,
|
|
163
|
+
...(framework.projectLayoutHints && { projectLayoutHints: framework.projectLayoutHints }),
|
|
164
|
+
...(oclcAccessibilityContext && { oclcAccessibilityContext }),
|
|
161
165
|
...(SERVICE_BASE_URL && { serviceBaseUrl: SERVICE_BASE_URL }),
|
|
162
166
|
...(maxAssertionsResolved != null && !isNaN(maxAssertionsResolved) && { maxAssertions: maxAssertionsResolved }),
|
|
163
167
|
...(scopeToRegionsResolved?.length && { scopeToRegions: scopeToRegionsResolved }),
|
|
@@ -239,8 +243,9 @@ server.registerTool(
|
|
|
239
243
|
}
|
|
240
244
|
|
|
241
245
|
if (writeToProject && resolvedPath && data) {
|
|
246
|
+
if (data.bddFramework == null) data.bddFramework = framework.bddFramework;
|
|
242
247
|
try {
|
|
243
|
-
const written = writeGeneratedFiles(resolvedPath, data);
|
|
248
|
+
const written = await writeGeneratedFiles(resolvedPath, data);
|
|
244
249
|
data._written = written;
|
|
245
250
|
} catch (err) {
|
|
246
251
|
data._writeError = err instanceof Error ? err.message : String(err);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# OCLC internal accessibility sources (GenAT)
|
|
2
|
+
|
|
3
|
+
GenAT injects this file into the n8n workflow as **oclcAccessibilityContext** so generated tests align with internal guidance, in addition to WCAG 2.1 AA and related standards from the accessibility analyzer.
|
|
4
|
+
|
|
5
|
+
Source URLs (canonical):
|
|
6
|
+
|
|
7
|
+
- **UAD — Accessibility Guidelines and Resources:** https://confluence.oclc.org/spaces/UAD/pages/103062358/Accessibility+Guidelines+and+Resources
|
|
8
|
+
- **UXUI — OCLC Accessibility Release Blockers:** https://confluence.oclc.org/spaces/UXUI/pages/119232333/OCLC+Accessibility+Release+Blockers
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Summary: UAD Accessibility Guidelines and Resources
|
|
13
|
+
|
|
14
|
+
**Policy / Definition of done:** Net-new applications (new UI stack, e.g. React/Angular with Material UI) are expected to be accessible and internationalized. Interfaces must be usable and inclusive.
|
|
15
|
+
|
|
16
|
+
**Principles:** Follow **POUR** — Perceivable, Operable, Understandable, Robust. Accessibility is broader than screen-reader-only compliance.
|
|
17
|
+
|
|
18
|
+
**Build (high level):**
|
|
19
|
+
|
|
20
|
+
- Use Storybook with accessibility tooling where applicable.
|
|
21
|
+
- Valid, well-structured HTML; landmark regions; semantic elements (`main`, `nav`, headings, etc.) over generic `div`/`span` when defining structure.
|
|
22
|
+
- Relative sizing (**rem**/**em**), sensible line-height; avoid locking users out of zoom.
|
|
23
|
+
- Prefer **native** HTML controls (`button`, `a`, `input`, `select`, `form`) over custom reimplementations.
|
|
24
|
+
- Use `<a>` for navigation (with real `href`); use `<button>` for actions (style as link if needed).
|
|
25
|
+
- All images need appropriate **alt** text.
|
|
26
|
+
- Use **ARIA** correctly; incorrect ARIA can harm more than help.
|
|
27
|
+
- **Keyboard:** no traps; focus management after dynamic changes; follow WAI-ARIA authoring practices for composite widgets.
|
|
28
|
+
- Tab order: interactive controls in a logical sequence; avoid `tabindex` on non-interactive elements except focus management (`tabindex="-1"` when appropriate).
|
|
29
|
+
- Treat a11y issues in code review similarly to i18n/hardcoded strings.
|
|
30
|
+
|
|
31
|
+
**Verify:** Target **WCAG 2.1 AA** plus real-world usability. Combine automated checks (e.g. axe, Pa11y), linting (e.g. eslint-plugin-jsx-a11y), integration/unit tests for behaviors (e.g. modal focus trap), browser tools, and **manual** testing including screen readers (VoiceOver, NVDA) with realistic navigation patterns.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Summary: OCLC Accessibility Release Blockers
|
|
36
|
+
|
|
37
|
+
**Intent:** Distinguish **critical failures** that must block a release vs. issues that can be fixed post-release.
|
|
38
|
+
|
|
39
|
+
**The three release blockers** (minimum for OCLC interfaces):
|
|
40
|
+
|
|
41
|
+
1. **Keyboard operable** — Everything operable without a mouse; no keyboard traps. Primary WCAG tie-in: **2.1.1 Keyboard**. Related: meaningful sequence, no keyboard trap, bypass blocks, focus order, focus visible.
|
|
42
|
+
2. **High contrast** — Text **4.5:1**; non-text UI (icons, borders) **3:1** vs. background. Check hover/focus/selected states. Primary: **1.4.3 Contrast (Minimum)**. Related: use of color, non-text contrast.
|
|
43
|
+
3. **Clearly labeled** — Text alternatives for non-text content; forms and controls have clear, distinguishable names/labels. Primary: **1.1.1 Non-text Content**. Related: info/relationships, link purpose, headings/labels, labels/instructions, name/role/value.
|
|
44
|
+
|
|
45
|
+
OCLC aims for **WCAG 2.2 A and AA** broadly; prioritize **site-wide** issues (login, headers/footers, navigation, reusable components, contrast) when triaging.
|
|
46
|
+
|
|
47
|
+
**For GenAT:** Generated tests should explicitly exercise these three themes (keyboard paths, contrast-relevant assertions where feasible, labels/roles/names) in addition to axe rule coverage.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
Teams may override this file via env **`GENAT_OCLC_ACCESSIBILITY_CONTEXT`** to paste updated Confluence excerpts without republishing the package.
|
package/oclc-context.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional OCLC Confluence text appended to the n8n webhook body as oclcAccessibilityContext.
|
|
3
|
+
* Env GENAT_OCLC_ACCESSIBILITY_CONTEXT overrides the default file.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const DEFAULT_FILE = join(__dirname, 'oclc-accessibility-confluence-context.md');
|
|
11
|
+
const MAX_LEN = 120000;
|
|
12
|
+
|
|
13
|
+
export function loadOclcAccessibilityContext() {
|
|
14
|
+
const fromEnv = process.env.GENAT_OCLC_ACCESSIBILITY_CONTEXT?.trim();
|
|
15
|
+
if (fromEnv) return fromEnv.slice(0, MAX_LEN);
|
|
16
|
+
if (existsSync(DEFAULT_FILE)) {
|
|
17
|
+
try {
|
|
18
|
+
return readFileSync(DEFAULT_FILE, 'utf8').trim().slice(0, MAX_LEN);
|
|
19
|
+
} catch {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return '';
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genat-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
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", "run-genat.mjs", "rules/genat-mcp.mdc"],
|
|
10
|
+
"files": ["index.js", "detect-framework.js", "detect-login.js", "write-files.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",
|
package/run-genat.mjs
CHANGED
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
* N8N_MAX_ASSERTIONS - override assertion count (e.g. 25)
|
|
14
14
|
* N8N_SCOPE_TO_REGIONS - comma-separated regions (e.g. header,main,table)
|
|
15
15
|
* N8N_ANALYZE_STATES - set to 1 to analyze dropdown/combobox expanded states
|
|
16
|
+
* GENAT_OCLC_ACCESSIBILITY_CONTEXT - optional; overrides bundled oclc-accessibility-confluence-context.md for the workflow prompt
|
|
16
17
|
*/
|
|
17
18
|
import https from 'node:https';
|
|
18
19
|
import { resolve } from 'node:path';
|
|
19
20
|
import { detectFramework } from './detect-framework.js';
|
|
20
21
|
import { detectLogin } from './detect-login.js';
|
|
21
22
|
import { writeGeneratedFiles } from './write-files.js';
|
|
23
|
+
import { loadOclcAccessibilityContext } from './oclc-context.js';
|
|
22
24
|
|
|
23
25
|
const DEFAULT_WEBHOOK = 'http://localhost:5678/webhook-test/webhook-genat-login';
|
|
24
26
|
const N8N_WEBHOOK_URL = process.env.N8N_WEBHOOK_URL || DEFAULT_WEBHOOK;
|
|
@@ -66,7 +68,13 @@ async function main() {
|
|
|
66
68
|
try {
|
|
67
69
|
framework = detectFramework(parentProjectFolder);
|
|
68
70
|
} catch (err) {
|
|
69
|
-
framework = {
|
|
71
|
+
framework = {
|
|
72
|
+
scriptType: 'typescript',
|
|
73
|
+
bddFramework: 'none',
|
|
74
|
+
pageObject: false,
|
|
75
|
+
projectSummary: '',
|
|
76
|
+
projectLayoutHints: '',
|
|
77
|
+
};
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
let login = {};
|
|
@@ -79,6 +87,7 @@ async function main() {
|
|
|
79
87
|
? process.env.N8N_SCOPE_TO_REGIONS.split(',').map((s) => s.trim()).filter(Boolean)
|
|
80
88
|
: null;
|
|
81
89
|
const analyzeStates = /^1|true|yes$/i.test(process.env.N8N_ANALYZE_STATES || '');
|
|
90
|
+
const oclcAccessibilityContext = loadOclcAccessibilityContext();
|
|
82
91
|
|
|
83
92
|
const body = {
|
|
84
93
|
url,
|
|
@@ -86,6 +95,8 @@ async function main() {
|
|
|
86
95
|
bddFramework: framework.bddFramework,
|
|
87
96
|
pageObject: framework.pageObject,
|
|
88
97
|
projectSummary: framework.projectSummary,
|
|
98
|
+
...(framework.projectLayoutHints && { projectLayoutHints: framework.projectLayoutHints }),
|
|
99
|
+
...(oclcAccessibilityContext && { oclcAccessibilityContext }),
|
|
89
100
|
...(maxAssertions != null && !isNaN(maxAssertions) && { maxAssertions }),
|
|
90
101
|
...(scopeToRegions?.length && { scopeToRegions }),
|
|
91
102
|
...(analyzeStates && { analyzeStates: true }),
|
|
@@ -116,6 +127,7 @@ async function main() {
|
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
const data = await response.json();
|
|
130
|
+
if (data.bddFramework == null) data.bddFramework = framework.bddFramework;
|
|
119
131
|
const written = await writeGeneratedFiles(parentProjectFolder, data);
|
|
120
132
|
console.log('GenAT wrote:', written.join(', '));
|
|
121
133
|
}
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|