diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4081a7d8..0d4c8a0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,6 +110,9 @@ jobs: - name: Run tests run: cargo test --release --all-features + - name: Run benchmarks + run: cargo bench --no-run 2>/dev/null || echo "No benchmarks defined; skipping." + # Use stellar contract build instead of raw cargo build --target wasm32 # This ensures proper metadata, optimizer flags, and correct wasm output paths - name: Build WASM contracts @@ -171,6 +174,48 @@ jobs: path: frontend/.next/ retention-days: 7 + # Phase 2: Secondary security & performance checks + # Added as part of Issue #463 – DevOps Pipeline Phase 2 + performance-audit: + name: Performance & Secondary Checks + runs-on: ubuntu-latest + needs: [ rust-tests, frontend-tests ] + defaults: + run: + working-directory: ./frontend + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: | + npm ci + npm install @rollup/rollup-linux-x64-gnu --save-optional + npm install lightningcss-linux-x64-gnu --save-optional + + - name: Lint checks (secondary) + run: npm run lint -- --max-warnings 0 + + - name: Run tests with coverage + run: npm test -- --coverage --reporter=verbose + + - name: Audit dependencies (secondary) + run: npm audit --audit-level=high + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: frontend/coverage/ + retention-days: 7 + frontend-e2e: name: Frontend E2E (Playwright) runs-on: ubuntu-latest @@ -219,7 +264,7 @@ jobs: build-verification: name: Build Verification runs-on: ubuntu-latest - needs: [ rust-tests, frontend-tests, frontend-e2e ] + needs: [ rust-tests, frontend-tests, frontend-e2e, performance-audit ] steps: - name: All builds passed run: echo "All upstream jobs completed successfully" diff --git a/frontend/components/__tests__/accessibility.test.tsx b/frontend/components/__tests__/accessibility.test.tsx index 5f3df376..b1872c05 100644 --- a/frontend/components/__tests__/accessibility.test.tsx +++ b/frontend/components/__tests__/accessibility.test.tsx @@ -6,6 +6,17 @@ import { describe, it, expect } from "vitest"; import { Input } from "@/components/ui/input"; import { FormStepIndicator } from "@/components/forms/FormStepIndicator"; import EventTypeSelector from "@/components/forms/EventTypeSelector"; +import { Button } from "@/components/ui/button"; +import { Navigation } from "@/components/layouts/Navigation"; +import { Footer } from "@/components/layouts/Footer"; +import { SkipToContentLink } from "@/components/SkipToContentLink"; +import { Features } from "@/components/layouts/Features"; +import { ProblemStats } from "@/components/layouts/ProblemStats"; +import { HowItWorks } from "@/components/layouts/HowItWorks"; +import { UseCases } from "@/components/layouts/UseCases"; +import { TrustBlockchain } from "@/components/layouts/TrustBlockchain"; +import { Hero } from "@/components/layouts/Hero"; +import { CTA } from "@/components/layouts/CTA"; function expectNoViolations(results: AxeCore.AxeResults) { const violations = results.violations; @@ -112,6 +123,145 @@ describe("Accessibility", () => { }); }); + describe("Button component", () => { + it("renders with accessible role", () => { + const { getByRole } = render(); + expect(getByRole("button", { name: "Submit" })).toBeInTheDocument(); + }); + + it("handles disabled state with aria-disabled", () => { + const { getByRole } = render(); + const button = getByRole("button", { name: "Disabled" }); + expect(button).toBeDisabled(); + }); + + it("has no axe violations", async () => { + const { container } = render(); + const results = await axe(container); + expectNoViolations(results); + }); + }); + + describe("Navigation component", () => { + it("has aria-label on nav element", () => { + const { getByRole } = render(); + const nav = getByRole("navigation"); + expect(nav).toHaveAttribute("aria-label", "Main navigation"); + }); + + it("has accessible hamburger button on mobile", () => { + const { getByLabelText } = render(); + const menuButton = getByLabelText("Open menu"); + expect(menuButton).toHaveAttribute("aria-expanded", "false"); + expect(menuButton).toHaveAttribute("aria-label", "Open menu"); + }); + + it("toggles mobile menu on hamburger click", async () => { + const user = userEvent.setup(); + const { getByLabelText } = render(); + + // Initially closed + const openButton = getByLabelText("Open menu"); + expect(openButton).toHaveAttribute("aria-expanded", "false"); + + // Click to open + await user.click(openButton); + const closeButton = getByLabelText("Close menu"); + expect(closeButton).toHaveAttribute("aria-expanded", "true"); + + // Mobile nav links are now visible — look for Features link in the mobile panel + const mobileLinks = closeButton.closest("nav")?.querySelectorAll("a"); + const featuresLink = Array.from(mobileLinks || []).find( + (el) => el.textContent?.trim() === "Features" && el.closest(".space-y-1") + ); + expect(featuresLink).toBeTruthy(); + + // Click to close + await user.click(closeButton); + expect(getByLabelText("Open menu")).toHaveAttribute("aria-expanded", "false"); + }); + + it("has no axe violations", async () => { + const { container } = render(); + const results = await axe(container); + expectNoViolations(results); + }); + }); + + describe("Footer component", () => { + it("has aria-labels on social media links", () => { + const { getByLabelText } = render(