2026-06-10 15:01:09 -05:00
|
|
|
import type { ReactElement } from 'react';
|
|
|
|
|
import { ScrollView, Text } from 'react-native';
|
|
|
|
|
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
|
|
|
|
|
|
|
|
|
|
import { AppNavigator } from '../src/navigation/AppNavigator';
|
2026-06-16 11:19:24 -05:00
|
|
|
import { buildVersionLabel } from '../src/config/buildInfo';
|
2026-06-10 15:01:09 -05:00
|
|
|
import {
|
|
|
|
|
policyRoutes,
|
|
|
|
|
primaryMenuRoutes,
|
|
|
|
|
routeDefinitions,
|
|
|
|
|
toolkitRoutes,
|
|
|
|
|
} from '../src/navigation/routes';
|
|
|
|
|
import { defaultVolumeSettings } from '../src/settings/gameSettings';
|
2026-06-16 11:19:24 -05:00
|
|
|
import { ChangelogScreen } from '../src/screens/ChangelogScreen';
|
2026-06-16 10:56:06 -05:00
|
|
|
import { GameplayScreen } from '../src/screens/GameplayScreen';
|
2026-06-10 15:01:09 -05:00
|
|
|
import { HomeScreen } from '../src/screens/HomeScreen';
|
|
|
|
|
import { PageScreen } from '../src/screens/PageScreen';
|
|
|
|
|
import { PoliciesScreen } from '../src/screens/PoliciesScreen';
|
2026-06-16 10:56:06 -05:00
|
|
|
import { SavesScreen } from '../src/screens/SavesScreen';
|
2026-06-10 15:01:09 -05:00
|
|
|
import { SettingsScreen } from '../src/screens/SettingsScreen';
|
|
|
|
|
import { ToolkitScreen } from '../src/screens/ToolkitScreen';
|
|
|
|
|
import {
|
|
|
|
|
useGameSettingsStore,
|
2026-06-12 08:19:40 -05:00
|
|
|
useToolkitContentStore,
|
2026-06-16 10:56:06 -05:00
|
|
|
useGameplayStore,
|
2026-06-10 15:01:09 -05:00
|
|
|
useNavigationStore,
|
2026-06-16 10:56:06 -05:00
|
|
|
useSaveGameStore,
|
2026-06-10 15:01:09 -05:00
|
|
|
useToolkitSocketStore,
|
|
|
|
|
} from '../src/state';
|
2026-06-12 08:19:40 -05:00
|
|
|
import { fallbackCreditsContent } from '../src/data';
|
2026-06-10 15:01:09 -05:00
|
|
|
|
|
|
|
|
const mountedRenderers: ReactTestRenderer[] = [];
|
|
|
|
|
|
|
|
|
|
jest.mock('@react-native-async-storage/async-storage', () => {
|
|
|
|
|
const storage = new Map<string, string>();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
__esModule: true,
|
|
|
|
|
default: {
|
|
|
|
|
getItem: jest.fn((key: string) => Promise.resolve(storage.get(key) ?? null)),
|
|
|
|
|
removeItem: jest.fn((key: string) => {
|
|
|
|
|
storage.delete(key);
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
}),
|
|
|
|
|
setItem: jest.fn((key: string, value: string) => {
|
|
|
|
|
storage.set(key, value);
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
jest.mock('react-native-safe-area-context', () => ({
|
|
|
|
|
SafeAreaProvider: ({ children }: { children: ReactElement }) => children,
|
|
|
|
|
useSafeAreaInsets: () => ({
|
|
|
|
|
bottom: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
right: 0,
|
|
|
|
|
top: 0,
|
|
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
function render(element: ReactElement) {
|
|
|
|
|
let renderer: ReactTestRenderer | undefined;
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
renderer = create(element);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (renderer == null) {
|
|
|
|
|
throw new Error('Renderer was not created.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mountedRenderers.push(renderer);
|
|
|
|
|
|
|
|
|
|
return renderer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getRenderedText(renderer: ReactTestRenderer) {
|
2026-06-16 10:56:06 -05:00
|
|
|
function flattenText(children: unknown): string {
|
|
|
|
|
if (Array.isArray(children)) {
|
|
|
|
|
return children.map(flattenText).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (children == null || typeof children === 'boolean') {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return String(children);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 15:01:09 -05:00
|
|
|
return renderer.root
|
|
|
|
|
.findAllByType(Text)
|
|
|
|
|
.map(instance => instance.props.children)
|
2026-06-16 10:56:06 -05:00
|
|
|
.map(flattenText)
|
2026-06-10 15:01:09 -05:00
|
|
|
.join(' ');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pressByTestID(renderer: ReactTestRenderer, testID: string) {
|
|
|
|
|
const element = renderer.root.findByProps({ testID });
|
|
|
|
|
const props = element.props as { onPress: () => void };
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
props.onPress();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function unmount(renderer: ReactTestRenderer) {
|
|
|
|
|
act(() => {
|
|
|
|
|
renderer.unmount();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const rendererIndex = mountedRenderers.indexOf(renderer);
|
|
|
|
|
|
|
|
|
|
if (rendererIndex >= 0) {
|
|
|
|
|
mountedRenderers.splice(rendererIndex, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('screen scaffold', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
act(() => {
|
|
|
|
|
useGameSettingsStore.setState({
|
2026-06-12 08:19:40 -05:00
|
|
|
audioMuted: false,
|
2026-06-10 15:01:09 -05:00
|
|
|
autosaveEnabled: true,
|
|
|
|
|
resolutionId: 'iphoneDefault',
|
|
|
|
|
volumes: defaultVolumeSettings,
|
|
|
|
|
});
|
2026-06-16 10:56:06 -05:00
|
|
|
useGameplayStore.setState({
|
|
|
|
|
activeRun: null,
|
|
|
|
|
});
|
2026-06-10 15:01:09 -05:00
|
|
|
useNavigationStore.setState({
|
|
|
|
|
currentRouteName: 'home',
|
|
|
|
|
pendingRouteName: null,
|
|
|
|
|
});
|
2026-06-16 10:56:06 -05:00
|
|
|
useSaveGameStore.setState({
|
|
|
|
|
saveSlots: [],
|
|
|
|
|
});
|
2026-06-10 15:01:09 -05:00
|
|
|
useToolkitSocketStore.setState({
|
|
|
|
|
lastConnectedAt: null,
|
|
|
|
|
socketId: null,
|
|
|
|
|
status: 'disconnected',
|
|
|
|
|
});
|
2026-06-12 08:19:40 -05:00
|
|
|
useToolkitContentStore.setState({
|
|
|
|
|
credits: fallbackCreditsContent,
|
|
|
|
|
error: null,
|
|
|
|
|
lastLoadedAt: null,
|
|
|
|
|
revision: null,
|
|
|
|
|
status: 'idle',
|
|
|
|
|
});
|
2026-06-10 15:01:09 -05:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
act(() => {
|
|
|
|
|
for (const renderer of mountedRenderers) {
|
|
|
|
|
renderer.unmount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mountedRenderers.length = 0;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders the home screen game name and primary menu entries', () => {
|
|
|
|
|
const renderer = render(<HomeScreen navigate={jest.fn()} />);
|
|
|
|
|
const text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Throne of the Bone King');
|
|
|
|
|
expect(text).toContain('New Game');
|
|
|
|
|
expect(text).toContain('Saves');
|
|
|
|
|
expect(text).toContain('Settings');
|
|
|
|
|
expect(text).toContain('Policies');
|
2026-06-16 11:19:24 -05:00
|
|
|
expect(text).toContain('Change Log');
|
2026-06-10 15:01:09 -05:00
|
|
|
expect(text).toContain('Credits');
|
2026-06-16 11:19:24 -05:00
|
|
|
expect(text).toContain(buildVersionLabel);
|
2026-06-10 15:01:09 -05:00
|
|
|
expect(text).not.toContain('Toolkit');
|
2026-06-12 08:19:40 -05:00
|
|
|
expect(renderer.root.findByProps({ testID: 'home-build-version' })).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'home-background' })).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'home-menu-background' })).toBeTruthy();
|
2026-06-10 15:01:09 -05:00
|
|
|
expect(renderer.root.findByProps({ testID: 'menu-newGame-icon' })).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'menu-saves-icon' })).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'menu-settings-icon' })).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'menu-policies-icon' })).toBeTruthy();
|
2026-06-16 11:19:24 -05:00
|
|
|
expect(renderer.root.findByProps({ testID: 'menu-changelog-icon' })).toBeTruthy();
|
2026-06-10 15:01:09 -05:00
|
|
|
expect(renderer.root.findByProps({ testID: 'menu-credits-icon' })).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findAllByProps({ testID: 'menu-toolkit' })).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-16 10:56:06 -05:00
|
|
|
it('starts a new game and navigates to gameplay from the home menu', () => {
|
|
|
|
|
const navigate = jest.fn();
|
|
|
|
|
const renderer = render(<HomeScreen navigate={navigate} />);
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'menu-newGame');
|
|
|
|
|
|
|
|
|
|
expect(navigate).toHaveBeenCalledWith('gameplay');
|
|
|
|
|
expect(useGameplayStore.getState().activeRun).toEqual(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
currentPhaseId: 'dailySeed',
|
|
|
|
|
turn: 1,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-10 15:01:09 -05:00
|
|
|
it('shows the toolkit home entry as a distinct button only in browser sizing', () => {
|
|
|
|
|
act(() => {
|
|
|
|
|
useGameSettingsStore.setState({
|
2026-06-12 08:19:40 -05:00
|
|
|
audioMuted: false,
|
2026-06-10 15:01:09 -05:00
|
|
|
autosaveEnabled: true,
|
|
|
|
|
resolutionId: 'browser',
|
|
|
|
|
volumes: defaultVolumeSettings,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const renderer = render(<HomeScreen navigate={jest.fn()} />);
|
|
|
|
|
const text = getRenderedText(renderer);
|
|
|
|
|
const toolkitIcon = renderer.root.findByProps({ testID: 'menu-toolkit-icon' });
|
|
|
|
|
const toolkitIconStyle = toolkitIcon.props.style as Array<
|
|
|
|
|
Record<string, string | number> | null
|
|
|
|
|
>;
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Toolkit');
|
|
|
|
|
expect(toolkitIcon).toBeTruthy();
|
|
|
|
|
expect(toolkitIconStyle).toEqual(
|
|
|
|
|
expect.arrayContaining([
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
color: '#f5d98d',
|
|
|
|
|
}),
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'menu-toolkit' }).props).toMatchObject(
|
|
|
|
|
{
|
|
|
|
|
disabled: true,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('enables the toolkit home entry when the toolkit socket is connected', () => {
|
|
|
|
|
act(() => {
|
|
|
|
|
useGameSettingsStore.setState({
|
2026-06-12 08:19:40 -05:00
|
|
|
audioMuted: false,
|
2026-06-10 15:01:09 -05:00
|
|
|
autosaveEnabled: true,
|
|
|
|
|
resolutionId: 'browser',
|
|
|
|
|
volumes: defaultVolumeSettings,
|
|
|
|
|
});
|
|
|
|
|
useToolkitSocketStore.getState().setConnected('test-socket');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const renderer = render(<HomeScreen navigate={jest.fn()} />);
|
|
|
|
|
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'menu-toolkit' }).props).toMatchObject(
|
|
|
|
|
{
|
|
|
|
|
disabled: false,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders a return-to-home control on non-home pages', () => {
|
|
|
|
|
const renderer = render(
|
|
|
|
|
<PageScreen goHome={jest.fn()} routeName="newGame" />,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'back-home-button' })).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-16 11:19:24 -05:00
|
|
|
it('renders changelog entries newest first', () => {
|
|
|
|
|
const renderer = render(<ChangelogScreen goHome={jest.fn()} />);
|
|
|
|
|
const text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Build Metadata and Changelog Scaffold');
|
|
|
|
|
expect(text).toContain('0.0.1-20260616-13aa686');
|
|
|
|
|
expect(text).toContain('Seeded data-backed changelog content');
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'changelog-screen' })).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'back-home-button' })).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-16 10:56:06 -05:00
|
|
|
it('renders gameplay phases and moves through the turn scaffold', () => {
|
|
|
|
|
act(() => {
|
|
|
|
|
useGameplayStore.getState().startNewGame();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const renderer = render(<GameplayScreen navigate={jest.fn()} />);
|
|
|
|
|
let text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Daily Seed / Start Day');
|
|
|
|
|
expect(text).toContain('Day 1');
|
|
|
|
|
expect(text).toContain('Phase 1 of 4');
|
|
|
|
|
expect(text).toContain('Resource HUD');
|
|
|
|
|
expect(text).toContain('Daily Seed');
|
|
|
|
|
expect(text).toContain('Throne Room');
|
|
|
|
|
expect(text).toContain('World Map');
|
|
|
|
|
expect(text).toContain('Daily Summary');
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'gameplay-prior-phase' }).props).toMatchObject(
|
|
|
|
|
{
|
|
|
|
|
disabled: true,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'gameplay-next-phase');
|
|
|
|
|
text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Throne Room Petitioners');
|
|
|
|
|
expect(text).toContain('Phase 2 of 4');
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'gameplay-next-phase');
|
|
|
|
|
pressByTestID(renderer, 'gameplay-next-phase');
|
|
|
|
|
text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Daily Summary');
|
|
|
|
|
expect(text).toContain('End Turn');
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'gameplay-next-phase');
|
|
|
|
|
text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Day 2');
|
|
|
|
|
expect(text).toContain('Daily Seed / Start Day');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('opens the escape menu and exposes save load sound and exit controls', () => {
|
|
|
|
|
const navigate = jest.fn();
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
useGameplayStore.getState().startNewGame();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const renderer = render(<GameplayScreen navigate={navigate} />);
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'gameplay-open-escape-menu');
|
|
|
|
|
|
|
|
|
|
let text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Escape Menu');
|
|
|
|
|
expect(text).toContain('Save');
|
|
|
|
|
expect(text).toContain('Load');
|
|
|
|
|
expect(text).toContain('Sound');
|
|
|
|
|
expect(text).toContain('Exit Game');
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'escape-mute' })).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'escape-volume-master' })).toBeTruthy();
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'escape-save');
|
|
|
|
|
text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Saved Ossuary Dominion Run.');
|
|
|
|
|
expect(useSaveGameStore.getState().saveSlots).toHaveLength(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders saved games and loads one into gameplay', () => {
|
|
|
|
|
const navigate = jest.fn();
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
useGameplayStore.getState().startNewGame();
|
|
|
|
|
useGameplayStore.getState().goToNextPhase();
|
|
|
|
|
useGameplayStore.getState().saveCurrentRun();
|
|
|
|
|
useGameplayStore.getState().goToNextPhase();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const saveSlot = useSaveGameStore.getState().saveSlots[0];
|
|
|
|
|
const renderer = render(
|
|
|
|
|
<SavesScreen goHome={jest.fn()} navigate={navigate} />,
|
|
|
|
|
);
|
|
|
|
|
const text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Ossuary Dominion Run');
|
|
|
|
|
expect(text).toContain('Day 1');
|
|
|
|
|
expect(text).toContain('Throne Room');
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, `load-save-${saveSlot.id}`);
|
|
|
|
|
|
|
|
|
|
expect(navigate).toHaveBeenCalledWith('gameplay');
|
|
|
|
|
expect(useGameplayStore.getState().activeRun?.currentPhaseId).toBe(
|
|
|
|
|
'throneRoom',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders an empty saves state', () => {
|
|
|
|
|
const renderer = render(
|
|
|
|
|
<SavesScreen goHome={jest.fn()} navigate={jest.fn()} />,
|
|
|
|
|
);
|
|
|
|
|
const text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('No saved games yet');
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'saves-empty' })).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-10 15:01:09 -05:00
|
|
|
it('renders all policy page links from the policies index', () => {
|
|
|
|
|
const renderer = render(
|
|
|
|
|
<PoliciesScreen goHome={jest.fn()} navigate={jest.fn()} />,
|
|
|
|
|
);
|
|
|
|
|
const text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Legal');
|
|
|
|
|
expect(text).toContain('Privacy');
|
|
|
|
|
expect(text).toContain('Terms and Conditions');
|
|
|
|
|
expect(text).toContain('Cookies');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders settings controls for resolution and audio channels', () => {
|
|
|
|
|
const renderer = render(<SettingsScreen goHome={jest.fn()} />);
|
|
|
|
|
const text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Game Resolution');
|
|
|
|
|
expect(text).toContain('Phone Default');
|
2026-06-12 08:19:40 -05:00
|
|
|
expect(text).toContain('Master');
|
|
|
|
|
expect(text).toContain('Music');
|
|
|
|
|
expect(text).toContain('Ambience');
|
|
|
|
|
expect(text).toContain('Speech');
|
|
|
|
|
expect(text).toContain('SFX');
|
|
|
|
|
expect(text).toContain('Mute');
|
2026-06-10 15:01:09 -05:00
|
|
|
expect(text).toContain('Autosave');
|
2026-06-12 08:19:40 -05:00
|
|
|
expect(renderer.root.findByProps({ testID: 'audio-mute-checkbox' })).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'volume-master-slider' })).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'volume-music-slider' })).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findAllByProps({ testID: 'volume-music-decrease' })).toHaveLength(
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
expect(renderer.root.findAllByProps({ testID: 'volume-music-increase' })).toHaveLength(
|
|
|
|
|
0,
|
|
|
|
|
);
|
2026-06-10 15:01:09 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders the toolkit template and dense toolkit menu entries', () => {
|
|
|
|
|
const renderer = render(
|
|
|
|
|
<ToolkitScreen
|
|
|
|
|
goHome={jest.fn()}
|
|
|
|
|
navigate={jest.fn()}
|
|
|
|
|
routeName="toolkit"
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
const text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Toolkit');
|
2026-06-12 07:13:41 -05:00
|
|
|
expect(text).toContain('Opening');
|
|
|
|
|
expect(text).toContain('Petitioners');
|
|
|
|
|
expect(text).toContain('Rulings');
|
|
|
|
|
expect(text).toContain('Seed');
|
2026-06-10 15:01:09 -05:00
|
|
|
expect(text).toContain('Resources');
|
2026-06-12 07:13:41 -05:00
|
|
|
expect(text).toContain('Map');
|
|
|
|
|
expect(text).toContain('Factions');
|
|
|
|
|
expect(text).toContain('Flags');
|
|
|
|
|
expect(text).toContain('Story');
|
|
|
|
|
expect(text).toContain('Lore');
|
|
|
|
|
expect(text).toContain('Audio');
|
2026-06-12 08:19:40 -05:00
|
|
|
expect(text).toContain('Credits');
|
2026-06-12 07:13:41 -05:00
|
|
|
expect(text).toContain('Endings');
|
|
|
|
|
expect(text).toContain('References');
|
|
|
|
|
expect(text).toContain('Read-only planning panel');
|
|
|
|
|
expect(text).toContain('Future authoring area');
|
2026-06-10 15:01:09 -05:00
|
|
|
|
|
|
|
|
const homeButton = renderer.root.findByProps({ testID: 'toolkit-home' });
|
|
|
|
|
|
|
|
|
|
expect(homeButton.props.accessibilityLabel).toBe('Home');
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'toolkit-home-icon' })).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findAllByProps({ testID: 'toolkit-home-label' })).toHaveLength(
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'toolkit-socket-indicator' })).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-12 07:13:41 -05:00
|
|
|
it('renders purpose panels for every toolkit section without forms', () => {
|
|
|
|
|
const expectedPurposeText = {
|
|
|
|
|
toolkitAudio: 'Preview and catalog sound identity',
|
|
|
|
|
toolkitEndings: 'Tune collapse states',
|
|
|
|
|
toolkitFactions: 'Tune faction trust',
|
|
|
|
|
toolkitFlags: 'Debug delayed consequences',
|
|
|
|
|
toolkitLore: 'Reference changing canon',
|
|
|
|
|
toolkitMap: 'Support settlement and world-map iteration',
|
|
|
|
|
toolkitOpening: 'Preview and sequence the first-run premise',
|
|
|
|
|
toolkitPetitioners: 'Inspect and eventually author throne-room cases',
|
|
|
|
|
toolkitReferences: 'Keep design inspirations',
|
|
|
|
|
toolkitResources: 'Tune the core Bones, Minions, and Milk economy',
|
|
|
|
|
toolkitRulings: 'Inspect and eventually tune choice outcomes',
|
|
|
|
|
toolkitSeed: 'Debug why a day produced specific petitioners',
|
|
|
|
|
toolkitStory: 'Organize Act I-IV pressure',
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
for (const routeName of toolkitRoutes) {
|
2026-06-12 08:19:40 -05:00
|
|
|
if (routeName === 'toolkitCredits') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 07:13:41 -05:00
|
|
|
const renderer = render(
|
|
|
|
|
<ToolkitScreen
|
|
|
|
|
goHome={jest.fn()}
|
|
|
|
|
navigate={jest.fn()}
|
|
|
|
|
routeName={routeName}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
const text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain(expectedPurposeText[routeName]);
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'toolkit-purpose' })).toBeTruthy();
|
|
|
|
|
expect(
|
|
|
|
|
renderer.root.findByProps({ testID: 'toolkit-change-note' }),
|
|
|
|
|
).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'toolkit-sources' })).toBeTruthy();
|
|
|
|
|
expect(
|
|
|
|
|
renderer.root.findByProps({ testID: 'toolkit-future-fields' }),
|
|
|
|
|
).toBeTruthy();
|
|
|
|
|
expect(
|
|
|
|
|
renderer.root.findByProps({ testID: 'toolkit-future-placeholder' }),
|
|
|
|
|
).toBeTruthy();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-12 08:19:40 -05:00
|
|
|
it('renders the toolkit credits display card from fallback data', () => {
|
|
|
|
|
const renderer = render(
|
|
|
|
|
<ToolkitScreen
|
|
|
|
|
goHome={jest.fn()}
|
|
|
|
|
navigate={jest.fn()}
|
|
|
|
|
routeName="toolkitCredits"
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
const text = getRenderedText(renderer);
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Throne of the Bone King');
|
|
|
|
|
expect(text).toContain('A Bone Lord Bob kingdom-management story');
|
|
|
|
|
expect(text).toContain('Jacob Mathison');
|
|
|
|
|
expect(text).toContain('React Native');
|
|
|
|
|
expect(text).toContain('Font Library contributors');
|
2026-06-16 11:19:24 -05:00
|
|
|
expect(text).toContain(buildVersionLabel);
|
2026-06-12 08:19:40 -05:00
|
|
|
expect(text).toContain('Bundled fallback credits');
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'toolkit-credits-card' })).toBeTruthy();
|
|
|
|
|
expect(
|
|
|
|
|
renderer.root.findByProps({ testID: 'toolkit-credits-contributors' }),
|
|
|
|
|
).toBeTruthy();
|
|
|
|
|
expect(
|
|
|
|
|
renderer.root.findByProps({ testID: 'toolkit-credits-technologies' }),
|
|
|
|
|
).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'toolkit-credits-thanks' })).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-10 15:01:09 -05:00
|
|
|
it('renders the toolkit websocket connection indicator', () => {
|
|
|
|
|
act(() => {
|
|
|
|
|
useToolkitSocketStore.getState().setConnected('test-socket');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const renderer = render(
|
|
|
|
|
<ToolkitScreen
|
|
|
|
|
goHome={jest.fn()}
|
|
|
|
|
navigate={jest.fn()}
|
|
|
|
|
routeName="toolkit"
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
const text = getRenderedText(renderer);
|
|
|
|
|
const socketDot = renderer.root.findByProps({ testID: 'toolkit-socket-dot' });
|
|
|
|
|
|
|
|
|
|
expect(text).toContain('Connected');
|
|
|
|
|
expect(socketDot.props.style).toEqual(
|
|
|
|
|
expect.arrayContaining([
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
backgroundColor: '#3fbf6f',
|
|
|
|
|
}),
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders toolkit navigation as a dense top toolbar', () => {
|
|
|
|
|
const renderer = render(
|
|
|
|
|
<ToolkitScreen
|
|
|
|
|
goHome={jest.fn()}
|
|
|
|
|
navigate={jest.fn()}
|
|
|
|
|
routeName="toolkit"
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const toolbar = renderer.root.findByProps({ testID: 'toolkit-toolbar' });
|
|
|
|
|
const optionGroup = renderer.root.findByProps({
|
|
|
|
|
testID: 'toolkit-option-group',
|
|
|
|
|
});
|
|
|
|
|
const homeSlot = renderer.root.findByProps({ testID: 'toolkit-home-slot' });
|
|
|
|
|
|
|
|
|
|
expect(toolbar.props.style).toMatchObject({ flexDirection: 'row' });
|
|
|
|
|
expect(optionGroup.props.style).toMatchObject({ flexDirection: 'row' });
|
|
|
|
|
expect(homeSlot.props.style).toMatchObject({ marginLeft: 'auto' });
|
|
|
|
|
expect(
|
2026-06-12 07:13:41 -05:00
|
|
|
renderer.root.findByProps({ testID: 'toolkit-toolkitOpening-icon' }),
|
2026-06-10 15:01:09 -05:00
|
|
|
).toBeTruthy();
|
|
|
|
|
expect(
|
2026-06-12 07:13:41 -05:00
|
|
|
renderer.root.findByProps({ testID: 'toolkit-toolkitOpening-label' }),
|
2026-06-10 15:01:09 -05:00
|
|
|
).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('opens the resolution select and renders all options', () => {
|
|
|
|
|
const renderer = render(<SettingsScreen goHome={jest.fn()} />);
|
|
|
|
|
|
|
|
|
|
expect(renderer.root.findAllByProps({ testID: 'resolution-options' })).toHaveLength(
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'resolution-select');
|
|
|
|
|
|
|
|
|
|
const options = renderer.root.findByProps({ testID: 'resolution-options' });
|
|
|
|
|
|
|
|
|
|
expect(options).toBeTruthy();
|
|
|
|
|
expect(options.props.style).toMatchObject({ position: 'absolute' });
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'resolution-browser' })).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'resolution-iphoneMini' })).toBeTruthy();
|
|
|
|
|
expect(
|
|
|
|
|
renderer.root.findByProps({ testID: 'resolution-iphoneDefault' }),
|
|
|
|
|
).toBeTruthy();
|
|
|
|
|
expect(
|
|
|
|
|
renderer.root.findByProps({ testID: 'resolution-androidDefault' }),
|
|
|
|
|
).toBeTruthy();
|
|
|
|
|
expect(renderer.root.findByProps({ testID: 'resolution-phoneLarge' })).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('keeps the active settings route when changing resolution', () => {
|
|
|
|
|
const renderer = render(<AppNavigator />);
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'menu-settings');
|
|
|
|
|
expect(getRenderedText(renderer)).toContain(routeDefinitions.settings.title);
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'resolution-select');
|
|
|
|
|
pressByTestID(renderer, 'resolution-browser');
|
|
|
|
|
|
|
|
|
|
expect(getRenderedText(renderer)).toContain(routeDefinitions.settings.title);
|
|
|
|
|
expect(getRenderedText(renderer)).not.toContain(routeDefinitions.home.title);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('restores the active route when the navigator remounts', () => {
|
|
|
|
|
const renderer = render(<AppNavigator />);
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'menu-settings');
|
|
|
|
|
expect(getRenderedText(renderer)).toContain(routeDefinitions.settings.title);
|
|
|
|
|
|
|
|
|
|
unmount(renderer);
|
|
|
|
|
|
|
|
|
|
const remountedRenderer = render(<AppNavigator />);
|
|
|
|
|
|
|
|
|
|
expect(getRenderedText(remountedRenderer)).toContain(
|
|
|
|
|
routeDefinitions.settings.title,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not use scroll containers for fixed landscape screens', () => {
|
|
|
|
|
const screens = [
|
|
|
|
|
<HomeScreen key="home" navigate={jest.fn()} />,
|
2026-06-16 11:19:24 -05:00
|
|
|
<ChangelogScreen key="changelog" goHome={jest.fn()} />,
|
2026-06-16 10:56:06 -05:00
|
|
|
<GameplayScreen key="gameplay" navigate={jest.fn()} />,
|
2026-06-10 15:01:09 -05:00
|
|
|
<PageScreen key="page" goHome={jest.fn()} routeName="newGame" />,
|
|
|
|
|
<PoliciesScreen key="policies" goHome={jest.fn()} navigate={jest.fn()} />,
|
2026-06-16 10:56:06 -05:00
|
|
|
<SavesScreen key="saves" goHome={jest.fn()} navigate={jest.fn()} />,
|
2026-06-10 15:01:09 -05:00
|
|
|
<SettingsScreen key="settings" goHome={jest.fn()} />,
|
|
|
|
|
<ToolkitScreen
|
|
|
|
|
key="toolkit"
|
|
|
|
|
goHome={jest.fn()}
|
|
|
|
|
navigate={jest.fn()}
|
|
|
|
|
routeName="toolkit"
|
|
|
|
|
/>,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const screen of screens) {
|
|
|
|
|
const renderer = render(screen);
|
|
|
|
|
expect(renderer.root.findAllByType(ScrollView)).toHaveLength(0);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('navigates to each scaffold page and returns home', () => {
|
|
|
|
|
const renderer = render(<AppNavigator />);
|
|
|
|
|
|
|
|
|
|
for (const routeName of primaryMenuRoutes) {
|
2026-06-16 10:56:06 -05:00
|
|
|
if (routeName === 'newGame' || routeName === 'toolkit') {
|
2026-06-10 15:01:09 -05:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, `menu-${routeName}`);
|
|
|
|
|
expect(getRenderedText(renderer)).toContain(routeDefinitions[routeName].title);
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'back-home-button');
|
|
|
|
|
expect(getRenderedText(renderer)).toContain(routeDefinitions.home.title);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'menu-policies');
|
|
|
|
|
|
|
|
|
|
for (const routeName of policyRoutes) {
|
|
|
|
|
pressByTestID(renderer, `policy-${routeName}`);
|
|
|
|
|
expect(getRenderedText(renderer)).toContain(routeDefinitions[routeName].title);
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'back-home-button');
|
|
|
|
|
expect(getRenderedText(renderer)).toContain(routeDefinitions.home.title);
|
|
|
|
|
|
|
|
|
|
if (routeName !== policyRoutes[policyRoutes.length - 1]) {
|
|
|
|
|
pressByTestID(renderer, 'menu-policies');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
useGameSettingsStore.getState().setResolution('browser');
|
|
|
|
|
useToolkitSocketStore.getState().setConnected('test-socket');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'menu-toolkit');
|
|
|
|
|
|
|
|
|
|
for (const routeName of toolkitRoutes) {
|
|
|
|
|
pressByTestID(renderer, `toolkit-${routeName}`);
|
|
|
|
|
expect(getRenderedText(renderer)).toContain(routeDefinitions[routeName].title);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pressByTestID(renderer, 'toolkit-home');
|
|
|
|
|
expect(getRenderedText(renderer)).toContain(routeDefinitions.home.title);
|
|
|
|
|
});
|
|
|
|
|
});
|