mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
6c63a9bb60
* refactor(client): opt into the v7 router behaviour while still on v6 Turning the behaviour on separately from the version bump keeps the two failure modes apart: red here means a behaviour change, red on the bump means the import rewrite. The suite is unchanged either way — 527 files, 11099 tests. Only the two flags a non-data router understands are set; the rest belong to createBrowserRouter, which this app doesn't use. Both are applied at all six router mounts, so the tests exercise the same behaviour as production rather than validating v6 while prod runs v7. * fix(client): move to react-router 7.18.2 6.30.4 is affected by GHSA-wrjc-x8rr-h8h6 — open redirect through a backslash in Link and useNavigate, which this app uses in 41 places — and the 6.x line has no fix: react-router-dom 6.30.2 to 6.30.4 carries GHSA-jjmj-jmhj-qwj2 with no patched version at all. Staying is not an option, and 7.0 to 7.17 only swaps it for GHSA-chx6-hx7r-mcp5. The package rename comes with it: react-router-dom is a re-export shim in v7, so using it would only defer the same rewrite to the v8 jump. 77 occurrences across 60 files, all of them import specifiers and vi.mock strings. The surface is small — useNavigate, useParams, useSearchParams, useLocation, Routes, Route, Link, Navigate, useMatch, MemoryRouter, BrowserRouter — no data router, no loaders or actions. Nothing else had to change; the future flags from the previous commit are simply how v7 behaves, so they come back out. npm audit still reports GHSA-qwww-vcr4-c8h2 on react-router (RSC mode CSRF, fixed in 8.3.0). It needs RSC mode, which needs a server; this is a static SPA. Closing it means the v8 major, which is its own piece of work.
184 lines
7.4 KiB
TypeScript
184 lines
7.4 KiB
TypeScript
import React from 'react';
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { useLocation } from 'react-router';
|
|
import { render, screen, fireEvent } from '../../helpers/render';
|
|
import MBottomNav from '../../../src/mobile/components/MBottomNav';
|
|
import { useAddonStore } from '../../../src/store/addonStore';
|
|
import { usePluginStore } from '../../../src/store/pluginStore';
|
|
|
|
function LocationEcho() {
|
|
const location = useLocation();
|
|
return <span data-testid="loc">{location.pathname + location.search}</span>;
|
|
}
|
|
|
|
const nav = (entry: string) =>
|
|
render(<><MBottomNav /><LocationEcho /></>, { initialEntries: [entry] });
|
|
|
|
const at = () => screen.getByTestId('loc').textContent;
|
|
|
|
// FE-MOB-NAV-001 onwards
|
|
|
|
function seedAddons(ids: string[]) {
|
|
useAddonStore.setState({
|
|
addons: ids.map((id) => ({ id, name: id, type: 'global', icon: 'globe', enabled: true })),
|
|
loaded: true,
|
|
});
|
|
}
|
|
|
|
describe('MBottomNav', () => {
|
|
beforeEach(() => {
|
|
useAddonStore.setState({ addons: [], loaded: true });
|
|
usePluginStore.setState({ plugins: [], loaded: true });
|
|
});
|
|
|
|
it('FE-MOB-NAV-001: renders the dashboard tab plus enabled global addons', () => {
|
|
seedAddons(['vacay', 'atlas']);
|
|
render(<MBottomNav />, { initialEntries: ['/dashboard'] });
|
|
|
|
expect(screen.getByRole('button', { name: 'My Trips' })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'Vacay' })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'Atlas' })).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-NAV-002: omits tabs for addons that are not enabled', () => {
|
|
render(<MBottomNav />, { initialEntries: ['/dashboard'] });
|
|
|
|
expect(screen.getByRole('button', { name: 'My Trips' })).toBeInTheDocument();
|
|
expect(screen.queryByRole('button', { name: 'Atlas' })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-NAV-003: marks the current route as the active tab', () => {
|
|
seedAddons(['atlas']);
|
|
render(<MBottomNav />, { initialEntries: ['/atlas'] });
|
|
|
|
expect(screen.getByRole('button', { name: 'Atlas' })).toHaveAttribute('aria-current', 'page');
|
|
expect(screen.getByRole('button', { name: 'My Trips' })).not.toHaveAttribute('aria-current');
|
|
});
|
|
|
|
it('FE-MOB-NAV-004: the "+" creates a trip on the dashboard', () => {
|
|
render(<MBottomNav />, { initialEntries: ['/dashboard'] });
|
|
expect(screen.getByRole('button', { name: 'New Trip' })).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-NAV-005: the "+" follows the persisted trip tab inside a trip', () => {
|
|
sessionStorage.setItem('trip-tab-7', 'buchungen');
|
|
render(<MBottomNav />, { initialEntries: ['/trips/7'] });
|
|
expect(screen.getByRole('button', { name: 'Manual Booking' })).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-NAV-006: journey and collections live in the More popover, not the dock', () => {
|
|
seedAddons(['vacay', 'atlas', 'journey', 'collections']);
|
|
render(<MBottomNav />, { initialEntries: ['/dashboard'] });
|
|
|
|
expect(screen.queryByRole('button', { name: 'Journey' })).not.toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole('button', { expanded: false }));
|
|
expect(screen.getByRole('button', { name: /Journey/ })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: /Collections/ })).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-NAV-007: no More slot without popover entries', () => {
|
|
seedAddons(['vacay', 'atlas']);
|
|
render(<MBottomNav />, { initialEntries: ['/dashboard'] });
|
|
expect(screen.queryByRole('button', { expanded: false })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-NAV-008: the centre button opens the country search on the atlas', () => {
|
|
seedAddons(['atlas']);
|
|
render(<MBottomNav />, { initialEntries: ['/atlas'] });
|
|
expect(screen.getByRole('button', { name: 'Search a country...' })).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-NAV-009: settings shows the disabled logo slot instead of a create action', () => {
|
|
render(<MBottomNav />, { initialEntries: ['/settings'] });
|
|
expect(screen.queryByRole('button', { name: 'New Trip' })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-NAV-010: admin also swaps the "+" for the logo slot', () => {
|
|
render(<MBottomNav />, { initialEntries: ['/admin'] });
|
|
expect(screen.queryByRole('button', { name: 'New Trip' })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-NAV-011: choosing a More entry navigates and closes the popover', () => {
|
|
seedAddons(['vacay', 'atlas', 'journey', 'collections']);
|
|
render(<MBottomNav />, { initialEntries: ['/dashboard'] });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'More' }));
|
|
fireEvent.click(screen.getByRole('button', { name: /Journey/ }));
|
|
|
|
const more = screen.getByRole('button', { name: 'More' });
|
|
expect(more).toHaveAttribute('aria-expanded', 'false');
|
|
expect(more).toHaveAttribute('aria-current', 'page');
|
|
});
|
|
|
|
it('FE-MOB-NAV-012: tapping the scrim closes the More popover again', () => {
|
|
seedAddons(['vacay', 'atlas', 'journey', 'collections']);
|
|
const { container } = render(<MBottomNav />, { initialEntries: ['/dashboard'] });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'More' }));
|
|
const scrim = container.querySelector('.fixed.inset-0') as HTMLElement;
|
|
|
|
fireEvent.click(scrim);
|
|
|
|
expect(screen.getByRole('button', { name: 'More' })).toHaveAttribute('aria-expanded', 'false');
|
|
expect(screen.queryByRole('button', { name: /Collections/ })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-NAV-013: a click inside the popover does not close it', () => {
|
|
seedAddons(['vacay', 'atlas', 'journey', 'collections']);
|
|
const { container } = render(<MBottomNav />, { initialEntries: ['/dashboard'] });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'More' }));
|
|
fireEvent.click(container.querySelector('.fixed.inset-0 > div') as HTMLElement);
|
|
|
|
expect(screen.getByRole('button', { name: /Journey/ })).toBeInTheDocument();
|
|
});
|
|
|
|
it.each([
|
|
['/journey', 'New Journey', '/journey?create=1'],
|
|
['/journey/4', 'Add Entry', '/journey/4?create=entry'],
|
|
['/collections', 'Add a place', '/collections?create=place'],
|
|
['/trips/7', 'Add Place/Activity', '/trips/7?create=place'],
|
|
['/dashboard', 'New Trip', '/dashboard?create=1'],
|
|
['/atlas', 'Search a country...', '/atlas?search=1'],
|
|
])('FE-MOB-NAV-014: the "+" on %s runs "%s"', (route, label, target) => {
|
|
seedAddons(['journey', 'collections', 'atlas']);
|
|
nav(route);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: label }));
|
|
|
|
expect(at()).toBe(target);
|
|
});
|
|
|
|
it.each([
|
|
['finanzplan', 'Add expense', '/trips/7?create=expense'],
|
|
['transports', 'Transport', '/trips/7?create=transport'],
|
|
['buchungen', 'Manual Booking', '/trips/7?create=reservation'],
|
|
])('FE-MOB-NAV-015: the trip "+" follows the %s tab', (tab, label, target) => {
|
|
sessionStorage.setItem('trip-tab-7', tab);
|
|
nav('/trips/7');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: label }));
|
|
|
|
expect(at()).toBe(target);
|
|
});
|
|
|
|
it('FE-MOB-NAV-016: a dock tab navigates to its route', () => {
|
|
seedAddons(['vacay', 'atlas']);
|
|
nav('/dashboard');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Atlas' }));
|
|
|
|
expect(at()).toBe('/atlas');
|
|
});
|
|
|
|
it('FE-MOB-NAV-017: a More entry navigates to its route', () => {
|
|
seedAddons(['vacay', 'atlas', 'journey', 'collections']);
|
|
nav('/dashboard');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'More' }));
|
|
fireEvent.click(screen.getByRole('button', { name: /Collections/ }));
|
|
|
|
expect(at()).toBe('/collections');
|
|
});
|
|
});
|