testsAndMisc/reverse_survivors/src/App.test.tsx
Krzysztof kuhy Rudnicki 1342a60a65
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
reverse-survivors CI / check (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Split reverse_survivors sim.ts 361 -> 69 + three modules
Split along actor lines, but not the obvious way. A plain sim/enemies cut
needs makeEnemy and grantXp going out and enemiesStep coming back -- a cycle.
Nothing here would have caught it: no eslint-plugin-import, and tsc and vitest
both pass on cyclic ES modules. It shows up as a TDZ error in the bundle.

So the graph is a tree instead:

  simUnit.ts (42)      leaf -- geometry, projectile constants, makeEnemy,
                       enemySpeed; imports only types and vec
  simSurvivor.ts (132) createSurvivor, xp/upgrades, survivorStep
  simThreats.ts (168)  enemiesStep, projectilesStep, and enemy death
  sim.ts (69)          createInitialState and step, the only module that
                       reaches all three

Repointing director.ts's makeEnemy at the leaf also removes the sim <->
director cycle that was already there, surviving on const-arrow late binding.

npm run check and npm run build both pass -- 160 tests, coverage identical at
531/159/101/514, 32 modules bundled at 210.09 kB against 210.11 kB before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 09:52:19 +02:00

91 lines
3.1 KiB
TypeScript

import { act, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { App } from './App'
import { makeEnemy } from './core/simUnit'
import type { RafHarness } from './test/harness'
import { installRaf, makeCtxStub } from './test/harness'
afterEach(() => {
vi.unstubAllGlobals()
vi.restoreAllMocks()
})
const pump = (raf: RafHarness, ts: number): void => {
act(() => {
raf.pump(ts)
})
}
describe('App', () => {
it('runs the loop and accrues souls even without a canvas context', () => {
const raf = installRaf()
render(<App />)
expect(screen.getByText('30 / 400')).toBeInTheDocument()
pump(raf, 0)
for (let i = 1; i <= 20; i += 1) {
pump(raf, i * 1000)
}
expect(screen.getByText('42 / 400')).toBeInTheDocument()
expect(screen.getByText(/0:02 \/ 5:00/)).toBeInTheDocument()
})
it('spends souls on a spawn and draws the horde', () => {
const raf = installRaf()
const stub = makeCtxStub()
vi.spyOn(HTMLCanvasElement.prototype, 'getContext')
.mockImplementation(() => stub as unknown as CanvasRenderingContext2D)
render(<App seed={5} />)
fireEvent.click(screen.getByRole('button', { name: /rusher/i }))
pump(raf, 0)
expect(screen.getByText('20 / 400')).toBeInTheDocument()
const arcsWithHorde = stub.arc.mock.calls.length
expect(arcsWithHorde).toBeGreaterThan(4)
})
it('shows the director victory overlay and restarts fresh', () => {
const raf = installRaf()
render(
<App
seed={9}
boot={(state) => {
state.survivor.hp = 5
makeEnemy(state, 'rusher', { x: state.survivor.pos.x, y: state.survivor.pos.y })
}}
/>,
)
pump(raf, 0)
expect(screen.getByText('The intruder falls')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /open the gates/i }))
expect(screen.queryByText('The intruder falls')).not.toBeInTheDocument()
expect(screen.getByText('30 / 400')).toBeInTheDocument()
})
it('restarts on a new tier when the difficulty is changed', () => {
const raf = installRaf()
render(<App seed={9} />)
pump(raf, 0)
// Normal runs 5:00; Crusade runs 6:00, so the clock proves the tier took.
expect(screen.getByText(/\/ 5:00/)).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Crusade' }))
expect(screen.getByText(/\/ 6:00/)).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Crusade' })).toHaveClass('diff-on')
})
it('dispatches a director power from the bar', () => {
const raf = installRaf()
render(<App seed={9} boot={(state) => { state.director.energy = 400 }} />)
pump(raf, 0)
fireEvent.click(screen.getByRole('button', { name: /frenzy/i }))
pump(raf, 16)
expect(screen.getByText(/frenzied/i)).toBeInTheDocument()
})
it('shows the dawn overlay when the survivor outlasts the horde', () => {
const raf = installRaf()
render(<App boot={(state) => { state.duration = 0.05 }} />)
pump(raf, 0)
pump(raf, 100)
expect(screen.getByText('Dawn breaks')).toBeInTheDocument()
})
})