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
87 changes: 87 additions & 0 deletions app/components/SuccessGuide.accessibility.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import { describe, it, expect, vi } from 'vitest';
import { SuccessGuide } from './SuccessGuide';

type MockMotionProps = {
children?: React.ReactNode;
className?: string;
[key: string]: unknown;
};

vi.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: MockMotionProps) => <div {...props}>{children}</div>,
},
AnimatePresence: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
}));

vi.mock('./Icons', () => ({
CloseIcon: () => <svg data-testid="close-icon" aria-hidden="true" />,
}));

vi.mock('@/context/TranslationContext', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));

const defaultProps = {
markdown: '![CommitPulse](https://commitpulse.vercel.app/api/streak?user=testuser)',
onDismiss: vi.fn(),
};

describe('SuccessGuide — Accessibility & Screen Reader Compliance', () => {
it('renders as a named region landmark linked to h2 via aria-labelledby', () => {
const { container } = render(<SuccessGuide {...defaultProps} />);

const region = container.querySelector('[role="region"]');
expect(region).toBeInTheDocument();
expect(region).toHaveAttribute('aria-labelledby', 'success-guide-heading');

const heading = screen.getByRole('heading', { level: 2 });
expect(heading).toHaveAttribute('id', 'success-guide-heading');
expect(heading).toHaveTextContent('success_guide.title');
});

it('renders dismiss button with aria-label and correct button role', () => {
render(<SuccessGuide {...defaultProps} />);

const dismissButton = screen.getByRole('button', { name: 'Dismiss guide' });
expect(dismissButton).toBeInTheDocument();
expect(dismissButton.tagName).toBe('BUTTON');
});

it('renders steps grid with aria-label and all 4 step titles visible', () => {
const { container } = render(<SuccessGuide {...defaultProps} />);

const stepsGrid = container.querySelector('[aria-label="Steps to embed your badge"]');
expect(stepsGrid).toBeInTheDocument();

expect(screen.getByText('success_guide.step_1_title')).toBeInTheDocument();
expect(screen.getByText('success_guide.step_2_title')).toBeInTheDocument();
expect(screen.getByText('success_guide.step_3_title')).toBeInTheDocument();
expect(screen.getByText('success_guide.step_4_title')).toBeInTheDocument();
});

it('renders step numbers as visible text for screen reader sequence announcement', () => {
render(<SuccessGuide {...defaultProps} />);

expect(screen.getByText('01')).toBeInTheDocument();
expect(screen.getByText('02')).toBeInTheDocument();
expect(screen.getByText('03')).toBeInTheDocument();
expect(screen.getByText('04')).toBeInTheDocument();
});

it('renders markdown snippet in code element with aria-label and hides $ symbol', () => {
const { container } = render(<SuccessGuide {...defaultProps} />);

const codeEl = container.querySelector('code[aria-label="Your badge markdown snippet"]');
expect(codeEl).toBeInTheDocument();
expect(codeEl).toHaveTextContent(defaultProps.markdown);

const dollar = container.querySelector('[aria-hidden="true"].select-none');
expect(dollar).toBeInTheDocument();
expect(dollar).toHaveTextContent('$');
});
});
28 changes: 22 additions & 6 deletions app/components/SuccessGuide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,32 @@ export function SuccessGuide({ markdown, onDismiss }: SuccessGuideProps) {
className="max-w-4xl mx-auto mb-12"
>
<div
role="region"
aria-labelledby="success-guide-heading"
className="relative rounded-[2rem] border border-emerald-500/20 bg-[#050505]/80 backdrop-blur-2xl overflow-hidden"
style={{
boxShadow: '0 0 60px -10px rgba(16,185,129,0.15), 0 0 0 1px rgba(16,185,129,0.08) inset',
}}
>
<div className="absolute -top-24 left-1/2 -translate-x-1/2 w-3/4 h-48 bg-emerald-500/10 blur-[80px] rounded-full pointer-events-none" />
<div
aria-hidden="true"
className="absolute -top-24 left-1/2 -translate-x-1/2 w-3/4 h-48 bg-emerald-500/10 blur-[80px] rounded-full pointer-events-none"
/>

<div className="flex items-start justify-between px-8 pt-8 pb-6 border-b border-white/5">
<div className="flex items-center gap-4">
<span className="relative flex h-3 w-3 mt-1">
<span aria-hidden="true" className="relative flex h-3 w-3 mt-1">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex rounded-full h-3 w-3 bg-emerald-500" />
</span>
<div>
<p className="text-xs font-bold uppercase tracking-[0.2em] text-emerald-400 mb-0.5">
{t('success_guide.markdown_copied')}
</p>
<h2 className="text-2xl font-extrabold text-white tracking-tight">
<h2
id="success-guide-heading"
className="text-2xl font-extrabold text-white tracking-tight"
>
{t('success_guide.title')}
</h2>
</div>
Expand All @@ -77,7 +85,10 @@ export function SuccessGuide({ markdown, onDismiss }: SuccessGuideProps) {
</button>
</div>

<div className="grid sm:grid-cols-2 gap-px bg-white/5 border-b border-white/5">
<div
aria-label="Steps to embed your badge"
className="grid sm:grid-cols-2 gap-px bg-white/5 border-b border-white/5"
>
{steps.map((step, i) => (
<motion.div
key={step.n}
Expand All @@ -102,8 +113,13 @@ export function SuccessGuide({ markdown, onDismiss }: SuccessGuideProps) {
{t('success_guide.copied_snippet_label')}
</p>
<div className="flex items-center gap-3 bg-black/60 border border-white/8 rounded-xl px-4 py-3 font-mono text-sm">
<span className="text-emerald-400/60 select-none shrink-0">$</span>
<code className="text-emerald-300 break-all leading-relaxed flex-1 overflow-x-auto">
<span aria-hidden="true" className="text-emerald-400/60 select-none shrink-0">
$
</span>
<code
aria-label="Your badge markdown snippet"
className="text-emerald-300 break-all leading-relaxed flex-1 overflow-x-auto"
>
{markdown}
</code>
</div>
Expand Down
Loading