import React, { useState } from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; // framer-motion's layout animations need a real layout engine; jsdom has none. // The sliding indicator is not what this file is about, so it is stubbed to a // plain span and the structural assertions below stay meaningful. jest.mock('framer-motion', () => ({ motion: { span: ({ layoutId, transition, ...rest }) => , }, })); import { Tabs } from '@/components/ds/Tabs'; /** * The tab strip must stay reachable when it is wider than its container. * * Deliveries renders ten status tabs — roughly 1050px — and the assistant dock * takes 30% of the viewport back, 40% expanded, and opens by default above * 1024px. The last two tabs, Delivered and Cancelled, were not merely clipped: * with nothing scrollable around them there was no gesture that could bring * them back. Four of the six pages using this component had hand-rolled their * own wrapper; the two that had not were broken. * * jsdom does not lay anything out, so these assert the STRUCTURE that makes * scrolling possible rather than the scrolling itself. That is the part that * regressed — the wrapper was deleted from Deliveries.jsx — so it is the part * worth pinning. */ // The real strip from Deliveries, in order. const DELIVERY_TABS = [ { value: 'all', label: 'All', count: 39 }, { value: 'pending', label: 'Pending', count: 32 }, { value: 'accepted', label: 'Accepted', count: 3 }, { value: 'arrived', label: 'Arrived', count: 0 }, { value: 'picked', label: 'Picked', count: 0 }, { value: 'active', label: 'Active', count: 1 }, { value: 'skipped', label: 'Skipped', count: 0 }, { value: 'delivered', label: 'Delivered', count: 3 }, { value: 'cancelled', label: 'Cancelled', count: 0 }, ]; const trackOf = (container) => container.querySelector('.overflow-x-auto'); describe('Tabs — overflow', () => { it.each(['pill', 'underline'])('wraps the %s strip in its own scroll track', (variant) => { const { container } = render( {}} variant={variant} /> ); const track = trackOf(container); expect(track).not.toBeNull(); // max-w-full bounds it; min-w-0 is what actually lets a flex item shrink // below its content width instead of overflowing the page. expect(track.className).toContain('max-w-full'); expect(track.className).toContain('min-w-0'); }); it('puts the tablist inside the track, not around it', () => { const { container } = render( {}} />); const track = trackOf(container); expect(track.querySelector('[role="tablist"]')).not.toBeNull(); }); it('keeps every tab at its natural width so the row overflows rather than squashing', () => { render( {}} />); screen.getAllByRole('tab').forEach((tab) => { // Without shrink-0 the flex row compresses ten tabs into the available // width and the track has nothing to scroll. expect(tab.className).toContain('shrink-0'); }); }); it('gives the focus ring room to escape the clip', () => { // overflow-x clips the ring on the first and last tab. The negative margin // paired with equal padding buys it space without moving the strip, and is // what makes the wrapper safe to nest inside the four pages that still // have their own. const { container } = render( {}} />); const track = trackOf(container); expect(track.className).toContain('-mx-1'); expect(track.className).toContain('px-1'); }); }); describe('Tabs — reachability', () => { it('renders the tabs that were cut off, Delivered and Cancelled included', () => { render( {}} />); expect(screen.getAllByRole('tab')).toHaveLength(DELIVERY_TABS.length); expect(screen.getByRole('tab', { name: /Delivered/ })).toBeTruthy(); expect(screen.getByRole('tab', { name: /Cancelled/ })).toBeTruthy(); }); it('selects a tab at the far end of the strip', () => { const Harness = () => { const [tab, setTab] = useState('all'); return ( <> {tab} ); }; render(); fireEvent.click(screen.getByRole('tab', { name: /Cancelled/ })); expect(screen.getByTestId('selected').textContent).toBe('cancelled'); expect(screen.getByRole('tab', { name: /Cancelled/ }).getAttribute('aria-selected')).toBe('true'); }); it('scrolls the selected tab into view without touching the page', () => { // scrollIntoView walks every scrollable ancestor and would yank the whole // document when the strip sits below the fold; the component moves the // track's own scrollLeft instead. Fail loudly if that ever changes. const scrollIntoView = jest.fn(); const original = window.HTMLElement.prototype.scrollIntoView; window.HTMLElement.prototype.scrollIntoView = scrollIntoView; try { render( {}} />); expect(scrollIntoView).not.toHaveBeenCalled(); } finally { window.HTMLElement.prototype.scrollIntoView = original; } }); }); describe('Tabs — unchanged behaviour', () => { it('still accepts plain strings', () => { render( {}} />); expect(screen.getAllByRole('tab')).toHaveLength(2); }); it('still forwards className to the tablist, not the track', () => { // The four pages that wrap Tabs themselves pass `shrink-0` through this. const { container } = render( {}} className="shrink-0" /> ); expect(container.querySelector('[role="tablist"]').className).toContain('shrink-0'); expect(trackOf(container).className).not.toContain('shrink-0'); }); it('still renders counts', () => { render( {}} />); expect(screen.getByRole('tab', { name: /All/ }).textContent).toContain('39'); }); });