codeburn/app/renderer/components/Splash.test.tsx
iamtoruk 000e39a034 feat(app): binary-flame brand art + animated video loader on the splash
New owner-approved art: the binary-digit flame. Checkerboard keyed out,
largest-component masking to drop glow-zone checker fringe, clean
synthetic ember glow rebuilt; composited to the Big Sur squircle as
app/build/icon.png and squared with alpha as the in-app FlameMark
raster (sidebar, About, static splash).

The supplied burn animation becomes the splash loader: checker keyed
per-frame and the splash's fixed dark canvas (#171310) baked in (h264
has no alpha), audio stripped, 944KB looping video shown while the
first scan runs. Splash canvas and text colors are now theme-fixed so
video and page never diverge; reduced motion/tests keep the static
mark, verified by test.

Flame/splash/motion suites green in isolation (full suite re-verified
when the in-flight quota batch commits); typecheck + build green.
2026-07-16 06:19:12 -07:00

86 lines
3.2 KiB
TypeScript

// @vitest-environment jsdom
import { act, render } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Splash } from './Splash'
import { mockMatchMedia as mockReducedMotion } from '../lib/testMatchMedia'
function splashEl(): HTMLElement | null {
return document.querySelector('.splash')
}
afterEach(() => {
vi.useRealTimers()
Reflect.deleteProperty(window, 'matchMedia')
})
describe('Splash', () => {
it('stays up while the first overview fetch has neither data nor error', () => {
render(<Splash hasData={false} hasError={false} />)
const el = splashEl()
expect(el).toBeInTheDocument()
// Static under vitest / the closed motion gate: no ignite/pulse class,
// the static mark instead of the loader video.
expect(el).not.toHaveClass('splash-lit')
expect(el?.querySelector('video')).toBeNull()
expect(el?.querySelector('.flamemark')).not.toBeNull()
})
it('holds the min on-screen time, then crossfades away once data lands', () => {
vi.useFakeTimers()
const { rerender } = render(<Splash hasData={false} hasError={false} />)
expect(splashEl()).toBeInTheDocument()
// First data lands immediately (warm cache): the floor must keep it up.
rerender(<Splash hasData hasError={false} />)
act(() => { vi.advanceTimersByTime(599) })
expect(splashEl()).toBeInTheDocument()
expect(splashEl()).not.toHaveClass('splash-out')
// Floor reached: begin the crossfade (still on screen during it).
act(() => { vi.advanceTimersByTime(1) })
expect(splashEl()).toHaveClass('splash-out')
// Crossfade complete: gone.
act(() => { vi.advanceTimersByTime(250) })
expect(splashEl()).not.toBeInTheDocument()
})
it('yields immediately when the first fetch errors, with no min-time', () => {
vi.useFakeTimers()
const { rerender } = render(<Splash hasData={false} hasError={false} />)
expect(splashEl()).toBeInTheDocument()
rerender(<Splash hasData={false} hasError />)
expect(splashEl()).not.toBeInTheDocument()
})
it('never reappears on a later loading state after it has dismissed', () => {
vi.useFakeTimers()
const { rerender } = render(<Splash hasData={false} hasError={false} />)
rerender(<Splash hasData hasError={false} />)
act(() => { vi.advanceTimersByTime(600) })
act(() => { vi.advanceTimersByTime(250) })
expect(splashEl()).not.toBeInTheDocument()
// A filter change re-enters loading and can clear last-good data; the splash
// must not come back.
rerender(<Splash hasData={false} hasError={false} />)
expect(splashEl()).not.toBeInTheDocument()
rerender(<Splash hasData hasError={false} />)
expect(splashEl()).not.toBeInTheDocument()
})
it('swaps instantly under reduced motion (no fade, no min-time)', () => {
mockReducedMotion(true)
const { rerender } = render(<Splash hasData={false} hasError={false} />)
const el = splashEl()
expect(el).toBeInTheDocument()
expect(el).not.toHaveClass('splash-lit')
// No timers advanced: data lands and the overlay is gone at once.
rerender(<Splash hasData hasError={false} />)
expect(splashEl()).not.toBeInTheDocument()
})
})