Skip to content
Merged
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
9 changes: 8 additions & 1 deletion components/moderation/MarketStatusBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Clock, AlertTriangle, ShieldAlert, Flag, XCircle, Loader2 } from 'lucid
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import { Badge, badgeVariants } from '@/components/ui/badge';
import { useAccessibility } from '@/context/AccessibilityContext';

import type { ModerationState } from '@/types/moderation';
import { MODERATION_CONFIG } from './moderation-config';
Expand All @@ -28,6 +29,11 @@ export function MarketStatusBadge({ state, className, showTooltip = true }: Mark
const config = MODERATION_CONFIG[state];
const Icon = STATE_ICONS[state];
const isResolving = state === 'resolving';

// Respect user's reduced-motion preference (Issue #XXX — Quality-2 High)
// When reduceMotion is true, the pulsing animation is suppressed so the
// "resolving" badge renders as a static indicator.
const { reduceMotion } = useAccessibility();

const variantMap: Record<ModerationState, keyof typeof badgeVariants['variants']['variant']> = {
under_review: 'info',
Expand All @@ -45,7 +51,8 @@ export function MarketStatusBadge({ state, className, showTooltip = true }: Mark
size="md"
className={cn(
config.badgeClass,
isResolving && 'animate-status-live-pulse',
// Only apply the pulse animation when reduceMotion is false
isResolving && !reduceMotion && 'animate-status-live-pulse',
className
)}
>
Expand Down
125 changes: 125 additions & 0 deletions components/moderation/__tests__/MarketStatusBadge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,37 @@ import { render, screen } from '@testing-library/react';
import { MarketStatusBadge } from '../MarketStatusBadge';
import type { ModerationState } from '@/types/moderation';

// Mock AccessibilityContext
jest.mock('@/context/AccessibilityContext', () => ({
useAccessibility: jest.fn(() => ({
reduceMotion: false,
disableParallax: false,
disableAutoplay: false,
increaseContrast: false,
setReduceMotion: jest.fn(),
setDisableParallax: jest.fn(),
setDisableAutoplay: jest.fn(),
setIncreaseContrast: jest.fn(),
})),
}));

const { useAccessibility } = require('@/context/AccessibilityContext');

describe('MarketStatusBadge', () => {
beforeEach(() => {
// Reset to default (motion enabled) before each test
useAccessibility.mockReturnValue({
reduceMotion: false,
disableParallax: false,
disableAutoplay: false,
increaseContrast: false,
setReduceMotion: jest.fn(),
setDisableParallax: jest.fn(),
setDisableAutoplay: jest.fn(),
setIncreaseContrast: jest.fn(),
});
});

it('renders the resolving badge with a live glow-pulse class', () => {
render(<MarketStatusBadge state="resolving" showTooltip={false} />);

Expand Down Expand Up @@ -40,4 +70,99 @@ describe('MarketStatusBadge', () => {
expect(badge).not.toHaveClass('animate-status-live-pulse');
expect(badge.getAttribute('aria-label')).not.toContain('Resolving now');
});

// ── Reduced-motion tests (Quality-2 High) ────────────────────────────────
describe('reduced-motion accessibility', () => {
it('suppresses the pulse animation when reduceMotion is true', () => {
useAccessibility.mockReturnValue({
reduceMotion: true,
disableParallax: false,
disableAutoplay: false,
increaseContrast: false,
setReduceMotion: jest.fn(),
setDisableParallax: jest.fn(),
setDisableAutoplay: jest.fn(),
setIncreaseContrast: jest.fn(),
});

render(<MarketStatusBadge state="resolving" showTooltip={false} />);

const badge = screen.getByRole('status');
// Pulse animation class should NOT be applied
expect(badge).not.toHaveClass('animate-status-live-pulse');
// Aria-label and icon are still present
expect(badge.getAttribute('aria-label')).toContain('Resolving now');
expect(badge).toHaveTextContent('Resolving');
});

it('applies the pulse animation when reduceMotion is false', () => {
useAccessibility.mockReturnValue({
reduceMotion: false,
disableParallax: false,
disableAutoplay: false,
increaseContrast: false,
setReduceMotion: jest.fn(),
setDisableParallax: jest.fn(),
setDisableAutoplay: jest.fn(),
setIncreaseContrast: jest.fn(),
});

render(<MarketStatusBadge state="resolving" showTooltip={false} />);

const badge = screen.getByRole('status');
expect(badge).toHaveClass('animate-status-live-pulse');
});

it('non-resolving states are never affected by reduceMotion', () => {
useAccessibility.mockReturnValue({
reduceMotion: true,
disableParallax: false,
disableAutoplay: false,
increaseContrast: false,
setReduceMotion: jest.fn(),
setDisableParallax: jest.fn(),
setDisableAutoplay: jest.fn(),
setIncreaseContrast: jest.fn(),
});

render(<MarketStatusBadge state="paused" showTooltip={false} />);

const badge = screen.getByRole('status');
// Still no pulse (because paused never has it)
expect(badge).not.toHaveClass('animate-status-live-pulse');
});

it('dynamically respects reduceMotion transitions on rerender', () => {
useAccessibility.mockReturnValue({
reduceMotion: false,
disableParallax: false,
disableAutoplay: false,
increaseContrast: false,
setReduceMotion: jest.fn(),
setDisableParallax: jest.fn(),
setDisableAutoplay: jest.fn(),
setIncreaseContrast: jest.fn(),
});

const { rerender } = render(<MarketStatusBadge state="resolving" showTooltip={false} />);
let badge = screen.getByRole('status');
expect(badge).toHaveClass('animate-status-live-pulse');

// User enables reduce-motion
useAccessibility.mockReturnValue({
reduceMotion: true,
disableParallax: false,
disableAutoplay: false,
increaseContrast: false,
setReduceMotion: jest.fn(),
setDisableParallax: jest.fn(),
setDisableAutoplay: jest.fn(),
setIncreaseContrast: jest.fn(),
});

rerender(<MarketStatusBadge state="resolving" showTooltip={false} />);
badge = screen.getByRole('status');
expect(badge).not.toHaveClass('animate-status-live-pulse');
});
});
});
Loading