Files
TREK/client/src/components/Settings/useMobileNavEditor.test.tsx
T
Maurice c04e740ff1 fix(mobile-nav): dock Journey by default, move Atlas one tap further
Feedback from Discord: on a phone in the field the two things you reach
for are the itinerary and the journal, but the default dock spends its
two free slots on Vacay and Atlas. Journey sits under More, and most
people never open the navbar customiser to fix that.

DEFAULT_DOCK_IDS becomes a priority list instead of a membership test:
the first MOBILE_NAV_MAX_BAR entries that are actually enabled take the
slots. Journey ships disabled, so an instance that never turned it on
keeps the exact Vacay/Atlas dock it has today. Listing all four known
global addons also fills the dock when one of them is switched off,
where the old membership test left the slot empty.

Accounts that saved their own bar/more split are untouched: a non-empty
config still wins over the default, so nobody's arrangement moves.
2026-09-07 23:18:23 +02:00

160 lines
6.7 KiB
TypeScript

// FE-COMP-NAVEDITOR-001 to FE-COMP-NAVEDITOR-014
import React from 'react';
import { renderHook } from '../../../tests/helpers/render';
import { act } from '@testing-library/react';
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
import { useAddonStore } from '../../store/addonStore';
import { usePluginStore } from '../../store/pluginStore';
import { TranslationProvider } from '../../i18n/TranslationContext';
import { useMobileNavEditor, type MobileNavValue } from './useMobileNavEditor';
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 },
];
function wrapper({ children }: { children: React.ReactNode }) {
return <TranslationProvider>{children}</TranslationProvider>;
}
function setup(value: MobileNavValue) {
const onChange = vi.fn<(next: MobileNavValue) => void>();
const { result } = renderHook(() => useMobileNavEditor(value, onChange), { wrapper });
return { result, onChange };
}
const ids = (items: { id: string }[]) => items.map(i => i.id);
beforeEach(() => {
resetAllStores();
vi.clearAllMocks();
seedStore(useAddonStore, { addons: GLOBAL_ADDONS, loaded: true });
usePluginStore.setState({ plugins: [], loaded: true });
});
describe('useMobileNavEditor', () => {
it('FE-COMP-NAVEDITOR-001: an empty value falls back to the built-in dock split', () => {
const { result } = setup({ bar: [], more: [] });
expect(ids(result.current.barItems)).toEqual(['vacay', 'journey']);
expect(ids(result.current.moreItems)).toEqual(['atlas', 'collections']);
expect(result.current.hasMore).toBe(true);
expect(result.current.barFull).toBe(true);
});
it('FE-COMP-NAVEDITOR-002: Dashboard is pinned, kept out of barItems and first in the preview', () => {
const { result } = setup({ bar: ['vacay'], more: ['atlas'] });
expect(result.current.dashboard?.id).toBe('dashboard');
expect(ids(result.current.barItems)).toEqual(['vacay']);
expect(ids(result.current.previewBar)).toEqual(['dashboard', 'vacay']);
});
it('FE-COMP-NAVEDITOR-003: a bar that is not full reports barFull false', () => {
const { result } = setup({ bar: ['vacay'], more: ['atlas', 'journey', 'collections'] });
expect(result.current.barFull).toBe(false);
});
it('FE-COMP-NAVEDITOR-004: everything in the bar leaves More empty', () => {
seedStore(useAddonStore, { addons: GLOBAL_ADDONS.slice(0, 2), loaded: true });
const { result } = setup({ bar: ['vacay', 'atlas'], more: [] });
expect(result.current.moreItems).toEqual([]);
expect(result.current.hasMore).toBe(false);
});
it('FE-COMP-NAVEDITOR-005: moving a bar item up emits the reordered id list', () => {
const { result, onChange } = setup({ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] });
act(() => result.current.move('bar', 1, 0));
expect(onChange).toHaveBeenCalledWith({ bar: ['atlas', 'vacay'], more: ['journey', 'collections'] });
});
it('FE-COMP-NAVEDITOR-006: moving inside More leaves the bar untouched', () => {
const { result, onChange } = setup({ bar: ['vacay'], more: ['journey', 'atlas', 'collections'] });
act(() => result.current.move('more', 0, 1));
expect(onChange).toHaveBeenCalledWith({ bar: ['vacay'], more: ['atlas', 'journey', 'collections'] });
});
it('FE-COMP-NAVEDITOR-007: a move past the end of a zone emits the list unchanged', () => {
const { result, onChange } = setup({ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] });
act(() => result.current.move('bar', 1, 2));
expect(onChange).toHaveBeenCalledWith({ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] });
});
it('FE-COMP-NAVEDITOR-008: a move before the start of a zone emits the list unchanged', () => {
const { result, onChange } = setup({ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] });
act(() => result.current.move('bar', 0, -1));
expect(onChange).toHaveBeenCalledWith({ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] });
});
it('FE-COMP-NAVEDITOR-009: a move onto itself emits the list unchanged', () => {
const { result, onChange } = setup({ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] });
act(() => result.current.move('bar', 1, 1));
expect(onChange).toHaveBeenCalledWith({ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] });
});
it('FE-COMP-NAVEDITOR-010: demoting a bar item appends it to the end of More', () => {
const { result, onChange } = setup({ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] });
act(() => result.current.toMore('vacay'));
expect(onChange).toHaveBeenCalledWith({ bar: ['atlas'], more: ['journey', 'collections', 'vacay'] });
});
it('FE-COMP-NAVEDITOR-011: demoting an id that is not in the bar is a no-op', () => {
const { result, onChange } = setup({ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] });
act(() => result.current.toMore('journey'));
expect(onChange).not.toHaveBeenCalled();
});
it('FE-COMP-NAVEDITOR-012: promoting appends to the bar and drops the item from More', () => {
const { result, onChange } = setup({ bar: ['vacay'], more: ['journey', 'atlas', 'collections'] });
act(() => result.current.toBar('atlas'));
expect(onChange).toHaveBeenCalledWith({ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] });
});
it('FE-COMP-NAVEDITOR-013: promoting is refused when the bar is full or the id is unknown', () => {
const full = setup({ bar: ['vacay', 'atlas'], more: ['journey', 'collections'] });
act(() => full.result.current.toBar('journey'));
expect(full.onChange).not.toHaveBeenCalled();
const room = setup({ bar: ['vacay'], more: ['journey', 'atlas', 'collections'] });
act(() => room.result.current.toBar('does-not-exist'));
expect(room.onChange).not.toHaveBeenCalled();
});
it('FE-COMP-NAVEDITOR-014: page plugins take part in the split under their plugin id', () => {
usePluginStore.setState({
plugins: [{ id: 'todos', name: 'Trip To-Dos', type: 'page', icon: 'list' }] as never,
loaded: true,
});
const { result, onChange } = setup({ bar: ['vacay'], more: ['plugin:todos'] });
// Items the stored config never mentioned are appended under More.
expect(ids(result.current.moreItems)).toEqual(['plugin:todos', 'atlas', 'journey', 'collections']);
act(() => result.current.toBar('plugin:todos'));
expect(onChange).toHaveBeenCalledWith({
bar: ['vacay', 'plugin:todos'],
more: ['atlas', 'journey', 'collections'],
});
});
});