Skip to content
Open
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
47 changes: 46 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
150 changes: 150 additions & 0 deletions frontend/components/__tests__/accessibility.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -112,6 +123,145 @@ describe("Accessibility", () => {
});
});

describe("Button component", () => {
it("renders with accessible role", () => {
const { getByRole } = render(<Button>Submit</Button>);
expect(getByRole("button", { name: "Submit" })).toBeInTheDocument();
});

it("handles disabled state with aria-disabled", () => {
const { getByRole } = render(<Button disabled>Disabled</Button>);
const button = getByRole("button", { name: "Disabled" });
expect(button).toBeDisabled();
});

it("has no axe violations", async () => {
const { container } = render(<Button>Accessible Button</Button>);
const results = await axe(container);
expectNoViolations(results);
});
});

describe("Navigation component", () => {
it("has aria-label on nav element", () => {
const { getByRole } = render(<Navigation />);
const nav = getByRole("navigation");
expect(nav).toHaveAttribute("aria-label", "Main navigation");
});

it("has accessible hamburger button on mobile", () => {
const { getByLabelText } = render(<Navigation />);
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(<Navigation />);

// 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(<Navigation />);
const results = await axe(container);
expectNoViolations(results);
});
});

describe("Footer component", () => {
it("has aria-labels on social media links", () => {
const { getByLabelText } = render(<Footer />);
expect(getByLabelText("GitHub")).toBeInTheDocument();
expect(getByLabelText("Twitter")).toBeInTheDocument();
expect(getByLabelText("Discord")).toBeInTheDocument();
});

it("has no axe violations", async () => {
const { container } = render(<Footer />);
const results = await axe(container);
expectNoViolations(results);
});
});

describe("SkipToContentLink component", () => {
it("has sr-only class for visibility", () => {
const { getByText } = render(<SkipToContentLink />);
const link = getByText(/skip to main/i);
expect(link).toHaveAttribute("href", "#main-content");
expect(link.className).toContain("sr-only");
});

it("has no axe violations", async () => {
const { container } = render(<SkipToContentLink />);
const results = await axe(container);
expectNoViolations(results);
});
});

describe("Layout components — axe audits", () => {
it("Hero has no axe violations", async () => {
const { container } = render(<Hero />);
const results = await axe(container);
expectNoViolations(results);
});

it("Features has no axe violations", async () => {
const { container } = render(<Features />);
const results = await axe(container);
expectNoViolations(results);
});

it("ProblemStats has no axe violations", async () => {
const { container } = render(<ProblemStats />);
const results = await axe(container);
expectNoViolations(results);
});

it("HowItWorks has no axe violations", async () => {
const { container } = render(<HowItWorks />);
const results = await axe(container);
expectNoViolations(results);
});

it("UseCases has no axe violations", async () => {
const { container } = render(<UseCases />);
const results = await axe(container);
expectNoViolations(results);
});

it("TrustBlockchain has no axe violations", async () => {
const { container } = render(<TrustBlockchain />);
const results = await axe(container);
expectNoViolations(results);
});

it("CTA has no axe violations", async () => {
const { container } = render(<CTA />);
const results = await axe(container);
expectNoViolations(results);
});
});

describe("FormStepIndicator component", () => {
const steps = [
{ id: 1, name: "Basic Info" },
Expand Down
3 changes: 2 additions & 1 deletion frontend/components/layouts/AnimatedSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ export function AnimatedSection({
return (
<div
ref={ref}
aria-hidden={!isVisible}
className={`transition-all duration-700 ease-out ${isVisible
? "opacity-100 translate-y-0"
: "opacity-0 translate-y-8"
} ${className}`}
>
{children}
{isVisible ? children : null}
</div>
);
}
6 changes: 6 additions & 0 deletions frontend/components/layouts/Features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function Features() {
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
Expand All @@ -34,6 +35,7 @@ export function Features() {
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
Expand All @@ -54,6 +56,7 @@ export function Features() {
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
Expand All @@ -74,6 +77,7 @@ export function Features() {
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
Expand All @@ -94,6 +98,7 @@ export function Features() {
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
Expand All @@ -114,6 +119,7 @@ export function Features() {
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
Expand Down
3 changes: 3 additions & 0 deletions frontend/components/layouts/HowItWorks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function HowItWorks() {
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
Expand All @@ -36,6 +37,7 @@ export function HowItWorks() {
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
Expand All @@ -57,6 +59,7 @@ export function HowItWorks() {
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
Expand Down
Loading
Loading