Skip to content

Commit 7dd6347

Browse files
viktoravelinoolayinkaadelakunOlayinka Adelakunautofix-ci[bot]Olayinka Adelakun
authored
feat: a11y (langflow-ai#13561)
* chore: add accessibility action plan and gap report for IBM Level 1 compliance * feat: integrate accessibility checker for automated a11y scans - Added `accessibility-checker` package to the project dependencies. - Enhanced test fixtures to include accessibility scanning capabilities. - Implemented `runA11yScan` method in the `LangflowPage` type for running accessibility checks. - Created utility functions for building accessibility scan labels, formatting failure messages, and generating summary attachments. - Updated test setup to manage accessibility sessions and assert compliance based on scan results. * feat: add accessibility test usage documentation for IBM scans * feat: enhance accessibility testing with JSON report aggregation and new tests * feat: add GitHub Actions workflow for IBM accessibility scans * feat: update a11y scan workflow to allow testing from feat/a11y branch * feat: skip Puppeteer download during npm install in Dockerfiles for accessibility testing * feat: enhance a11y scan workflow to resolve and scan latest release branches * test: add component a11y unit tests (jest-axe) (langflow-ai#13613) * test: add component a11y unit tests (jest-axe) Cover 14 components in two waves. 16 tests fail by design - each encodes a known gap from a11y-action-plan (phases 0-3) and flips to a regression lock once the component fix lands. 23 tests pass as regression locks for already-correct semantics. * ci: split a11y unit tests into separate workflow Main jest CI excludes *.a11y.test.* so known-gap failures don't block PRs; new a11y-unit-tests.yml runs them as informational check. * fix(a11y): resolve component gaps flagged by a11y unit tests Fixes all 16 failing jest-axe gap tests: - TableHead defaults scope=col - alert display area gets aria-live region - app header becomes <header> landmark; bell button labeled - icon wrapper decorative by default (aria-hidden), ariaLabel opt-in - Input drops wrapping <label> (name pollution) and hides placeholder span - password toggle back in tab order with aria-label/aria-pressed - CheckBoxDiv exposes checkbox role + state - accordion trigger renders native Radix button - dialogs focus container on open instead of suppressing autofocus - full-screen modal exposes dialog role/aria-modal/ariaLabel - flow list card focusable with keyboard activation Also wraps test focus calls in act() and mocks the icon loader so the a11y suite runs without React warnings. * fix: skip Puppeteer Chrome download in remaining Dockerfiles frontend and base images still ran bare npm install; puppeteer (via accessibility-checker, test-only) tried to fetch Chrome and broke the Docker image build in CI. * test: extend a11y unit test coverage (jest-axe) Adds axe + semantics tests for untested primitives (button, alert, textarea, radio-group, dropdown-menu, tooltip, popover) and two known-gap tests that fail by design until fixes land: - copyFieldAreaComponent copy action is a bare div onClick (plan 2.4) - DialogContentWithouFixed renders an empty fragment (plan 1.6) * fix: make webhook copy field a real button (a11y) Copy action was a bare div onClick - no role, name, or keyboard support (a11y-action-plan 2.4). Drops the no-longer-needed DialogContentWithouFixed known-gap test. * feat: update a11y unit tests and improve accessibility semantics in components * feat: enable pull request trigger for a11y scan workflow * feat: Add a11y regression scan suite (langflow-ai#13663) * test: add component a11y unit tests (jest-axe) Cover 14 components in two waves. 16 tests fail by design - each encodes a known gap from a11y-action-plan (phases 0-3) and flips to a regression lock once the component fix lands. 23 tests pass as regression locks for already-correct semantics. * ci: split a11y unit tests into separate workflow Main jest CI excludes *.a11y.test.* so known-gap failures don't block PRs; new a11y-unit-tests.yml runs them as informational check. * fix(a11y): resolve component gaps flagged by a11y unit tests Fixes all 16 failing jest-axe gap tests: - TableHead defaults scope=col - alert display area gets aria-live region - app header becomes <header> landmark; bell button labeled - icon wrapper decorative by default (aria-hidden), ariaLabel opt-in - Input drops wrapping <label> (name pollution) and hides placeholder span - password toggle back in tab order with aria-label/aria-pressed - CheckBoxDiv exposes checkbox role + state - accordion trigger renders native Radix button - dialogs focus container on open instead of suppressing autofocus - full-screen modal exposes dialog role/aria-modal/ariaLabel - flow list card focusable with keyboard activation Also wraps test focus calls in act() and mocks the icon loader so the a11y suite runs without React warnings. * fix: skip Puppeteer Chrome download in remaining Dockerfiles frontend and base images still ran bare npm install; puppeteer (via accessibility-checker, test-only) tried to fetch Chrome and broke the Docker image build in CI. * test: extend a11y unit test coverage (jest-axe) Adds axe + semantics tests for untested primitives (button, alert, textarea, radio-group, dropdown-menu, tooltip, popover) and two known-gap tests that fail by design until fixes land: - copyFieldAreaComponent copy action is a bare div onClick (plan 2.4) - DialogContentWithouFixed renders an empty fragment (plan 1.6) * fix: make webhook copy field a real button (a11y) Copy action was a bare div onClick - no role, name, or keyboard support (a11y-action-plan 2.4). Drops the no-longer-needed DialogContentWithouFixed known-gap test. * feat: update a11y unit tests and improve accessibility semantics in components * feat: enable pull request trigger for a11y scan workflow * test: add a11y regression scan suite * test: make a11y scans non-blocking * test: discover a11y scan specs * ci: group a11y workflow checks * fix(a11y): restore focus-visible indicators (langflow-ai#13664) * fix(a11y): restore focus-visible indicators * [autofix.ci] apply automated fixes --------- Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Viktor Avelino <64113566+viktoravelino@users.noreply.github.com> * a11y: improve auth accessibility coverage (langflow-ai#13724) * fix: improve auth accessibility coverage * fix(a11y): restore delete trigger tab focus * test(dialog): cover nested description * fix(a11y): add accessible names and hide decorative icons in LangflowCounts (langflow-ai#13731) * fix(a11y): add accessible names and hide decorative icons in LangflowCounts * [autofix.ci] apply automated fixes --------- Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.war.can.ibm.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix(a11y): add accessible names to icon-only buttons, fix placeholder (langflow-ai#13666) * fix(a11y): add accessible names to icon-only buttons, fix placeholder contrast, and enforce touch target size on /flows * [autofix.ci] apply automated fixes * fix issues * [autofix.ci] apply automated fixes * fix(a11y): fix aria-allowed-attr, invalid sidebar list structure, and session filter label mismatch on flow editor (langflow-ai#13668) * fix(a11y): fix aria-allowed-attr, invalid sidebar list structure, and session filter label mismatch on flow editor * [autofix.ci] apply automated fixes * improve translations * remove app-header-a11y --------- Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.war.can.ibm.com> * label get-started close button and fix dark-mode placeholder contrast * remove testing change * improve the lint of the testcase --------- Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Viktor Avelino <64113566+viktoravelino@users.noreply.github.com> Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.war.can.ibm.com> * fix: update package dependencies and versions in package-lock.json * chore: Remove accessibility documentation and reports - Deleted the accessibility action plan, gap report, level 1 requirements, and test usage documentation. - This cleanup is part of a broader effort to streamline accessibility resources and focus on actionable items. * chore: Add accessibility scan reports (langflow-ai#13812) * feat: add accessibility scan reports * docs: add a11y scan route map * test(a11y): add manifest-backed scans * ci(a11y): summarize scans by route * fix: update Playwright version to 1.60.0 and enhance a11y scan options * fix(test): keep fixture args destructured * test(a11y): bootstrap asset route scans --------- Co-authored-by: olayinkaadelakun <olayinka.adelakun@ibm.com> Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.war.can.ibm.com>
1 parent 776b9e6 commit 7dd6347

109 files changed

Lines changed: 7651 additions & 2034 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
---
2+
name: ibm-a11y-automation
3+
description: Run Langflow's local accessibility scanner script against frontend routes from src/frontend/src/routes.tsx and summarize the JSON report.
4+
---
5+
6+
# Langflow Accessibility Scanner
7+
8+
Use this skill when asked to scan Langflow frontend pages for accessibility issues.
9+
10+
## Scanner
11+
12+
Use the Python script:
13+
14+
```bash
15+
uv run python scripts/a11y/a11y_scan.py \
16+
--url http://localhost:3000 \
17+
--routes-file scripts/a11y/a11y_routes.json \
18+
--route-group static \
19+
--out /tmp/langflow-a11y-report.json \
20+
--markdown /tmp/langflow-a11y-report.md \
21+
--html /tmp/langflow-a11y-report.html \
22+
--timeout-ms 45000
23+
```
24+
25+
Script options:
26+
27+
- `--url`: base app URL, usually `http://localhost:3000`.
28+
- `--routes-file`: route manifest JSON file. Prefer `scripts/a11y/a11y_routes.json`.
29+
- `--route-group`: manifest group to scan. Default: `static`.
30+
- `--routes`: comma-separated route paths to scan.
31+
- `--route`: one route path; can be repeated instead of `--routes`.
32+
- `--levels`: comma-separated issue levels. Default: `violation`.
33+
- `--out`: JSON report path.
34+
- `--markdown`: optional Markdown report path.
35+
- `--html`: optional self-contained HTML report path.
36+
- `--timeout-ms`: per-route timeout.
37+
- `--quiet-ms`: network quiet window before scanning. Default: `1000`.
38+
- `--states-file`: JSON file with explicit modal/state actions.
39+
- `--headed`: show browser while scanning.
40+
41+
## Route Selection
42+
43+
Use `scripts/a11y/a11y_routes.json` as the source of truth for route selection.
44+
45+
The normal CI/local batch is the manifest `static` group. Prefer that unless the user asks for custom, dynamic, or gated routes.
46+
47+
Common manifest-backed command:
48+
49+
```bash
50+
uv run python scripts/a11y/a11y_scan.py \
51+
--url http://localhost:3000 \
52+
--routes-file scripts/a11y/a11y_routes.json \
53+
--route-group static \
54+
--out /tmp/langflow-a11y-static.json \
55+
--markdown /tmp/langflow-a11y-static.md \
56+
--html /tmp/langflow-a11y-static.html
57+
```
58+
59+
Dynamic routes need real IDs before scanning:
60+
61+
- `/flow/:id/`
62+
- `/flow/:id/view`
63+
- `/playground/:id/`
64+
- `/assets/knowledge-bases/:sourceId/chunks`
65+
66+
For dynamic routes, get IDs from the loaded app, API responses, or existing test data before replacing placeholders.
67+
68+
## Examples
69+
70+
Scan one route:
71+
72+
```bash
73+
uv run python scripts/a11y/a11y_scan.py \
74+
--url http://localhost:3000 \
75+
--route /flows \
76+
--out /tmp/langflow-a11y-flows.json
77+
```
78+
79+
Scan multiple routes:
80+
81+
```bash
82+
uv run python scripts/a11y/a11y_scan.py \
83+
--url http://localhost:3000 \
84+
--routes-file scripts/a11y/a11y_routes.json \
85+
--route-group static \
86+
--out /tmp/langflow-a11y-report.json \
87+
--markdown /tmp/langflow-a11y-report.md \
88+
--html /tmp/langflow-a11y-report.html
89+
```
90+
91+
Scan more than violations:
92+
93+
```bash
94+
uv run python scripts/a11y/a11y_scan.py \
95+
--url http://localhost:3000 \
96+
--routes-file scripts/a11y/a11y_routes.json \
97+
--route-group static \
98+
--levels violation,potentialviolation,recommendation \
99+
--out /tmp/langflow-a11y-expanded.json
100+
```
101+
102+
Scan route plus modal states:
103+
104+
```bash
105+
uv run python scripts/a11y/a11y_scan.py \
106+
--url http://localhost:3000 \
107+
--states-file /tmp/langflow-a11y-states.json \
108+
--out /tmp/langflow-a11y-modal-report.json \
109+
--markdown /tmp/langflow-a11y-modal-report.md \
110+
--html /tmp/langflow-a11y-modal-report.html \
111+
--timeout-ms 45000
112+
```
113+
114+
State file shape:
115+
116+
```json
117+
[
118+
{
119+
"route": "/settings/global-variables",
120+
"states": [
121+
{
122+
"name": "new-global-variable-modal",
123+
"open": [
124+
{ "click": "[data-testid='api-key-button-store']" },
125+
{ "waitFor": "[role='dialog']" }
126+
],
127+
"close": [
128+
{ "press": "Escape" },
129+
{ "waitForHidden": "[role='dialog']" }
130+
]
131+
}
132+
]
133+
}
134+
]
135+
```
136+
137+
Supported state actions:
138+
139+
- `{ "click": "<css selector>" }`
140+
- `{ "clickText": "<visible text>" }`
141+
- `{ "clickRole": { "role": "button", "name": "Create" } }`
142+
- `{ "fill": { "selector": "<css selector>", "value": "text" } }`
143+
- `{ "press": "Escape" }`
144+
- `{ "press": { "selector": "<css selector>", "key": "Enter" } }`
145+
- `{ "waitFor": "<css selector>" }`
146+
- `{ "waitForHidden": "<css selector>" }`
147+
- `{ "waitForText": "<visible text>" }`
148+
- `{ "wait": 500 }`
149+
150+
## Report
151+
152+
The scanner always writes JSON. It can also write Markdown and HTML for presentation.
153+
154+
Use JSON for exact data. Use Markdown for PR comments or issues. Use HTML when the user wants a browsable report.
155+
156+
Summarize:
157+
158+
- report path
159+
- Markdown/HTML report paths, when generated
160+
- total issue count
161+
- per-route issue count
162+
- per-route API request count
163+
- per-route request failure count
164+
- top rule IDs
165+
166+
Use report fields directly:
167+
168+
- `totalIssues`
169+
- `results[].route`
170+
- `results[].state`
171+
- `results[].phase`
172+
- `results[].apiRequests`
173+
- `results[].requestFailures`
174+
- `results[].diagnostics`
175+
- `results[].issues[].ruleId`
176+
177+
## Rules
178+
179+
- Use only scanner output for findings.
180+
- Do not invent route names. Read `routes.tsx`.
181+
- Do not auto-click arbitrary buttons to find modals. Use explicit state actions.
182+
- Avoid destructive modal actions unless the user explicitly asks and data is safe.
183+
- If a route has zero API requests, mention that scan quality may be limited.
184+
- Ask before fixing files unless the user explicitly asks for fixes.

.github/workflows/a11y-scan.yml

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
name: A11y / IBM Equal Access Scans
2+
3+
on:
4+
pull_request:
5+
schedule:
6+
# Run nightly at 02:00 UTC, after the nightly build window
7+
- cron: "0 2 * * *"
8+
workflow_dispatch:
9+
inputs:
10+
ref:
11+
description: "(Optional) ref to checkout"
12+
required: false
13+
type: string
14+
assert:
15+
description: "Fail the run when scans differ from committed baselines"
16+
required: false
17+
type: boolean
18+
default: false
19+
20+
env:
21+
NODE_VERSION: "22"
22+
PYTHON_VERSION: "3.13"
23+
# Define the directory where Playwright browsers will be installed.
24+
# This path is used for caching across workflows
25+
PLAYWRIGHT_BROWSERS_PATH: "ms-playwright"
26+
PLAYWRIGHT_VERSION: "1.60.0"
27+
28+
jobs:
29+
a11y-scan:
30+
name: Playwright scan runner
31+
runs-on: ubuntu-latest
32+
permissions:
33+
contents: read
34+
steps:
35+
# Cron only fires from the default branch, so scheduled runs resolve the
36+
# latest release-* branch and scan that (same pattern as nightly_build).
37+
- name: Resolve Ref To Scan
38+
id: resolve_ref
39+
shell: bash
40+
run: |
41+
if [ -n "${{ inputs.ref }}" ]; then
42+
REF="${{ inputs.ref }}"
43+
elif [ "${{ github.event_name }}" = "schedule" ]; then
44+
REF=$(git ls-remote --heads https://github.com/${{ github.repository }} 'refs/heads/release-*' \
45+
| awk '{print $2}' \
46+
| sed 's|refs/heads/||' \
47+
| grep -E '^release-[0-9]+\.[0-9]+\.[0-9]+$' \
48+
| sort -V \
49+
| tail -n 1)
50+
if [ -z "$REF" ]; then
51+
echo "No release-* branch found in ${{ github.repository }}"
52+
exit 1
53+
fi
54+
else
55+
REF="${{ github.ref }}"
56+
fi
57+
echo "ref=$REF" >> "$GITHUB_OUTPUT"
58+
echo "Scanning ref: $REF"
59+
60+
- name: Checkout Repository
61+
uses: actions/checkout@v6
62+
with:
63+
ref: ${{ steps.resolve_ref.outputs.ref }}
64+
65+
- name: Setup Node.js Environment
66+
uses: actions/setup-node@v6
67+
with:
68+
node-version: ${{ env.NODE_VERSION }}
69+
70+
- name: Get npm cache directory
71+
id: npm-cache-dir
72+
shell: bash
73+
run: echo "dir=$(npm config get cache)" >> "$GITHUB_OUTPUT"
74+
75+
- name: Cache npm dependencies
76+
uses: actions/cache@v5
77+
continue-on-error: true
78+
with:
79+
path: ${{ steps.npm-cache-dir.outputs.dir }}
80+
key: ${{ runner.os }}-npm-${{ env.NODE_VERSION }}-${{ hashFiles('src/frontend/package-lock.json') }}
81+
restore-keys: |
82+
${{ runner.os }}-npm-${{ env.NODE_VERSION }}-
83+
84+
- name: Install Frontend Dependencies
85+
run: npm ci
86+
working-directory: ./src/frontend
87+
88+
- name: Cache Playwright Browsers
89+
id: cache-playwright
90+
uses: actions/cache@v5
91+
continue-on-error: true
92+
with:
93+
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
94+
key: playwright-${{ env.PLAYWRIGHT_VERSION }}-chromium-${{ runner.os }}
95+
restore-keys: |
96+
playwright-${{ env.PLAYWRIGHT_VERSION }}-chromium-${{ runner.os }}
97+
98+
- name: Install Playwright Browser Dependencies
99+
if: steps.cache-playwright.outputs.cache-hit != 'true'
100+
shell: bash
101+
run: |
102+
cd ./src/frontend
103+
npx playwright install --with-deps chromium
104+
105+
- name: "Setup Environment"
106+
uses: astral-sh/setup-uv@v6
107+
with:
108+
enable-cache: true
109+
cache-dependency-glob: "uv.lock"
110+
python-version: ${{ env.PYTHON_VERSION }}
111+
prune-cache: false
112+
113+
- name: Install Python Dependencies
114+
run: uv sync
115+
116+
- name: Run IBM Equal Access Scans
117+
shell: bash
118+
env:
119+
RUN_A11Y: "true"
120+
RUN_A11Y_ASSERT: ${{ github.event_name == 'workflow_dispatch' && inputs.assert && 'true' || 'false' }}
121+
LANGFLOW_DEACTIVATE_TRACING: "true"
122+
run: |
123+
cd src/frontend
124+
# Only specs that call runA11yScan produce reports; discover them
125+
# so new scan hosts are picked up without editing this workflow.
126+
SCAN_SPECS=$(grep -rl "runA11yScan(" tests --include="*.spec.ts" | sort)
127+
echo "Specs with a11y scans:"
128+
echo "$SCAN_SPECS"
129+
test -n "$SCAN_SPECS"
130+
npx playwright test $SCAN_SPECS --project=chromium --workers=1 --retries=2
131+
132+
- name: Build IBM Scan Summary
133+
if: always()
134+
shell: bash
135+
run: |
136+
cd src/frontend
137+
if ls coverage/accessibility-reports/*.json > /dev/null 2>&1; then
138+
npm run a11y:html-report --silent
139+
{
140+
echo '## Accessibility Report'
141+
echo ''
142+
echo 'Full route-by-route HTML report: download the `ibm-a11y-reports-${{ github.run_attempt }}` artifact from this run and open `index.html`.'
143+
echo ''
144+
npm run a11y:job-summary --silent
145+
} >> "$GITHUB_STEP_SUMMARY"
146+
else
147+
echo "No accessibility reports were generated." | tee -a "$GITHUB_STEP_SUMMARY"
148+
fi
149+
150+
- name: Upload Accessibility Reports
151+
if: always()
152+
uses: actions/upload-artifact@v6
153+
with:
154+
name: ibm-a11y-reports-${{ github.run_attempt }}
155+
path: src/frontend/coverage/accessibility-reports
156+
retention-days: 30
157+
overwrite: true
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: A11y / Component Unit Tests
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "src/frontend/**"
7+
- ".github/workflows/a11y-unit-tests.yml"
8+
- "Makefile.frontend"
9+
workflow_call:
10+
inputs:
11+
ref:
12+
description: "(Optional) ref to checkout"
13+
required: false
14+
type: string
15+
workflow_dispatch:
16+
inputs:
17+
ref:
18+
description: "(Optional) ref to checkout"
19+
required: false
20+
type: string
21+
22+
env:
23+
NODE_VERSION: "22"
24+
25+
jobs:
26+
a11y-unit-tests:
27+
name: jest-axe
28+
runs-on: ubuntu-latest
29+
permissions:
30+
contents: read
31+
steps:
32+
- name: Checkout Repository
33+
uses: actions/checkout@v6
34+
with:
35+
ref: ${{ inputs.ref || github.ref }}
36+
37+
- name: Setup Node.js Environment
38+
uses: actions/setup-node@v6
39+
with:
40+
node-version: ${{ env.NODE_VERSION }}
41+
cache: "npm"
42+
cache-dependency-path: ./src/frontend/package-lock.json
43+
44+
# These are regression locks: every a11y unit test passes as of the
45+
# component fixes in feat/a11y-unit-tests, so a failure here means a
46+
# real a11y regression and blocks the PR.
47+
- name: Run jest-axe unit tests
48+
run: make test_frontend_a11y_unit_ci

0 commit comments

Comments
 (0)