From aa4716888d706107ecae2686c2114e46e662da48 Mon Sep 17 00:00:00 2001 From: Abiola Ojo Date: Sun, 30 Aug 2026 01:33:23 +0100 Subject: [PATCH] feat: respect reduced motion in live market transitions --- components/moderation/MarketStatusBadge.tsx | 9 +- .../__tests__/MarketStatusBadge.test.tsx | 125 ++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/components/moderation/MarketStatusBadge.tsx b/components/moderation/MarketStatusBadge.tsx index 8ffb8ea2..cf139baa 100644 --- a/components/moderation/MarketStatusBadge.tsx +++ b/components/moderation/MarketStatusBadge.tsx @@ -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'; @@ -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 = { under_review: 'info', @@ -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 )} > diff --git a/components/moderation/__tests__/MarketStatusBadge.test.tsx b/components/moderation/__tests__/MarketStatusBadge.test.tsx index 73cede0f..1e890d43 100644 --- a/components/moderation/__tests__/MarketStatusBadge.test.tsx +++ b/components/moderation/__tests__/MarketStatusBadge.test.tsx @@ -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(); @@ -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(); + + 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(); + + 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(); + + 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(); + 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(); + badge = screen.getByRole('status'); + expect(badge).not.toHaveClass('animate-status-live-pulse'); + }); + }); });