|
| 1 | +import { render, screen } from '@testing-library/react'; |
| 2 | +import userEvent from '@testing-library/user-event'; |
| 3 | +import { IndustryHero } from '@/components/industry/IndustryHero'; |
| 4 | +import { useIndustryHero } from '@/hooks/useIndustryHero'; |
| 5 | +import type { UseIndustryHeroResult } from '@/hooks/useIndustryHero'; |
| 6 | +import type { IndustrySplitFeature } from '@/types/industry'; |
| 7 | + |
| 8 | +jest.mock('@/hooks/useIndustryHero', () => ({ |
| 9 | + useIndustryHero: jest.fn(), |
| 10 | +})); |
| 11 | + |
| 12 | +const mockUseIndustryHero = useIndustryHero as jest.MockedFunction< |
| 13 | + typeof useIndustryHero |
| 14 | +>; |
| 15 | + |
| 16 | +const feature: IndustrySplitFeature = { |
| 17 | + id: 'enterprise-logistics', |
| 18 | + eyebrow: 'Enterprise Logistics', |
| 19 | + title: 'One command center for every shipment you move', |
| 20 | + description: 'Dispatch, exceptions and escrow release in a single view.', |
| 21 | + imagePosition: 'right', |
| 22 | + image: { |
| 23 | + src: '/images/industry/enterprise-command-center.png', |
| 24 | + alt: 'SwiftChain enterprise command center', |
| 25 | + width: 1280, |
| 26 | + height: 960, |
| 27 | + }, |
| 28 | + highlights: [ |
| 29 | + { |
| 30 | + id: 'highlight-1', |
| 31 | + title: 'Live network visibility', |
| 32 | + description: 'Track every driver and lane on one map.', |
| 33 | + }, |
| 34 | + ], |
| 35 | + cta: { label: 'See how it works', href: '/contact' }, |
| 36 | +}; |
| 37 | + |
| 38 | +const loadedState: UseIndustryHeroResult = { |
| 39 | + hero: { |
| 40 | + eyebrow: 'Industry Solutions', |
| 41 | + title: 'Modernizing the Global Supply Chain', |
| 42 | + description: 'Escrow-backed logistics for enterprise networks.', |
| 43 | + primaryCta: { label: 'Talk to our team', href: '/contact' }, |
| 44 | + secondaryCta: null, |
| 45 | + stats: [{ id: 'stat-1', label: 'Active trade corridors', value: '120+' }], |
| 46 | + }, |
| 47 | + features: [feature], |
| 48 | + isLoading: false, |
| 49 | + error: null, |
| 50 | + refetch: jest.fn(), |
| 51 | +}; |
| 52 | + |
| 53 | +function mockState(overrides: Partial<UseIndustryHeroResult> = {}) { |
| 54 | + mockUseIndustryHero.mockReturnValue({ ...loadedState, ...overrides }); |
| 55 | +} |
| 56 | + |
| 57 | +describe('IndustryHero', () => { |
| 58 | + beforeEach(() => jest.clearAllMocks()); |
| 59 | + |
| 60 | + it('renders the supply chain header and stats from the API', () => { |
| 61 | + mockState(); |
| 62 | + render(<IndustryHero />); |
| 63 | + |
| 64 | + expect( |
| 65 | + screen.getByRole('heading', { |
| 66 | + level: 1, |
| 67 | + name: 'Modernizing the Global Supply Chain', |
| 68 | + }), |
| 69 | + ).toBeInTheDocument(); |
| 70 | + expect(screen.getByText('Active trade corridors')).toBeInTheDocument(); |
| 71 | + expect(screen.getByText('120+')).toBeInTheDocument(); |
| 72 | + expect( |
| 73 | + screen.getByRole('link', { name: 'Talk to our team' }), |
| 74 | + ).toHaveAttribute('href', '/contact'); |
| 75 | + }); |
| 76 | + |
| 77 | + it('renders the Enterprise Logistics split block with a next/image asset', () => { |
| 78 | + mockState(); |
| 79 | + render(<IndustryHero />); |
| 80 | + |
| 81 | + expect(screen.getByText('Enterprise Logistics')).toBeInTheDocument(); |
| 82 | + expect( |
| 83 | + screen.getByRole('heading', { |
| 84 | + level: 2, |
| 85 | + name: 'One command center for every shipment you move', |
| 86 | + }), |
| 87 | + ).toBeInTheDocument(); |
| 88 | + expect(screen.getByText('Live network visibility')).toBeInTheDocument(); |
| 89 | + |
| 90 | + const image = screen.getByAltText('SwiftChain enterprise command center'); |
| 91 | + // next/image emits an <img> with an optimizer srcset and intrinsic |
| 92 | + // dimensions, which is what prevents the layout shift. |
| 93 | + expect(image).toHaveAttribute('width', '1280'); |
| 94 | + expect(image).toHaveAttribute('height', '960'); |
| 95 | + expect(image.getAttribute('srcset')).toContain( |
| 96 | + encodeURIComponent('/images/industry/enterprise-command-center.png'), |
| 97 | + ); |
| 98 | + }); |
| 99 | + |
| 100 | + it('shows the loading skeleton while content is fetched', () => { |
| 101 | + mockState({ isLoading: true, hero: null, features: [] }); |
| 102 | + render(<IndustryHero />); |
| 103 | + |
| 104 | + expect(screen.getByRole('status')).toBeInTheDocument(); |
| 105 | + expect(screen.queryByRole('heading', { level: 1 })).not.toBeInTheDocument(); |
| 106 | + }); |
| 107 | + |
| 108 | + it('shows an error state and retries through the hook', async () => { |
| 109 | + const refetch = jest.fn(); |
| 110 | + mockState({ |
| 111 | + error: 'Service unavailable', |
| 112 | + hero: null, |
| 113 | + features: [], |
| 114 | + refetch, |
| 115 | + }); |
| 116 | + render(<IndustryHero />); |
| 117 | + |
| 118 | + expect(screen.getByRole('alert')).toHaveTextContent('Service unavailable'); |
| 119 | + |
| 120 | + await userEvent.click(screen.getByRole('button', { name: 'Try again' })); |
| 121 | + expect(refetch).toHaveBeenCalledTimes(1); |
| 122 | + }); |
| 123 | + |
| 124 | + it('shows an empty state when nothing is published', () => { |
| 125 | + mockState({ hero: null, features: [] }); |
| 126 | + render(<IndustryHero />); |
| 127 | + |
| 128 | + expect( |
| 129 | + screen.getByText('No industry solutions content is published yet.'), |
| 130 | + ).toBeInTheDocument(); |
| 131 | + }); |
| 132 | +}); |
0 commit comments