Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions .github/workflows/accessibility-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
name: Accessibility Tests

on:
pull_request:
branches: [ main, dev ]
push:
branches: [ main, dev ]

jobs:
pa11y-test:
name: Pa11y Accessibility Testing
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version-file: .python-version

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: package.json

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Install Python dependencies
run: |
uv pip install --upgrade pip
uv pip install -r requirements.txt

- name: Install Node dependencies
run: npm install

- name: Build frontend assets
run: npm run build

- name: Run Django migrations
env:
DJANGO_SECRET_KEY: test-secret-key-for-ci
DJANGO_DEBUG: 'False'
run: |
python manage.py migrate --noinput
python manage.py collectstatic --noinput

- name: Load test data
env:
DJANGO_SECRET_KEY: test-secret-key-for-ci
run: |
python manage.py loaddata ./data/*.json || echo "Test data load failed, continuing..."

- name: Create test session for authenticated pages
env:
DJANGO_SECRET_KEY: test-secret-key-for-ci
run: |
# Create test user and get session cookie
SESSION_JSON=$(python manage.py create_test_session --email=a11y_test@sort.com --password=a11y_test_123)
echo "Session created"

# Extract session key from JSON output (skip non-JSON lines)
SESSION_KEY=$(echo "$SESSION_JSON" | grep -o '"session_key":"[^"]*"' | cut -d'"' -f4)
echo "SESSION_KEY=$SESSION_KEY" >> $GITHUB_ENV
echo "Session key: $SESSION_KEY"

# Inject session cookie into Pa11y config
python scripts/inject_session_cookie.py "$SESSION_KEY" .pa11yci.json sessionid

- name: Start Django server
env:
DJANGO_SECRET_KEY: test-secret-key-for-ci
DJANGO_DEBUG: 'False'
run: |
python manage.py runserver 8000 &
sleep 5
curl http://localhost:8000 || echo "Server not ready yet"
sleep 5

- name: Install Pa11y
run: npm install -g pa11y-ci

- name: Run Pa11y tests
run: npx pa11y-ci --config .pa11yci.json
continue-on-error: true

- name: Upload Pa11y results
if: always()
uses: actions/upload-artifact@v4
with:
name: pa11y-results
path: pa11y-results/
retention-days: 30

lighthouse-test:
name: Lighthouse Accessibility Audit
runs-on: ubuntu-24.04

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version-file: .python-version

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: package.json

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Install Python dependencies
run: |
uv pip install --upgrade pip
uv pip install -r requirements.txt

- name: Install Node dependencies
run: npm install

- name: Build frontend assets
run: npm run build

- name: Run Django migrations
env:
DJANGO_SECRET_KEY: test-secret-key-for-ci
DJANGO_DEBUG: 'False'
run: |
python manage.py migrate --noinput
python manage.py collectstatic --noinput

- name: Load test data
env:
DJANGO_SECRET_KEY: test-secret-key-for-ci
run: |
python manage.py loaddata ./data/*.json || echo "Test data load failed, continuing..."

- name: Create test session for authenticated pages
env:
DJANGO_SECRET_KEY: test-secret-key-for-ci
run: |
# Create test user and get session cookie for authenticated testing
python manage.py create_test_session --email=a11y_test@sort.com --password=a11y_test_123

- name: Start Django server
env:
DJANGO_SECRET_KEY: test-secret-key-for-ci
DJANGO_DEBUG: 'False'
run: |
python manage.py runserver 8000 &
sleep 10

- name: Install Lighthouse CI
run: npm install -g @lhci/cli@latest

- name: Run Lighthouse CI
run: lhci autorun
continue-on-error: true

- name: Upload Lighthouse results
if: always()
uses: actions/upload-artifact@v4
with:
name: lighthouse-results
path: .lighthouseci/
retention-days: 30
52 changes: 52 additions & 0 deletions .pa11yci.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"defaults": {
"runners": [
"axe",
"htmlcs"
],
"standard": "WCAG2AA",
"timeout": 10000,
"wait": 2000,
"chromeLaunchConfig": {
"args": [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage"
]
},
"includeNotices": false,
"includeWarnings": true,
"threshold": 10,
"reporters": [
"cli",
"json"
]
},
"urls": [
{
"url": "http://localhost:8000/",
"screenCapture": "screenshots/home.png"
},
{
"url": "http://localhost:8000/about/",
"screenCapture": "screenshots/about.png"
},
{
"url": "http://localhost:8000/help/",
"screenCapture": "screenshots/help.png"
},
{
"url": "http://localhost:8000/privacy/",
"screenCapture": "screenshots/privacy.png"
},
{
"url": "http://localhost:8000/eula/",
"screenCapture": "screenshots/eula.png"
},
{
"url": "http://localhost:8000/myorganisation/",
"screenCapture": "screenshots/myorganisation.png",
"actions": []
}
]
}
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,14 @@ We expect all contributors to follow the SORT [Code of Conduct](CODE_OF_CONDUCT.
# Testing

Please read the [testing documentation](docs/testing.md).

# Accessibility

## Manual checks

Use the [WAVE extension](https://wave.webaim.org/extension/) for your web browser to check the production website for errors. It doesn't work as well in development because of the Django debug toolbar.

## Automatic checks

There is a GitHub Actions workflow in [`.github/workflows/accessibility-tests.yml`](./.github/workflows/accessibility-tests.yml) that uses a couple of different frameworks to inspect the website for access issues.

20 changes: 20 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import a11y from 'eslint-plugin-jsx-a11y';
import globals from 'globals';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
Expand All @@ -10,6 +11,9 @@ export default ts.config(
...ts.configs.recommended,
...svelte.configs.recommended,
{
plugins: {
'jsx-a11y': a11y
},
languageOptions: {
globals: {
...globals.browser,
Expand Down Expand Up @@ -48,6 +52,22 @@ export default ts.config(
'@typescript-eslint/no-unused-vars' : 'warn',
'svelte/no-at-html-tags': 'warn',
"no-import-assign": "off",

// Accessibility rules
'jsx-a11y/alt-text': 'error',
'jsx-a11y/aria-props': 'error',
'jsx-a11y/aria-proptypes': 'error',
'jsx-a11y/aria-role': 'error',
'jsx-a11y/aria-unsupported-elements': 'error',
'jsx-a11y/click-events-have-key-events': 'warn',
'jsx-a11y/heading-has-content': 'error',
'jsx-a11y/html-has-lang': 'error',
'jsx-a11y/img-redundant-alt': 'warn',
'jsx-a11y/label-has-associated-control': 'error',
'jsx-a11y/no-autofocus': 'warn',
'jsx-a11y/no-static-element-interactions': 'warn',
'jsx-a11y/role-has-required-aria-props': 'error',
'jsx-a11y/tabindex-no-positive': 'error',
}
}
);
70 changes: 70 additions & 0 deletions home/management/commands/create_test_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
Management command to create a test user and generate a session cookie for accessibility testing.
This is used in CI/CD to allow Pa11y and Lighthouse to test authenticated pages.
"""
import json
from django.contrib.auth import get_user_model
from django.contrib.sessions.backends.db import SessionStore
from django.core.management.base import BaseCommand


class Command(BaseCommand):
help = "Create a test user and generate session cookie for accessibility testing"

def add_arguments(self, parser):
parser.add_argument(
"--email",
type=str,
default="a11y_test@sort.com",
help="Email for the test user",
)
parser.add_argument(
"--password",
type=str,
default="a11y_test_password_123",
help="Password for the test user",
)

def handle(self, *args, **options):
User = get_user_model()
email = options["email"]
password = options["password"]

# Create or get test user
user, created = User.objects.get_or_create(
email=email,
defaults={
"first_name": "A11y",
"last_name": "Test",
"is_active": True,
},
)

if created:
user.set_password(password)
user.save()
self.stdout.write(
self.style.SUCCESS(f'Created test user: {email}')
)
else:
self.stdout.write(
self.style.WARNING(f'Test user already exists: {email}')
)

# Create a session for this user
session = SessionStore()
session['_auth_user_id'] = str(user.pk)
session['_auth_user_backend'] = 'django.contrib.auth.backends.ModelBackend'
session['_auth_user_hash'] = user.get_session_auth_hash()
session.save()

# Output session information as JSON for easy parsing
session_data = {
"session_key": session.session_key,
"cookie_name": "sessionid",
"cookie_value": session.session_key,
"user_email": email,
"user_id": user.pk,
}

self.stdout.write(json.dumps(session_data))
29 changes: 29 additions & 0 deletions lighthouserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"ci": {
"collect": {
"url": [
"http://localhost:8000/",
"http://localhost:8000/about/",
"http://localhost:8000/help/",
"http://localhost:8000/privacy/",
"http://localhost:8000/eula/"
],
"numberOfRuns": 1,
"settings": {
"chromeFlags": "--no-sandbox --disable-setuid-sandbox",
"onlyCategories": ["accessibility", "best-practices", "seo"]
}
},
"upload": {
"target": "temporary-public-storage"
},
"assert": {
"preset": "lighthouse:recommended",
"assertions": {
"categories:accessibility": ["error", {"minScore": 0.90}],
"categories:best-practices": ["warn", {"minScore": 0.85}],
"categories:seo": ["warn", {"minScore": 0.80}]
}
}
}
}
Loading
Loading