diff --git a/.github/workflows/a11y-live.yml b/.github/workflows/a11y-live.yml
new file mode 100644
index 0000000..47d5d0c
--- /dev/null
+++ b/.github/workflows/a11y-live.yml
@@ -0,0 +1,43 @@
+name: Live header accessibility audit
+
+# Post-deploy / scheduled Lighthouse on production mim4u.org (requires Chrome).
+on:
+ schedule:
+ - cron: '0 14 * * 1'
+ workflow_dispatch:
+
+jobs:
+ lighthouse-a11y:
+ name: Lighthouse + WCAG header audit (live)
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+
+ - name: Setup Chrome
+ uses: browser-actions/setup-chrome@v1
+ id: setup-chrome
+
+ - name: Run live header accessibility audit
+ run: |
+ export CHROME_PATH="${{ steps.setup-chrome.outputs.chrome-path }}"
+ if [[ -z "$CHROME_PATH" ]]; then
+ CHROME_PATH="$(command -v google-chrome || command -v chromium-browser)"
+ fi
+ export CHROME_PATH
+ npm run a11y:header-audit:live
+
+ - name: Upload audit reports
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: mim4u-a11y-reports
+ path: |
+ reports/status/mim4u-header-wcag-audit-latest.json
+ reports/status/lighthouse-a11y-mim4u-latest.json
+ if-no-files-found: warn
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 096633d..12b41f4 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -2,86 +2,47 @@ name: Build and Deploy
on:
push:
- branches: [ main ]
+ branches: [main]
pull_request:
- branches: [ main ]
+ branches: [main]
jobs:
- test:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
-
- - name: Setup Node.js
- uses: actions/setup-node@v3
- with:
- node-version: '18'
- cache: 'npm'
-
- - name: Install dependencies
- run: npm ci
-
- - name: Run type checking
- run: npm run type-check
-
- - name: Run linting
- run: npm run lint
-
- - name: Run tests
- run: npm run test:ci
-
- - name: Security audit
- run: npm audit --audit-level moderate
-
- build:
- needs: test
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
-
- - name: Setup Node.js
- uses: actions/setup-node@v3
- with:
- node-version: '18'
- cache: 'npm'
-
- - name: Install dependencies
- run: npm ci
-
- - name: Build application
- run: npm run build
- env:
- VITE_APP_VERSION: ${{ github.sha }}
- VITE_BUILD_TIME: ${{ github.event.head_commit.timestamp }}
-
- - name: Analyze bundle size
- run: npx bundlesize
-
- - name: Upload build artifacts
- uses: actions/upload-artifact@v3
- with:
- name: dist
- path: dist/
-
+ validate:
+ uses: ./.github/workflows/validate.yml
+
deploy:
- needs: build
+ needs: validate
runs-on: ubuntu-latest
- if: github.ref == 'refs/heads/main'
-
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
+
+ permissions:
+ contents: write
+ pages: write
+ id-token: write
+
steps:
- - uses: actions/checkout@v3
-
- - name: Download build artifacts
- uses: actions/download-artifact@v3
- with:
- name: dist
- path: dist/
-
- - name: Deploy to GitHub Pages
- uses: peaceiris/actions-gh-pages@v3
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- publish_dir: ./dist
- cname: miraclesinmotion.org
\ No newline at end of file
+ - uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: npm
+
+ - name: Install ImageMagick (brand asset prebuild)
+ run: sudo apt-get update && sudo apt-get install -y imagemagick
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build application
+ run: npm run build
+ env:
+ VITE_APP_VERSION: ${{ github.sha }}
+
+ - name: Deploy to GitHub Pages
+ uses: peaceiris/actions-gh-pages@v4
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./dist
+ cname: mim4u.org
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
new file mode 100644
index 0000000..0271d23
--- /dev/null
+++ b/.github/workflows/validate.yml
@@ -0,0 +1,68 @@
+name: Validate
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ workflow_call:
+
+concurrency:
+ group: validate-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test-and-a11y:
+ name: Tests + header WCAG audit
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Type check
+ run: npm run type-check
+
+ - name: Run unit tests
+ run: npm run test:ci
+
+ - name: Header WCAG contrast audit (token pairs)
+ run: npm run a11y:header-audit
+
+ build:
+ name: Production build
+ runs-on: ubuntu-latest
+ needs: test-and-a11y
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: npm
+
+ - name: Install ImageMagick (brand asset prebuild)
+ run: sudo apt-get update && sudo apt-get install -y imagemagick
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build
+ run: npm run build
+
+ - name: Upload dist artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: dist
+ path: dist/
+ if-no-files-found: error
diff --git a/package.json b/package.json
index 736042b..f3376d7 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,10 @@
"prebuild": "npm run brand:export",
"build": "tsc && vite build",
"test": "vitest run",
+ "test:ci": "vitest run --reporter=verbose",
"test:watch": "vitest",
+ "type-check": "tsc --noEmit",
+ "validate:ci": "npm run type-check && npm run test:ci && npm run a11y:header-audit",
"a11y:header-audit": "node scripts/a11y/audit-header-wcag.mjs",
"a11y:header-audit:live": "node scripts/a11y/audit-header-wcag.mjs --lighthouse",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
diff --git a/src/components/__tests__/Navigation.test.tsx b/src/components/__tests__/Navigation.test.tsx
index dc0ef8e..0512df4 100644
--- a/src/components/__tests__/Navigation.test.tsx
+++ b/src/components/__tests__/Navigation.test.tsx
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render } from '@testing-library/react'
-import { screen, fireEvent } from '@testing-library/dom'
+import { screen, fireEvent, within } from '@testing-library/dom'
import '@testing-library/jest-dom'
import { Navigation } from '../Navigation'
@@ -57,8 +57,12 @@ describe('Navigation Component', () => {
it('displays mobile menu when mobileMenuOpen is true', () => {
render()
- expect(screen.getAllByText('Stories').length).toBeGreaterThanOrEqual(1)
- expect(screen.getByText('Events')).toBeInTheDocument()
- expect(screen.getByText('Contact')).toBeInTheDocument()
+ const mobileMenu = document.getElementById('mobile-menu')
+ expect(mobileMenu).toBeTruthy()
+
+ const menu = within(mobileMenu!)
+ expect(menu.getByText('Stories')).toBeInTheDocument()
+ expect(menu.getByText('Events')).toBeInTheDocument()
+ expect(menu.getByText('Contact')).toBeInTheDocument()
})
})
diff --git a/src/components/sections/__tests__/HeroSection.test.tsx b/src/components/sections/__tests__/HeroSection.test.tsx
index 0d6758e..b2741b0 100644
--- a/src/components/sections/__tests__/HeroSection.test.tsx
+++ b/src/components/sections/__tests__/HeroSection.test.tsx
@@ -37,10 +37,10 @@ describe('HeroSection Component', () => {
it('renders call-to-action buttons', () => {
render()
-
- const donateButton = screen.getByRole('link', { name: /Donate Now/ })
- const helpButton = screen.getByRole('link', { name: /Get Help/ })
-
+
+ const donateButton = screen.getByRole('link', { name: /Donate now to Miracles in Motion/i })
+ const helpButton = screen.getByRole('link', { name: /Get help — request assistance/i })
+
expect(donateButton).toHaveAttribute('href', '#/donate')
expect(helpButton).toHaveAttribute('href', '#/request-assistance')
})
diff --git a/src/test/setup.ts b/src/test/setup.ts
index cb80ef2..18124ac 100644
--- a/src/test/setup.ts
+++ b/src/test/setup.ts
@@ -33,7 +33,8 @@ global.ResizeObserver = MockResizeObserver
// Mock matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
- value: vi.fn().mockImplementation(query => ({
+ configurable: true,
+ value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
@@ -53,26 +54,11 @@ const localStorageMock = {
clear: vi.fn(),
}
Object.defineProperty(window, 'localStorage', {
- value: localStorageMock
+ configurable: true,
+ value: localStorageMock,
})
-// Mock window.location
-delete (window as any).location
-
-Object.defineProperty(window, 'location', {
- writable: true,
- value: {
- ...window.location,
- hash: '#/',
- pathname: '/',
- get search() {
- return '';
- },
- get href() {
- return 'http://localhost:3000/';
- },
- assign: vi.fn(),
- replace: vi.fn(),
- reload: vi.fn(),
- },
-});
\ No newline at end of file
+// Hash-router default — avoid deleting window.location (breaks Vitest jsdom teardown)
+if (window.location.hash !== '#/') {
+ window.history.replaceState({}, '', '/#/')
+}
diff --git a/vitest.config.ts b/vitest.config.ts
index d74862e..b90d058 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -10,6 +10,11 @@ export default defineConfig({
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
css: true,
+ environmentOptions: {
+ jsdom: {
+ url: 'http://localhost:3000/#/',
+ },
+ },
coverage: {
reporter: ['text', 'json', 'html'],
exclude: [