diff --git a/.github/workflows/run-ci-cd.yaml b/.github/workflows/run-ci-cd.yaml index b3dfaccab6..b8295fa1af 100644 --- a/.github/workflows/run-ci-cd.yaml +++ b/.github/workflows/run-ci-cd.yaml @@ -271,6 +271,40 @@ jobs: docker run --env-file frontend/.env.example owasp/nest:test-frontend-e2e-latest pnpm run test:e2e timeout-minutes: 10 + run-frontend-a11y-tests: + name: Run frontend accessibility tests + needs: + - scan-code + - scan-ci-dependencies + permissions: + contents: read + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: Set up Docker buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f + + - name: Build frontend a11y-testing image + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 + with: + cache-from: | + type=gha + type=registry,ref=owasp/nest:test-frontend-a11y-cache + cache-to: | + type=gha,compression=zstd + context: frontend + file: docker/frontend/Dockerfile.a11y.test + load: true + platforms: linux/amd64 + tags: owasp/nest:test-frontend-a11y-latest + + - name: Run frontend a11y tests + run: | + docker run --env-file frontend/.env.example owasp/nest:test-frontend-a11y-latest pnpm run test:a11y + timeout-minutes: 10 + set-release-version: name: Set release version outputs: @@ -297,6 +331,7 @@ jobs: github.ref == 'refs/heads/main' needs: - run-backend-tests + - run-frontend-a11y-tests - run-frontend-e2e-tests - run-frontend-unit-tests - set-release-version @@ -662,6 +697,7 @@ jobs: github.event.action == 'published' needs: - run-backend-tests + - run-frontend-a11y-tests - run-frontend-e2e-tests - run-frontend-unit-tests - set-release-version diff --git a/docker/frontend/Dockerfile.a11y.test b/docker/frontend/Dockerfile.a11y.test new file mode 100644 index 0000000000..05c19451ce --- /dev/null +++ b/docker/frontend/Dockerfile.a11y.test @@ -0,0 +1,27 @@ +FROM node:24-alpine + +ENV FORCE_COLOR=1 \ + NPM_CACHE="/app/.npm" \ + PNPM_HOME="/pnpm" + +ENV NPM_CONFIG_RETRY=5 \ + NPM_CONFIG_TIMEOUT=30000 \ + PATH="$PNPM_HOME:$PATH" + +RUN --mount=type=cache,target=${NPM_CACHE} \ + npm install --ignore-scripts -g pnpm --cache ${NPM_CACHE} + +WORKDIR /app + +COPY --chmod=444 --chown=root:root package.json pnpm-lock.yaml ./ +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install --frozen-lockfile --ignore-scripts && \ + chown node:node /app + +COPY __tests__/a11y __tests__/a11y +COPY __tests__/mockData __tests__/mockData +COPY .pnpmrc jest.config.ts jest.setup.ts tsconfig.json ./ +COPY public public +COPY src src + +USER node diff --git a/docker/frontend/Dockerfile.e2e.test b/docker/frontend/Dockerfile.e2e.test index 681ef18691..bd03a0130c 100644 --- a/docker/frontend/Dockerfile.e2e.test +++ b/docker/frontend/Dockerfile.e2e.test @@ -18,7 +18,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ pnpm install --frozen-lockfile --ignore-scripts COPY __tests__/e2e __tests__/e2e -COPY __tests__/unit/data __tests__/unit/data +COPY __tests__/mockData __tests__/mockData COPY .pnpmrc next.config.ts postcss.config.js playwright.config.ts tailwind.config.mjs tsconfig.json ./ COPY public public COPY src src diff --git a/docker/frontend/Dockerfile.unit.test b/docker/frontend/Dockerfile.unit.test index 7a0e19e2bd..b93d0f6aec 100644 --- a/docker/frontend/Dockerfile.unit.test +++ b/docker/frontend/Dockerfile.unit.test @@ -19,6 +19,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ chown node:node /app COPY __tests__/unit __tests__/unit +COPY __tests__/mockData __tests__/mockData COPY .pnpmrc jest.config.ts jest.setup.ts tsconfig.json ./ COPY public public COPY src src diff --git a/frontend/Makefile b/frontend/Makefile index 468a16c1fb..c61cd6aed7 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -69,8 +69,16 @@ shell-frontend: test-frontend: \ test-frontend-unit \ + test-frontend-a11y \ test-frontend-e2e +test-frontend-a11y: + @DOCKER_BUILDKIT=1 NEXT_PUBLIC_ENVIRONMENT=local docker build \ + --cache-from nest-test-frontend-a11y \ + -f docker/frontend/Dockerfile.a11y.test frontend \ + -t nest-test-frontend-a11y + @docker run --env-file frontend/.env.example --rm nest-test-frontend-a11y pnpm run test:a11y + test-frontend-e2e: @DOCKER_BUILDKIT=1 NEXT_PUBLIC_ENVIRONMENT=local docker build \ --cache-from nest-test-frontend-e2e \ diff --git a/frontend/__tests__/a11y/components/ActionButton.a11y.test.tsx b/frontend/__tests__/a11y/components/ActionButton.a11y.test.tsx new file mode 100644 index 0000000000..802871689f --- /dev/null +++ b/frontend/__tests__/a11y/components/ActionButton.a11y.test.tsx @@ -0,0 +1,32 @@ +import { render } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import ActionButton from 'components/ActionButton' + +expect.extend(toHaveNoViolations) + +describe('ActionButton Accessibility', () => { + it('should not have any accessibility violations when no url is provided', async () => { + const { container } = render(Sample Text) + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) + + it('should not have any accessibility violations when url is provided', async () => { + const { container } = render(Visit Site) + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) + + it('should not have any accessibility violations when tooltipLabel is provided', async () => { + const { baseElement } = render( + Test Button + ) + const results = await axe(baseElement) + + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/AnchorTitle.a11y.test.tsx b/frontend/__tests__/a11y/components/AnchorTitle.a11y.test.tsx new file mode 100644 index 0000000000..fa1a1a201e --- /dev/null +++ b/frontend/__tests__/a11y/components/AnchorTitle.a11y.test.tsx @@ -0,0 +1,15 @@ +import { render } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import AnchorTitle from 'components/AnchorTitle' + +expect.extend(toHaveNoViolations) + +describe('AnchorTitle Accessibility', () => { + it('should not have any accessibility violations', async () => { + const { container } = render() + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/AutoScrollToTop.a11y.test.tsx b/frontend/__tests__/a11y/components/AutoScrollToTop.a11y.test.tsx new file mode 100644 index 0000000000..cb7418b321 --- /dev/null +++ b/frontend/__tests__/a11y/components/AutoScrollToTop.a11y.test.tsx @@ -0,0 +1,27 @@ +import { render } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import AutoScrollToTop from 'components/AutoScrollToTop' + +expect.extend(toHaveNoViolations) + +jest.mock('next/navigation', () => ({ + usePathname: () => '/test-path', +})) + +beforeAll(() => { + window.scrollTo = jest.fn() +}) + +afterAll(() => { + jest.clearAllMocks() +}) + +describe('AutoScrollToTop Accessibility', () => { + it('should not have any accessibility violations', async () => { + const { container } = render() + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/Badges.a11y.test.tsx b/frontend/__tests__/a11y/components/Badges.a11y.test.tsx new file mode 100644 index 0000000000..fb344b1770 --- /dev/null +++ b/frontend/__tests__/a11y/components/Badges.a11y.test.tsx @@ -0,0 +1,28 @@ +import { render } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import Badges from 'components/Badges' + +const defaultProps = { + name: 'Test Badge', + cssClass: 'medal', +} + +expect.extend(toHaveNoViolations) + +describe('Badges Accessibility', () => { + it('should not have any accessibility violations when tooltip is enabled', async () => { + const { baseElement } = render() + + const results = await axe(baseElement) + + expect(results).toHaveNoViolations() + }) + + it('should not have any accessibility violations when tooltip is disabled', async () => { + const { baseElement } = render() + + const results = await axe(baseElement) + + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/BarChart.a11y.test.tsx b/frontend/__tests__/a11y/components/BarChart.a11y.test.tsx new file mode 100644 index 0000000000..5829397c99 --- /dev/null +++ b/frontend/__tests__/a11y/components/BarChart.a11y.test.tsx @@ -0,0 +1,83 @@ +import { render } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import { ReactNode } from 'react' +import BarChart from 'components/BarChart' + +const mockProps = { + title: 'Calories Burned', + labels: ['Mon', 'Tue', 'Wed'], + days: [200, 150, 100], + requirements: [180, 170, 90], +} + +jest.mock('react-apexcharts', () => { + return function MockChart(props: { + options: unknown + series: unknown + height: number + type: string + }) { + const mockOptions = props.options as Record + + return ( +
+ ) + } +}) + +jest.mock('next/dynamic', () => { + return function mockDynamic() { + return jest.requireMock('react-apexcharts') + } +}) + +jest.mock('next-themes', () => ({ + ThemeProvider: ({ children, ...props }: { children: ReactNode; [key: string]: unknown }) => ( +
{children}
+ ), + useTheme: () => ({ theme: 'light', setTheme: jest.fn() }), +})) + +jest.mock('components/AnchorTitle', () => { + return function MockAnchorTitle({ title }: { title: string }) { + return
{title}
+ } +}) + +jest.mock('components/SecondaryCard', () => { + return function MockSecondaryCard({ + title, + icon, + children, + }: { + title: ReactNode + icon?: unknown + children: ReactNode + }) { + return ( +
+
{title}
+ {icon &&
icon
} +
{children}
+
+ ) + } +}) + +expect.extend(toHaveNoViolations) + +describe('BarChart Accessibility', () => { + it('should not have any accessibility violations', async () => { + const { container } = render() + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/BreadCrumbs.a11y.test.tsx b/frontend/__tests__/a11y/components/BreadCrumbs.a11y.test.tsx new file mode 100644 index 0000000000..775a35a797 --- /dev/null +++ b/frontend/__tests__/a11y/components/BreadCrumbs.a11y.test.tsx @@ -0,0 +1,15 @@ +import { Breadcrumbs } from '@heroui/react' +import { render } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' + +expect.extend(toHaveNoViolations) + +describe('Breadcrumbs a11y', () => { + it('should not have any accessibility violations', async () => { + const { container } = render() + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/BreadCrumbsWrapper.a11y.test.tsx b/frontend/__tests__/a11y/components/BreadCrumbsWrapper.a11y.test.tsx new file mode 100644 index 0000000000..03588a6b4d --- /dev/null +++ b/frontend/__tests__/a11y/components/BreadCrumbsWrapper.a11y.test.tsx @@ -0,0 +1,34 @@ +import { render } from '@testing-library/react' +import { useBreadcrumbs } from 'hooks/useBreadcrumbs' +import { axe, toHaveNoViolations } from 'jest-axe' +import { usePathname } from 'next/navigation' +import BreadCrumbsWrapper from 'components/BreadCrumbsWrapper' + +expect.extend(toHaveNoViolations) + +jest.mock('next/navigation', () => ({ + usePathname: jest.fn(), +})) + +jest.mock('hooks/useBreadcrumbs', () => ({ + useBreadcrumbs: jest.fn(), +})) + +describe('BreadcrumbsWrapper a11y', () => { + beforeAll(() => { + ;(usePathname as jest.Mock).mockReturnValue('/projects/test-project') + ;(useBreadcrumbs as jest.Mock).mockReturnValue([ + { title: 'Home', path: '/' }, + { title: 'Projects', path: '/projects' }, + { title: 'Test Project', path: '/projects/test-project' }, + ]) + }) + + it('should not have any accessibility violations', async () => { + const { container } = render() + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/CalendarButton.a11y.test.tsx b/frontend/__tests__/a11y/components/CalendarButton.a11y.test.tsx new file mode 100644 index 0000000000..54a7fa84a1 --- /dev/null +++ b/frontend/__tests__/a11y/components/CalendarButton.a11y.test.tsx @@ -0,0 +1,38 @@ +import { render } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import { FaCalendarAlt } from 'react-icons/fa' +import CalendarButton from 'components/CalendarButton' + +expect.extend(toHaveNoViolations) + +const mockEvent = { + title: 'Test Event', + description: 'Test description', + location: 'Test Location', + startDate: '2025-12-01', + endDate: '2025-12-02', +} + +describe('CalendarButton Accessibility', () => { + it('should not have any accessibility violations as an icon-only button', async () => { + const { container } = render() + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) + + it('should not have any accessibility violations when showLabel is enabled', async () => { + const { container } = render() + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) + + it('should not have any accessibility violations when custom icon is provided', async () => { + const { container } = render(} />) + const results = await axe(container) + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/Card.a11y.test.tsx b/frontend/__tests__/a11y/components/Card.a11y.test.tsx new file mode 100644 index 0000000000..70bfb52418 --- /dev/null +++ b/frontend/__tests__/a11y/components/Card.a11y.test.tsx @@ -0,0 +1,83 @@ +import { render } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import { ReactNode } from 'react' +import { FaCrown } from 'react-icons/fa6' +import Card from 'components/Card' + +interface MockLinkProps { + children: ReactNode + href: string + target?: string + rel?: string + className?: string +} + +jest.mock('components/MarkdownWrapper', () => ({ + __esModule: true, + default: ({ children }) =>
{children}
, +})) + +jest.mock('next/link', () => { + return function MockedLink({ children, href, ...props }: MockLinkProps) { + return ( + + {children} + + ) + } +}) + +const baseProps = { + title: 'Test Project', + url: 'https://github.com/test/project', + summary: 'This is a test project summary', + button: { + label: 'View Project', + url: 'https://github.com/test', + icon: github, + onclick: jest.fn(), + }, +} + +expect.extend(toHaveNoViolations) + +describe('Card Accessibility', () => { + it('should not have any accessibility violations with minimal props', async () => { + const { container } = render() + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) + + it('should not have any accessibility violations when level is provided', async () => { + const { container } = render( + + ) + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) + + it('should not have any accessibility violations when project name is provided', async () => { + const { container } = render() + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) + + it('should not have any accessibility violations when socials is provided', async () => { + const { container } = render( + + ) + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/CardDetailsPage.a11y.test.tsx b/frontend/__tests__/a11y/components/CardDetailsPage.a11y.test.tsx new file mode 100644 index 0000000000..b1e63f91ae --- /dev/null +++ b/frontend/__tests__/a11y/components/CardDetailsPage.a11y.test.tsx @@ -0,0 +1,213 @@ +import { mockChapterData } from '@mockData/mockChapterData' +import { render } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import React from 'react' +import { FaCode, FaTags } from 'react-icons/fa6' +import { ExperienceLevelEnum } from 'types/__generated__/graphql' +import { DetailsCardProps } from 'types/card' +import DetailsCard from 'components/CardDetailsPage' + +jest.mock('next/link', () => { + return function MockLink({ + children, + href, + ...props + }: { + children: React.ReactNode + href: string + [key: string]: unknown + }) { + return ( + + {children} + + ) + } +}) + +jest.mock('components/ChapterMapWrapper', () => ({ + __esModule: true, + default: ({ + geoLocData: _geoLocData, + showLocal, + style, + showLocationSharing: _showLocationSharing, + ...otherProps + }: { + geoLocData?: unknown + showLocal: boolean + style: React.CSSProperties + showLocationSharing?: boolean + [key: string]: unknown + }) => { + return ( +
+ Chapter Map {showLocal ? '(Local)' : ''} +
+ ) + }, +})) + +jest.mock('react-apexcharts', () => ({ + __esModule: true, + default: () =>
, +})) + +jest.mock('next/dynamic', () => ({ + __esModule: true, + default: () => { + return function MockDynamicComponent({ + children, + ...props + }: React.ComponentPropsWithoutRef<'div'>) { + return
{children}
+ } + }, +})) + +jest.mock('next-auth/react', () => ({ + useSession: jest.fn(() => ({ data: null, status: 'unauthenticated' })), +})) + +const mockHealthMetricsData = [ + { + ageDays: 365, + ageDaysRequirement: 365, + id: 'test-id', + createdAt: '2023-01-01', + contributorsCount: 10, + forksCount: 5, + isFundingRequirementsCompliant: true, + isLeaderRequirementsCompliant: true, + lastCommitDays: 1, + lastCommitDaysRequirement: 30, + lastPullRequestDays: 2, + lastPullRequestDaysRequirement: 30, + lastReleaseDays: 10, + lastReleaseDaysRequirement: 90, + openIssuesCount: 5, + openPullRequestsCount: 3, + owaspPageLastUpdateDays: 30, + owaspPageLastUpdateDaysRequirement: 90, + projectName: 'Test Project', + projectKey: 'test-project', + recentReleasesCount: 2, + score: 85, + starsCount: 100, + totalIssuesCount: 20, + totalReleasesCount: 5, + unassignedIssuesCount: 2, + unansweredIssuesCount: 1, + }, +] + +const mockStats = [ + { + icon: FaCode, + pluralizedName: 'repositories', + unit: '', + value: 10, + }, + { + icon: FaTags, + pluralizedName: 'stars', + unit: '', + value: 100, + }, +] + +const mockDetails = [ + { label: 'Created', value: '2023-01-01' }, + { label: 'Leaders', value: 'John Doe, Jane Smith' }, + { label: 'Status', value: 'Active' }, +] + +const defaultProps: DetailsCardProps = { + title: 'Test Project', + description: 'A test project for demonstration', + type: 'project', + details: mockDetails, + stats: mockStats, + isActive: true, + showAvatar: true, + languages: ['JavaScript', 'TypeScript'], + topics: ['web', 'frontend'], + repositories: [], + recentIssues: [], + recentMilestones: [], + recentReleases: [], + pullRequests: [], + topContributors: [], + healthMetricsData: mockHealthMetricsData, + socialLinks: [], +} + +expect.extend(toHaveNoViolations) + +describe('CardDetailsPage a11y', () => { + it('should have no accessibility violations', async () => { + const { container } = render() + const results = await axe(container) + expect(results).toHaveNoViolations() + }) + + it('should have no violations for chapter type', async () => { + const { container } = render( + + ) + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) + + it('should have no violations for program type', async () => { + const { container } = render( + + ) + const results = await axe(container) + expect(results).toHaveNoViolations() + }) + + it('should have no violations in archived state', async () => { + let container: HTMLElement + + await React.act(async () => { + const renderResult = render( + + ) + container = renderResult.container + }) + + const results = await axe(container) + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/ChapterMap.a11y.test.tsx b/frontend/__tests__/a11y/components/ChapterMap.a11y.test.tsx new file mode 100644 index 0000000000..adce4d0b7b --- /dev/null +++ b/frontend/__tests__/a11y/components/ChapterMap.a11y.test.tsx @@ -0,0 +1,170 @@ +import { mockChapterData } from '@mockData/mockChapterData' +import { screen, fireEvent, render, waitFor } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import * as L from 'leaflet' +import React, { useEffect } from 'react' +import ChapterMap from 'components/ChapterMap' + +expect.extend(toHaveNoViolations) + +const mockMap = { + setView: jest.fn().mockReturnThis(), + fitBounds: jest.fn().mockReturnThis(), + scrollWheelZoom: { + enable: jest.fn(), + disable: jest.fn(), + }, +} + +const mockZoomControl = { + addTo: jest.fn().mockReturnThis(), + remove: jest.fn(), +} + +/* eslint-disable @typescript-eslint/naming-convention */ +jest.mock('leaflet', () => ({ + map: jest.fn(() => mockMap), + tileLayer: jest.fn(() => ({ + addTo: jest.fn().mockReturnThis(), + })), + marker: jest.fn(() => ({ + bindPopup: jest.fn().mockReturnThis(), + })), + popup: jest.fn(() => ({ + setContent: jest.fn().mockReturnThis(), + })), + latLngBounds: jest.fn(() => ({})), + Icon: jest.fn(() => ({})), + divIcon: jest.fn(() => ({})), + control: { + zoom: jest.fn(() => mockZoomControl), + }, +})) +/* eslint-enable @typescript-eslint/naming-convention */ + +// Mock CSS imports +jest.mock('leaflet/dist/leaflet.css', () => ({})) +jest.mock('leaflet.markercluster/dist/MarkerCluster.css', () => ({})) +jest.mock('leaflet.markercluster/dist/MarkerCluster.Default.css', () => ({})) +jest.mock('leaflet.markercluster', () => ({})) + +// Mock react-leaflet +jest.mock('react-leaflet', () => ({ + MapContainer: ({ + children, + center, + zoom, + scrollWheelZoom, + style, + zoomControl, + maxBounds, + maxBoundsViscosity, + className, + }: { + children: React.ReactNode + center: L.LatLngExpression + zoom: number + scrollWheelZoom: boolean + style: React.CSSProperties + zoomControl: boolean + maxBounds: L.LatLngBoundsExpression + maxBoundsViscosity: number + className: string + }) => { + useEffect(() => { + L.map('chapter-map', { + worldCopyJump: false, + maxBounds, + maxBoundsViscosity, + scrollWheelZoom, + zoomControl, + }).setView(center, zoom) + }, [center, zoom, scrollWheelZoom, zoomControl, maxBounds, maxBoundsViscosity]) + return ( +
+ {children} +
+ ) + }, + TileLayer: ({ + attribution, + url, + className, + }: { + attribution: string + url: string + className: string + }) => { + useEffect(() => { + L.tileLayer(url, { attribution, className }).addTo(mockMap as unknown as L.Map) + }, [url, attribution, className]) + return null + }, + Marker: ({ + children, + position, + icon, + }: { + children: React.ReactNode + position: L.LatLngExpression + icon: L.Icon + }) => { + useEffect(() => { + const marker = L.marker(position, { icon }) + marker.bindPopup(L.popup().setContent('mock content')) + }, [position, icon]) + return
{children}
+ }, + Popup: ({ children }: { children: React.ReactNode }) =>
{children}
, + useMap: () => mockMap as unknown as L.Map, +})) + +// Mock react-leaflet-cluster +jest.mock('react-leaflet-cluster', () => ({ + __esModule: true, + default: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})) + +const defaultProps = { + geoLocData: mockChapterData.chapters, + showLocal: false, + style: { width: '100%', height: '400px' }, +} + +describe('ChapterMap a11y', () => { + it('should not have any accessibility violations in locked state', async () => { + const { baseElement } = render() + + const results = await axe(baseElement) + + expect(results).toHaveNoViolations() + }) + + it('should not have any accessibility violations when map is unlocked', async () => { + const { baseElement } = render() + + const unlockButton = screen.getByLabelText('Unlock map') + fireEvent.click(unlockButton) + + await waitFor(() => expect(screen.getByLabelText(/Share location/i)).toBeInTheDocument()) + + const results = await axe(baseElement) + + expect(results).toHaveNoViolations() + }) + + it('should not have any accessibility violations when user location is shared', async () => { + const { baseElement } = render( + + ) + + const results = await axe(baseElement) + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/ContributionHeatmap.a11y.test.tsx b/frontend/__tests__/a11y/components/ContributionHeatmap.a11y.test.tsx new file mode 100644 index 0000000000..f1e7cc1bf7 --- /dev/null +++ b/frontend/__tests__/a11y/components/ContributionHeatmap.a11y.test.tsx @@ -0,0 +1,94 @@ +import { render, screen } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import { ReactNode } from 'react' +import ContributionHeatmap from 'components/ContributionHeatmap' + +expect.extend(toHaveNoViolations) + +jest.mock('react-apexcharts', () => { + return function MockChart(props: { + options: unknown + series: unknown + height: string | number + type: string + }) { + const mockSeries = props.series as Array<{ + name: string + data: Array<{ x: string; y: number; date: string }> + }> + const mockOptions = props.options as Record + + if (mockOptions.tooltip && typeof mockOptions.tooltip === 'object') { + const tooltip = mockOptions.tooltip as { custom?: (...args: unknown[]) => unknown } + if (tooltip.custom) { + if (mockSeries[0]?.data.length > 0) { + tooltip.custom({ + seriesIndex: 0, + dataPointIndex: 0, + w: { config: { series: mockSeries } }, + }) + } + tooltip.custom({ + seriesIndex: 0, + dataPointIndex: 999, + w: { config: { series: mockSeries } }, + }) + } + } + + return ( +
+ {mockSeries.map((series) => ( +
+ {series.name}: {series.data.length} data points +
+ ))} +
+ ) + } +}) + +jest.mock('next-themes', () => ({ + useTheme: () => ({ theme: 'light', setTheme: jest.fn() }), + ThemeProvider: ({ children }: { children: ReactNode }) =>
{children}
, +})) + +const mockData: Record = { + '2024-01-01': 5, + '2024-01-02': 8, + '2024-01-03': 12, + '2024-01-04': 15, + '2024-01-05': 0, + '2024-01-08': 3, + '2024-01-15': 20, +} +const defaultProps = { + contributionData: mockData, + startDate: '2024-01-01', + endDate: '2024-01-31', +} + +describe('ContributionHeatmap Accessibility', () => { + it('should not have any accessibility violations', async () => { + const { container } = render() + + await screen.findByTestId('mock-heatmap-chart') + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) + + it('should not have any accessibility violations when title is provided', async () => { + const { container } = render() + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/ContributorAvatar.a11y.test.tsx b/frontend/__tests__/a11y/components/ContributorAvatar.a11y.test.tsx new file mode 100644 index 0000000000..b6fa9d9e8f --- /dev/null +++ b/frontend/__tests__/a11y/components/ContributorAvatar.a11y.test.tsx @@ -0,0 +1,57 @@ +import { render } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import { ReactNode } from 'react' +import { Contributor } from 'types/contributor' +import ContributorAvatar from 'components/ContributorAvatar' + +expect.extend(toHaveNoViolations) + +jest.mock('@heroui/tooltip', () => ({ + Tooltip: ({ children, content, id }: { children: ReactNode; content: string; id: string }) => ( +
+
{children}
+ +
+ ), +})) + +jest.mock('next/link', () => { + return ({ + children, + href, + target, + rel, + }: { + children: ReactNode + href: string + target?: string + rel?: string + }) => ( + + {children} + + ) +}) + +const mockGitHubContributor: Contributor = { + login: 'jane-doe', + name: 'Jane Doe', + avatarUrl: 'https://avatars.githubusercontent.com/u/12345', + contributionsCount: 15, + projectName: 'OWASP-Nest', + projectKey: 'test-key', +} + +describe('ContributorAvatar a11y', () => { + it('should not have any accessibility violations', async () => { + const { container } = render( + + ) + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/DashboardCard.a11y.test.tsx b/frontend/__tests__/a11y/components/DashboardCard.a11y.test.tsx new file mode 100644 index 0000000000..3f97aa505c --- /dev/null +++ b/frontend/__tests__/a11y/components/DashboardCard.a11y.test.tsx @@ -0,0 +1,23 @@ +import { render } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import { FaUser } from 'react-icons/fa' +import DashboardCard from 'components/DashboardCard' + +expect.extend(toHaveNoViolations) + +const baseProps = { + title: 'Test Card', + icon: FaUser, + className: undefined, + stats: undefined, +} + +describe('DashboardCard a11y', () => { + it('should not have any accessibility violations', async () => { + const { container } = render() + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/DisplayIcon.a11y.test.tsx b/frontend/__tests__/a11y/components/DisplayIcon.a11y.test.tsx new file mode 100644 index 0000000000..0376309147 --- /dev/null +++ b/frontend/__tests__/a11y/components/DisplayIcon.a11y.test.tsx @@ -0,0 +1,25 @@ +import { render } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import { Icon } from 'types/icon' +import DisplayIcon from 'components/DisplayIcon' + +expect.extend(toHaveNoViolations) + +const mockIcons: Icon = { + starsCount: 1250, + forksCount: 350, + contributorsCount: 25, + contributionCount: 25, + issuesCount: 42, + license: 'MIT', +} + +describe('DisplayIcon a11y', () => { + it('should not have any accessibility violations', async () => { + const { container } = render() + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/DonutBarChart.a11y.test.tsx b/frontend/__tests__/a11y/components/DonutBarChart.a11y.test.tsx new file mode 100644 index 0000000000..913858fb4c --- /dev/null +++ b/frontend/__tests__/a11y/components/DonutBarChart.a11y.test.tsx @@ -0,0 +1,38 @@ +import { render } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import { FaChartPie } from 'react-icons/fa' +import DonutBarChart from 'components/DonutBarChart' + +expect.extend(toHaveNoViolations) + +jest.mock('next/dynamic', () => { + return jest.fn(() => { + // Mock Chart component that mimics react-apexcharts + const MockChart = ({ options, series, height, type, ...props }) => ( +
+ ApexCharts Mock +
+ ) + MockChart.displayName = 'Chart' + return MockChart + }) +}) + +describe('DonutBarChart a11y', () => { + it('should not have any accessibility violations', async () => { + const { container } = render( + + ) + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/EntityActions.a11y.test.tsx b/frontend/__tests__/a11y/components/EntityActions.a11y.test.tsx new file mode 100644 index 0000000000..ee85711ccf --- /dev/null +++ b/frontend/__tests__/a11y/components/EntityActions.a11y.test.tsx @@ -0,0 +1,44 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import EntityActions from 'components/EntityActions' + +expect.extend(toHaveNoViolations) + +describe('EntityActions a11y', () => { + it('should not have any accessibility violations', async () => { + const setStatus = jest.fn() + + const { container } = render( + + ) + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) + + it('should not have any accessibility violations when dropDown is open', async () => { + const setStatus = jest.fn() + + const { container } = render( + + ) + + const toggleButton = screen.getByTestId('program-actions-button') + fireEvent.click(toggleButton) + + const results = await axe(container) + + expect(results).toHaveNoViolations() + }) +}) diff --git a/frontend/__tests__/a11y/components/Footer.a11y.test.tsx b/frontend/__tests__/a11y/components/Footer.a11y.test.tsx new file mode 100644 index 0000000000..48443c4ab5 --- /dev/null +++ b/frontend/__tests__/a11y/components/Footer.a11y.test.tsx @@ -0,0 +1,26 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { axe, toHaveNoViolations } from 'jest-axe' +import Footer from 'components/Footer' + +expect.extend(toHaveNoViolations) + +describe('Footer a11y', () => { + it('should not have any accessibility violations', async () => { + const { container } = render(