import React from 'react' import { render, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router' import { describe, it, expect, beforeEach, vi } from 'vitest' import { useAuthStore } from './store/authStore' import { useAddonStore } from './store/addonStore' import { resetAllStores } from '../tests/helpers/store' import { buildUser } from '../tests/helpers/factories' import App from './App' /** * The viewport decides which chunk gets fetched, so the switch lives in App.tsx * and no longer in the pages. This is that contract, for all ten routes in one * place — it used to be five scattered spot checks, one per page test. * * A stub per screen keeps the test about one thing: below the breakpoint the M * root hangs off the route, and the desktop page is never rendered at all. */ const isPhone = vi.hoisted(() => ({ value: false })) vi.mock('./mobile/useIsPhone', () => ({ useIsPhone: () => isPhone.value })) vi.mock('./mobile/screens/dashboard/MDashboard', () => ({ default: () =>
m-dashboard
})) vi.mock('./mobile/screens/trip/MTripShell', () => ({ default: () =>
m-trip
})) vi.mock('./mobile/screens/admin/MAdmin', () => ({ default: () =>
m-admin
})) vi.mock('./mobile/screens/settings/MSettings', () => ({ default: () =>
m-settings
})) vi.mock('./mobile/screens/vacay/MVacay', () => ({ default: () =>
m-vacay
})) vi.mock('./mobile/screens/atlas/MAtlas', () => ({ default: () =>
m-atlas
})) vi.mock('./mobile/screens/journey/MJourney', () => ({ default: () =>
m-journey
})) vi.mock('./mobile/screens/journey/MJourneyDetail', () => ({ default: () =>
m-journey-detail
})) vi.mock('./mobile/screens/collections/MCollections', () => ({ default: () =>
m-collections
})) vi.mock('./mobile/screens/notifications/MNotifications', () => ({ default: () =>
m-notifications
})) vi.mock('./pages/DashboardPage', () => ({ default: () =>
d-dashboard
})) vi.mock('./pages/TripPlannerPage', () => ({ default: () =>
d-trip
})) vi.mock('./pages/AdminPage', () => ({ default: () =>
d-admin
})) vi.mock('./pages/SettingsPage', () => ({ default: () =>
d-settings
})) vi.mock('./pages/VacayPage', () => ({ default: () =>
d-vacay
})) vi.mock('./pages/AtlasPage', () => ({ default: () =>
d-atlas
})) vi.mock('./pages/JourneyPage', () => ({ default: () =>
d-journey
})) vi.mock('./pages/JourneyDetailPage', () => ({ default: () =>
d-journey-detail
})) vi.mock('./pages/CollectionsPage', () => ({ default: () =>
d-collections
})) vi.mock('./pages/InAppNotificationsPage.tsx', () => ({ default: () =>
d-notifications
})) // The notification listener opens a WebSocket on mount. vi.mock('./hooks/useInAppNotificationListener.ts', () => ({ useInAppNotificationListener: vi.fn(), })) /** path, mobile marker, desktop marker */ const ROUTES: [string, string, string][] = [ ['/dashboard', 'm-dashboard', 'd-dashboard'], ['/trips/1', 'm-trip', 'd-trip'], ['/admin', 'm-admin', 'd-admin'], ['/settings', 'm-settings', 'd-settings'], ['/vacay', 'm-vacay', 'd-vacay'], ['/atlas', 'm-atlas', 'd-atlas'], ['/journey', 'm-journey', 'd-journey'], ['/journey/1', 'm-journey-detail', 'd-journey-detail'], ['/collections', 'm-collections', 'd-collections'], ['/collections/1', 'm-collections', 'd-collections'], ['/notifications', 'm-notifications', 'd-notifications'], ] function renderAt(path: string) { return render( ) } beforeEach(() => { resetAllStores() vi.clearAllMocks() isPhone.value = false // An admin passes the role check on /admin; every other route ignores it. useAuthStore.setState({ isLoading: false, isAuthenticated: true, user: buildUser({ role: 'admin', mfa_enabled: true }), appRequireMfa: false, loadUser: vi.fn().mockResolvedValue(undefined), }) // /journey and /collections sit behind addons; without this they redirect to // the dashboard and the route under test never renders. useAddonStore.setState({ loaded: true, isEnabled: () => true }) }) describe('App — viewport routing', () => { it.each(ROUTES)( 'FE-COMP-APPVP-001: %s renders the mobile screen below the breakpoint', async (path, mobile, desktop) => { isPhone.value = true renderAt(path) expect(await screen.findByText(mobile)).toBeInTheDocument() expect(screen.queryByText(desktop)).not.toBeInTheDocument() } ) it.each(ROUTES)( 'FE-COMP-APPVP-002: %s renders the desktop page above the breakpoint', async (path, mobile, desktop) => { renderAt(path) expect(await screen.findByText(desktop)).toBeInTheDocument() expect(screen.queryByText(mobile)).not.toBeInTheDocument() } ) })