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 (
<>
>
);
};
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');
});
});