import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { act, render, screen } from '../../../helpers/render'
import MTripLoadingSplash from '../../../../src/mobile/screens/trip/MTripLoadingSplash'
// FE-MOB-SPLASH-001 to FE-MOB-SPLASH-012
const STEP_MS = 1400
const origMatchMedia = window.matchMedia
/** One shared query object per stub so a test can flip `matches` and notify. */
let mediaQuery: { matches: boolean; listeners: Set<() => void> }
function stubReducedMotion(matches: boolean) {
mediaQuery = { matches, listeners: new Set() }
window.matchMedia = vi.fn().mockImplementation((query: string) => ({
get matches() { return mediaQuery.matches },
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: (_type: string, fn: () => void) => mediaQuery.listeners.add(fn),
removeEventListener: (_type: string, fn: () => void) => mediaQuery.listeners.delete(fn),
dispatchEvent: vi.fn(),
})) as unknown as typeof window.matchMedia
}
function setReducedMotion(matches: boolean) {
act(() => {
mediaQuery.matches = matches
mediaQuery.listeners.forEach(fn => fn())
})
}
const tick = (ms: number) => act(() => { vi.advanceTimersByTime(ms) })
const dots = (container: HTMLElement) =>
Array.from(container.querySelectorAll('span[class*="h-[6px]"]'))
const activeDotIndex = (container: HTMLElement) =>
dots(container).findIndex(d => d.className.includes('w-5'))
describe('MTripLoadingSplash', () => {
beforeEach(() => {
vi.useFakeTimers()
stubReducedMotion(false)
})
afterEach(() => {
vi.useRealTimers()
window.matchMedia = origMatchMedia
})
it('FE-MOB-SPLASH-001: opens on the packing beat with the trip title', () => {
const { container } = render()
expect(screen.getByText('Japan 2026')).toBeInTheDocument()
expect(screen.getByText('Packing your bags...')).toBeInTheDocument()
expect(container.querySelector('.trek--packing')).toBeTruthy()
})
it('FE-MOB-SPLASH-002: falls back to the TREK wordmark when the trip has no title', () => {
render()
expect(screen.getByText('TREK')).toBeInTheDocument()
})
it('FE-MOB-SPLASH-003: walks the four beats in order, one every 1400ms', () => {
const { container } = render()
tick(STEP_MS)
expect(screen.getByText('Hitting the road...')).toBeInTheDocument()
expect(container.querySelector('.trek--transport')).toBeTruthy()
tick(STEP_MS)
expect(screen.getByText('Loading place photos...')).toBeInTheDocument()
expect(container.querySelector('.trek--dashboard')).toBeTruthy()
tick(STEP_MS)
expect(screen.getByText('Almost there...')).toBeInTheDocument()
expect(container.querySelector('.trek--collections')).toBeTruthy()
})
it('FE-MOB-SPLASH-004: wraps back to the first beat after the last one', () => {
render()
tick(STEP_MS * 4)
expect(screen.getByText('Packing your bags...')).toBeInTheDocument()
})
it('FE-MOB-SPLASH-005: does not advance before the beat is due', () => {
render()
tick(STEP_MS - 1)
expect(screen.getByText('Packing your bags...')).toBeInTheDocument()
})
it('FE-MOB-SPLASH-006: the active dot tracks the current beat', () => {
const { container } = render()
expect(dots(container)).toHaveLength(4)
expect(activeDotIndex(container)).toBe(0)
tick(STEP_MS * 2)
expect(activeDotIndex(container)).toBe(2)
})
it('FE-MOB-SPLASH-007: animates the mascot stage between beats', () => {
const { container } = render()
const stage = container.querySelector('.trek--packing')?.parentElement
expect(stage).toHaveStyle({ animation: 'm-trek-beat 460ms cubic-bezier(.34,1.56,.64,1) both' })
})
it('FE-MOB-SPLASH-008: reduced motion parks on the photo beat and never advances', () => {
stubReducedMotion(true)
const { container } = render()
expect(screen.getByText('Loading place photos...')).toBeInTheDocument()
expect(activeDotIndex(container)).toBe(2)
tick(STEP_MS * 3)
expect(screen.getByText('Loading place photos...')).toBeInTheDocument()
expect(activeDotIndex(container)).toBe(2)
})
it('FE-MOB-SPLASH-009: reduced motion drops the beat animation', () => {
stubReducedMotion(true)
const { container } = render()
const stage = container.querySelector('.trek--dashboard')?.parentElement
expect(stage?.getAttribute('style')).toBeNull()
})
it('FE-MOB-SPLASH-011: follows the OS setting while the splash is up', () => {
const { container } = render()
expect(screen.getByText('Packing your bags...')).toBeInTheDocument()
setReducedMotion(true)
expect(screen.getByText('Loading place photos...')).toBeInTheDocument()
expect(activeDotIndex(container)).toBe(2)
tick(STEP_MS * 2)
expect(screen.getByText('Loading place photos...')).toBeInTheDocument()
})
it('FE-MOB-SPLASH-012: drops the media listener on unmount', () => {
const { unmount } = render()
expect(mediaQuery.listeners.size).toBe(1)
unmount()
expect(mediaQuery.listeners.size).toBe(0)
})
it('FE-MOB-SPLASH-010: clears its beat interval on unmount', () => {
const before = vi.getTimerCount()
const { unmount } = render()
expect(vi.getTimerCount()).toBe(before + 1)
unmount()
expect(vi.getTimerCount()).toBe(before)
})
})