414a554549
- content/seasons/01-soil/ui-strings.yaml: player-visible Phase-2 copy externalized per CLAUDE.md (Begin / seed picker / post-harvest beat / journal / settings / plant display names); voice reviewed against bible + anti-fomo-doctrine.md
- content/seasons/01-soil/fragments.yaml: placeholder Season-1 fragment file (Plan 02-03 expands to ≥10 authored)
- content/seasons/00-demo/: deleted (Phase-1 demo replaced)
- src/content/schemas/ui-strings.ts: UiStringsSchema (Zod) — validates structure of every season's ui-strings.yaml at load time
- src/content/schemas/index.ts + src/content/index.ts: re-export UiStringsSchema/UiStrings
- src/content/loader.ts: eager `uiStrings` glob + PIPE-02 lazy `loadSeasonFragments(seasonId)` (Plan 02-03+ exploit)
- src/ui/begin/use-audio-bootstrap.ts: bootstrapAudioContext() lazy-creates + resumes (RESEARCH Pattern 9; Pitfall 5 mitigation — context construction inside the gesture for iOS Safari) + installFirstInteractionGestureHandler() one-shot for D-22 returning players + __resetAudioBootstrapForTest()
- src/ui/begin/BeginScreen.tsx: D-21 typographic Begin screen — title + subtitle + CTA from uiStrings[1].begin; onClick calls bootstrapAudioContext synchronously inside the click event then dismisses the session gate (D-22)
- src/ui/begin/BeginScreen.test.tsx: 4 tests — render / D-22 skip / click bootstraps + dismisses / subtitle string
- src/ui/garden/SeedPicker.tsx: D-02 inline DOM popover; subscribes to 'tile-clicked-coords'; renders one button per unlocked plant type from uiStrings[1].plants; click enqueues plantSeed command via store.enqueueCommand
- src/ui/garden/SeedPicker.test.tsx: 6 tests — initial-null / coords-positioned / unlocked-only / enqueue / dismiss / multi-plant; mocks game/event-bus to avoid Phaser canvas init under happy-dom (deviation Rule 3)
- src/ui/{begin,garden,index}.ts: barrels
- src/App.tsx: mount BeginScreen + SeedPicker as overlay siblings to PhaserGame
- src/PhaserGame.tsx: bootstrap unlockedPlantTypes=['rosemary'] for first-run; install gesture handler + scene-ready listener
- npm run ci exits 0; 163/163 tests pass (10 new this commit + 25 from Task 1 + 128 baseline)
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
|
import { BeginScreen } from './BeginScreen';
|
|
import { appStore } from '../../store';
|
|
import { __resetAudioBootstrapForTest } from './use-audio-bootstrap';
|
|
|
|
// Mock the audio bootstrap module so happy-dom (which lacks AudioContext)
|
|
// doesn't blow up; also gives us a spy to assert the click invokes it.
|
|
vi.mock('./use-audio-bootstrap', async () => {
|
|
const actual =
|
|
await vi.importActual<typeof import('./use-audio-bootstrap')>('./use-audio-bootstrap');
|
|
return {
|
|
...actual,
|
|
bootstrapAudioContext: vi.fn().mockResolvedValue(null),
|
|
};
|
|
});
|
|
|
|
import { bootstrapAudioContext } from './use-audio-bootstrap';
|
|
|
|
describe('BeginScreen (AEST-07, D-21, D-22)', () => {
|
|
beforeEach(() => {
|
|
cleanup();
|
|
appStore.setState({ beginGateDismissed: false });
|
|
__resetAudioBootstrapForTest();
|
|
vi.mocked(bootstrapAudioContext).mockClear();
|
|
});
|
|
|
|
it('renders the title and Begin CTA when not dismissed', () => {
|
|
render(<BeginScreen />);
|
|
expect(screen.getByText('The Last Garden')).toBeTruthy();
|
|
expect(screen.getByRole('button', { name: 'Begin' })).toBeTruthy();
|
|
});
|
|
|
|
it('renders nothing when beginGateDismissed=true (D-22 returning-player skip)', () => {
|
|
appStore.setState({ beginGateDismissed: true });
|
|
const { container } = render(<BeginScreen />);
|
|
expect(container.firstChild).toBeNull();
|
|
});
|
|
|
|
it('dismisses the gate and triggers audio bootstrap on click', () => {
|
|
render(<BeginScreen />);
|
|
fireEvent.click(screen.getByRole('button', { name: 'Begin' }));
|
|
expect(bootstrapAudioContext).toHaveBeenCalledTimes(1);
|
|
expect(appStore.getState().beginGateDismissed).toBe(true);
|
|
});
|
|
|
|
it('subtitle is the externalized "tend" string (CLAUDE.md tone)', () => {
|
|
render(<BeginScreen />);
|
|
expect(screen.getByText('tend')).toBeTruthy();
|
|
});
|
|
});
|