diff --git a/.github/workflows/accessibility.yml b/.github/workflows/accessibility.yml new file mode 100644 index 00000000..bfc52aba --- /dev/null +++ b/.github/workflows/accessibility.yml @@ -0,0 +1,70 @@ +name: Accessibility + +on: + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: accessibility-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + axe: + name: Axe accessibility checks + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: latest + + - name: Install Python dependencies + working-directory: efile_app + run: uv sync --group dev + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 22 + cache: npm + cache-dependency-path: efile_app/package-lock.json + + - name: Install browser dependencies + working-directory: efile_app + run: | + npm ci + npx playwright install --with-deps chromium + + - name: Seed the local accessibility fixture + working-directory: efile_app + run: | + uv run python manage.py migrate --noinput + uv run python manage.py seed_accessibility_session --output playwright/.auth/a11y.json + + - name: Run Axe checks + working-directory: efile_app + env: + A11Y_STORAGE_STATE: playwright/.auth/a11y.json + run: | + uv run python manage.py runserver 127.0.0.1:8000 > /tmp/litefile-a11y-server.log 2>&1 & + curl --retry 30 --retry-connrefused --retry-delay 1 --fail http://127.0.0.1:8000/choose-jurisdiction + npm run test:a11y + + - name: Upload accessibility results + if: always() + uses: actions/upload-artifact@v4 + with: + name: axe-accessibility-results + path: | + efile_app/test-results/ + /tmp/litefile-a11y-server.log + if-no-files-found: warn diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c091860a..019ffabf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,6 +112,10 @@ jobs: working-directory: efile_app run: npm run format:js:check + - name: Stylelint accessibility rules + working-directory: efile_app + run: npm run lint:css + - name: JavaScript unit tests working-directory: efile_app run: npm run test:unit diff --git a/.gitignore b/.gitignore index 3b69ec9f..96256622 100644 --- a/.gitignore +++ b/.gitignore @@ -170,6 +170,7 @@ docs/.docusaurus/ # Playwright test-results/ +efile_app/playwright/.auth/ efile_app/screenshots efile_app/playwright-report efile_app/tmp diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 69e1aefb..45971ba2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,6 +56,17 @@ repos: uv run --directory "$ROOT_DIR/efile_app" css-beautify -r "${files[@]}" - -- + - id: stylelint + name: Stylelint accessibility rules + entry: bash -c + language: system + files: ^efile_app/efile/.*\.css$ + args: + - | + ROOT_DIR=$(git rev-parse --show-toplevel) + cd "$ROOT_DIR/efile_app" && npm exec -- stylelint "efile/**/*.css" + - -- + - id: js-beautify name: js-beautify (format) entry: bash -c diff --git a/efile_app/crosswalk_review/templates/crosswalk_review/index.html b/efile_app/crosswalk_review/templates/crosswalk_review/index.html index e2a6d894..8dd271ea 100644 --- a/efile_app/crosswalk_review/templates/crosswalk_review/index.html +++ b/efile_app/crosswalk_review/templates/crosswalk_review/index.html @@ -72,7 +72,7 @@ padding: 0.5rem 0.75rem; font-size: 0.95rem; width: 260px; - outline: none; + outline: 2px solid transparent; transition: border-color 0.15s; } diff --git a/efile_app/crosswalk_review/templates/crosswalk_review/review.html b/efile_app/crosswalk_review/templates/crosswalk_review/review.html index 755bd484..a0cecefa 100644 --- a/efile_app/crosswalk_review/templates/crosswalk_review/review.html +++ b/efile_app/crosswalk_review/templates/crosswalk_review/review.html @@ -131,7 +131,7 @@ .review-field textarea:focus, .review-field select:focus, .lookup-controls select:focus { - outline: none; + outline: 2px solid transparent; border-color: #1a6b3c; box-shadow: 0 0 0 3px rgba(26, 107, 60, 0.12); } diff --git a/efile_app/efile/management/commands/seed_accessibility_session.py b/efile_app/efile/management/commands/seed_accessibility_session.py new file mode 100644 index 00000000..2bbd3829 --- /dev/null +++ b/efile_app/efile/management/commands/seed_accessibility_session.py @@ -0,0 +1,129 @@ +"""Create deterministic, local-only data for browser accessibility checks.""" + +import json +from pathlib import Path + +from django.conf import settings +from django.core.management.base import BaseCommand +from django.test import Client + +from efile.models import FilingDocument, FilingDraft, FilingParty, FilingPlan +from efile.services.current_drafts import CURRENT_DRAFT_SESSION_KEY +from efile.workflow import ExistingCase, WorkflowStepKey + + +class Command(BaseCommand): + help = "Seed a local browser session for the Axe accessibility suite." + + def add_arguments(self, parser): + parser.add_argument("--output", required=True, help="Path for Playwright storage state JSON.") + parser.add_argument("--origin", default="http://127.0.0.1:8000", help="Origin served to Playwright.") + + def handle(self, *args, **options): + user_model = settings.AUTH_USER_MODEL + from django.apps import apps + + user_class = apps.get_model(user_model) + user, _ = user_class.objects.update_or_create( + username="accessibility-checker", + defaults={ + "email": "accessibility-checker@example.com", + "tyler_jurisdiction": "illinois", + "tyler_username": "accessibility-checker@example.com", + "first_name": "Avery", + "last_name": "Checker", + }, + ) + user.set_unusable_password() + user.save() + + FilingDraft.objects.filter(user=user).delete() + FilingPlan.objects.filter(user=user).delete() + plan = FilingPlan.objects.create(user=user, jurisdiction="illinois", title="Accessibility test filing") + draft = FilingDraft.objects.create( + user=user, + plan=plan, + jurisdiction="illinois", + workflow_version=2, + existing_case=ExistingCase.NEW, + current_step=WorkflowStepKey.REVIEW, + court_code="cook:cvd1", + court_name="Cook County", + case_category_code="6198", + case_category_name="Small Claims", + case_type_code="183541", + case_type_name="Contract", + filing_type_code="143132", + filing_type_name="Complaint", + document_checklist_acknowledged=True, + selected_payment_account_id="a11y-payment-account", + selected_payment_account_name="Accessibility payment account", + quoted_fee_total="0.00", + ) + FilingDocument.objects.create( + draft=draft, + role=FilingDocument.Role.LEAD, + name="Accessibility complaint.pdf", + original_filename="Accessibility complaint.pdf", + filing_type_code="143132", + filing_type_name="Complaint", + document_type_code="public", + document_type_name="Public", + ) + FilingParty.objects.create( + draft=draft, + role="filer", + sort_order=0, + first_name="Avery", + last_name="Checker", + address_line_1="100 Main Street", + city="Chicago", + state="IL", + zip_code="60601", + email=user.email, + party_type="plaintiff", + party_type_name="Plaintiff", + is_filing_party=True, + ) + other_party = FilingParty.objects.create( + draft=draft, + role="other", + sort_order=1, + first_name="Jordan", + last_name="Example", + ) + + # Let Django's own test client construct the authenticated session. This + # tracks framework changes to session-auth details without duplicating + # private authentication keys in this browser-only fixture command. + client = Client() + client.force_login(user) + session = client.session + session[CURRENT_DRAFT_SESSION_KEY] = draft.pk + session["jurisdiction"] = "illinois" + session["auth_tokens"] = {"TYLER-TOKEN-ILLINOIS": "accessibility-test-token"} + session.save() + + output = Path(options["output"]) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text( + json.dumps( + { + "cookies": [ + { + "name": settings.SESSION_COOKIE_NAME, + "value": session.session_key, + "domain": "127.0.0.1", + "path": "/", + "expires": -1, + "httpOnly": True, + "secure": False, + "sameSite": "Lax", + } + ], + "origins": [], + }, + indent=2, + ) + ) + self.stdout.write(self.style.SUCCESS(f"Seeded accessibility session for party {other_party.pk}.")) diff --git a/efile_app/efile/static/css/common.css b/efile_app/efile/static/css/common.css index 3c33fbf1..ed88d413 100644 --- a/efile_app/efile/static/css/common.css +++ b/efile_app/efile/static/css/common.css @@ -114,6 +114,31 @@ h2 { --bs-btn-active-border-color: var(--brand-blue-active); } +/* Bootstrap's default secondary outline is too faint on several light + surfaces, especially in compact controls. */ +.btn-outline-secondary { + --bs-btn-color: #343a40; + --bs-btn-border-color: #343a40; + --bs-btn-hover-bg: #343a40; + --bs-btn-hover-border-color: #343a40; + --bs-btn-active-bg: #212529; + --bs-btn-active-border-color: #212529; +} + +/* Respect a person's system setting even when an individual component's + motion has not opted into the no-preference media query yet. */ +@media (prefers-reduced-motion: reduce) { + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} + /* Persistent, in-flow feedback shared by account, dashboard, list, and workflow screens. Messages stay available to zoom and assistive-technology users until they navigate or correct the problem. */ diff --git a/efile_app/efile/static/css/reorganized-flow.css b/efile_app/efile/static/css/reorganized-flow.css index 47bd38b0..2566c404 100644 --- a/efile_app/efile/static/css/reorganized-flow.css +++ b/efile_app/efile/static/css/reorganized-flow.css @@ -1392,6 +1392,27 @@ margin-top: 0; } +/* Bootstrap's secondary outline is too light against the warning panel used + for this alternative action. Keep its text and border legible at AA. */ +#claim-party-instead-filing-for, +#claim-party-cancel { + --bs-btn-color: #343a40; + --bs-btn-border-color: #343a40; + --bs-btn-hover-bg: #343a40; + --bs-btn-hover-border-color: #343a40; + --bs-btn-active-bg: #212529; + --bs-btn-active-border-color: #212529; +} + +.document-plan__save { + --bs-btn-color: #343a40; + --bs-btn-border-color: #343a40; + --bs-btn-hover-bg: #343a40; + --bs-btn-hover-border-color: #343a40; + --bs-btn-active-bg: #212529; + --bs-btn-active-border-color: #212529; +} + .claim-party-dialog .workflow-actions { display: flex; gap: 0.75rem; diff --git a/efile_app/efile/templates/efile/choose_jurisdiction.html b/efile_app/efile/templates/efile/choose_jurisdiction.html index a8c73b31..3540321e 100644 --- a/efile_app/efile/templates/efile/choose_jurisdiction.html +++ b/efile_app/efile/templates/efile/choose_jurisdiction.html @@ -40,6 +40,7 @@

{{ the_title }}

{% for value in jurisdiction_details %}
- {{ case_category_label }} +