Files
TREK/client/tests/unit/mobile/settings/MMobileNavCustomizer.test.tsx
T
Maurice 569f0ca1a5 test(mobile-nav): follow the new default dock through the mobile suites
Three expectations still described the Vacay/Atlas dock. Two of them
asserted the old arrangement by name, so they were pinning the very thing
that changed rather than catching a regression.

FE-MOB-NAV-006 now watches Atlas take the demoted seat next to
Collections, and picks Atlas out of the popover in FE-MOB-NAV-011 for
the same reason. Two new cases cover what the change is actually about:
Journey sits in the dock when it is enabled, and an instance without the
addon still gets Vacay and Atlas.
2026-09-08 14:39:34 +02:00

143 lines
7.0 KiB
TypeScript

// FE-MOB-SETNAVC-001 onwards
import { describe, it, expect, vi, beforeEach } from 'vitest';
import userEvent from '@testing-library/user-event';
import { act, render, screen, within } from '../../../helpers/render';
import { resetAllStores, seedStore } from '../../../helpers/store';
import { useAddonStore } from '../../../../src/store/addonStore';
import { usePluginStore } from '../../../../src/store/pluginStore';
import type { MobileNavValue } from '../../../../src/components/Settings/useMobileNavEditor';
import MMobileNavCustomizer from '../../../../src/mobile/screens/settings/MMobileNavCustomizer';
const GLOBAL_ADDONS = [
{ id: 'vacay', name: 'Vacay', type: 'global', icon: 'calendar', enabled: true },
{ id: 'atlas', name: 'Atlas', type: 'global', icon: 'map', enabled: true },
{ id: 'journey', name: 'Journey', type: 'global', icon: 'compass', enabled: true },
{ id: 'collections', name: 'Collections', type: 'global', icon: 'bookmark', enabled: true },
];
/** The section wrapper holding the rows of a zone ("In the bar" / Under "More"). */
function zone(heading: string): HTMLElement {
return screen.getByText(heading).closest('div')!.parentElement!;
}
function rowLabels(heading: string): string[] {
return within(zone(heading))
.getAllByLabelText('Move up')
.map((btn) => btn.closest('div')!.querySelector('span.truncate')!.textContent ?? '');
}
describe('MMobileNavCustomizer', () => {
beforeEach(() => {
resetAllStores();
seedStore(useAddonStore, { addons: GLOBAL_ADDONS, loaded: true });
usePluginStore.setState({ plugins: [], loaded: true });
});
it('FE-MOB-SETNAVC-001: an uncustomised value falls back to the built-in dock split', () => {
render(<MMobileNavCustomizer value={{ bar: [], more: [] }} onChange={vi.fn()} />);
expect(rowLabels('In the bar')).toEqual(['Vacay', 'Journey']);
expect(rowLabels('Under “More”')).toEqual(['Atlas', 'Collections']);
});
it('FE-MOB-SETNAVC-002: Dashboard is listed pinned and has no reorder buttons', () => {
render(<MMobileNavCustomizer value={{ bar: [], more: [] }} onChange={vi.fn()} />);
expect(screen.getByText('My Trips')).toBeInTheDocument();
expect(screen.getByText('Pinned')).toBeInTheDocument();
// Dashboard sits in the bar but is not one of the movable rows.
expect(rowLabels('In the bar')).not.toContain('My Trips');
});
it('FE-MOB-SETNAVC-003: the bar counter includes the pinned Dashboard slot', () => {
render(<MMobileNavCustomizer value={{ bar: ['vacay'], more: ['atlas'] }} onChange={vi.fn()} />);
expect(screen.getByText('2/3')).toBeInTheDocument();
});
it('FE-MOB-SETNAVC-004: demoting a bar item emits it at the end of More', async () => {
const user = userEvent.setup();
const onChange = vi.fn<(next: MobileNavValue) => void>();
render(<MMobileNavCustomizer value={{ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] }} onChange={onChange} />);
await user.click(within(zone('In the bar')).getAllByLabelText('Move under “More”')[0]);
expect(onChange).toHaveBeenCalledWith({ bar: ['atlas'], more: ['journey', 'collections', 'vacay'] });
});
it('FE-MOB-SETNAVC-005: promoting a More item appends it to the bar', async () => {
const user = userEvent.setup();
const onChange = vi.fn<(next: MobileNavValue) => void>();
render(<MMobileNavCustomizer value={{ bar: ['vacay'], more: ['journey', 'atlas', 'collections'] }} onChange={onChange} />);
await user.click(within(zone('Under “More”')).getAllByLabelText('Move into the bar')[1]);
expect(onChange).toHaveBeenCalledWith({ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] });
});
it('FE-MOB-SETNAVC-006: promotion is disabled once the bar is full', () => {
render(<MMobileNavCustomizer value={{ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] }} onChange={vi.fn()} />);
for (const btn of within(zone('Under “More”')).getAllByLabelText('Move into the bar')) {
expect(btn).toBeDisabled();
}
});
it('FE-MOB-SETNAVC-007: reordering inside the bar emits the swapped id list', async () => {
const user = userEvent.setup();
const onChange = vi.fn<(next: MobileNavValue) => void>();
render(<MMobileNavCustomizer value={{ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] }} onChange={onChange} />);
await user.click(within(zone('In the bar')).getAllByLabelText('Move up')[1]);
expect(onChange).toHaveBeenCalledWith({ bar: ['atlas', 'vacay'], more: ['journey', 'collections'] });
});
it('FE-MOB-SETNAVC-008: reordering inside More leaves the bar untouched', async () => {
const user = userEvent.setup();
const onChange = vi.fn<(next: MobileNavValue) => void>();
render(<MMobileNavCustomizer value={{ bar: ['vacay'], more: ['journey', 'atlas', 'collections'] }} onChange={onChange} />);
await user.click(within(zone('Under “More”')).getAllByLabelText('Move down')[0]);
expect(onChange).toHaveBeenCalledWith({ bar: ['vacay'], more: ['atlas', 'journey', 'collections'] });
});
it('FE-MOB-SETNAVC-009: the ends of a zone have their arrows disabled', () => {
render(<MMobileNavCustomizer value={{ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] }} onChange={vi.fn()} />);
const ups = within(zone('In the bar')).getAllByLabelText('Move up');
const downs = within(zone('In the bar')).getAllByLabelText('Move down');
expect(ups[0]).toBeDisabled();
expect(downs[1]).toBeDisabled();
expect(ups[1]).not.toBeDisabled();
});
it('FE-MOB-SETNAVC-010: an empty More zone shows the placeholder instead of rows', () => {
seedStore(useAddonStore, { addons: GLOBAL_ADDONS.slice(0, 2), loaded: true });
render(<MMobileNavCustomizer value={{ bar: ['vacay', 'atlas'], more: [] }} onChange={vi.fn()} />);
expect(screen.getByText('Nothing here yet — everything fits in the bar.')).toBeInTheDocument();
expect(within(zone('Under “More”')).queryByLabelText('Move up')).not.toBeInTheDocument();
});
it('FE-MOB-SETNAVC-011: the preview mirrors the dock and only shows More when items are demoted', () => {
const { rerender } = render(
<MMobileNavCustomizer value={{ bar: ['vacay', 'atlas'], more: ['journey'] }} onChange={vi.fn()} />,
);
expect(screen.getByTitle('More')).toBeInTheDocument();
expect(screen.getByTitle('My Trips')).toBeInTheDocument();
expect(screen.getByTitle('Vacay')).toBeInTheDocument();
act(() => seedStore(useAddonStore, { addons: GLOBAL_ADDONS.slice(0, 2), loaded: true }));
rerender(<MMobileNavCustomizer value={{ bar: ['vacay', 'atlas'], more: [] }} onChange={vi.fn()} />);
expect(screen.queryByTitle('More')).not.toBeInTheDocument();
});
it('FE-MOB-SETNAVC-012: page plugins appear as nav items under More', () => {
usePluginStore.setState({
plugins: [{ id: 'todos', name: 'Trip To-Dos', type: 'page', icon: 'list' }] as never,
loaded: true,
});
render(<MMobileNavCustomizer value={{ bar: ['vacay'], more: ['plugin:todos'] }} onChange={vi.fn()} />);
expect(rowLabels('Under “More”')).toContain('Trip To-Dos');
});
});