Files
blb-throne-of-the-bone-king/__tests__/screenScaffold.test.tsx
T

538 lines
18 KiB
TypeScript

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';
import {
policyRoutes,
primaryMenuRoutes,
routeDefinitions,
toolkitRoutes,
} from '../src/navigation/routes';
import { defaultVolumeSettings } from '../src/settings/gameSettings';
import { HomeScreen } from '../src/screens/HomeScreen';
import { PageScreen } from '../src/screens/PageScreen';
import { PoliciesScreen } from '../src/screens/PoliciesScreen';
import { SettingsScreen } from '../src/screens/SettingsScreen';
import { ToolkitScreen } from '../src/screens/ToolkitScreen';
import {
useGameSettingsStore,
useToolkitContentStore,
useNavigationStore,
useToolkitSocketStore,
} from '../src/state';
import { fallbackCreditsContent } from '../src/data';
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) {
return renderer.root
.findAllByType(Text)
.map(instance => instance.props.children)
.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({
audioMuted: false,
autosaveEnabled: true,
resolutionId: 'iphoneDefault',
volumes: defaultVolumeSettings,
});
useNavigationStore.setState({
currentRouteName: 'home',
pendingRouteName: null,
});
useToolkitSocketStore.setState({
lastConnectedAt: null,
socketId: null,
status: 'disconnected',
});
useToolkitContentStore.setState({
credits: fallbackCreditsContent,
error: null,
lastLoadedAt: null,
revision: null,
status: 'idle',
});
});
});
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');
expect(text).toContain('Credits');
expect(text).toContain('Build v0.0.1');
expect(text).not.toContain('Toolkit');
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();
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();
expect(renderer.root.findByProps({ testID: 'menu-credits-icon' })).toBeTruthy();
expect(renderer.root.findAllByProps({ testID: 'menu-toolkit' })).toHaveLength(0);
});
it('shows the toolkit home entry as a distinct button only in browser sizing', () => {
act(() => {
useGameSettingsStore.setState({
audioMuted: false,
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({
audioMuted: false,
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();
});
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');
expect(text).toContain('Master');
expect(text).toContain('Music');
expect(text).toContain('Ambience');
expect(text).toContain('Speech');
expect(text).toContain('SFX');
expect(text).toContain('Mute');
expect(text).toContain('Autosave');
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,
);
});
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');
expect(text).toContain('Opening');
expect(text).toContain('Petitioners');
expect(text).toContain('Rulings');
expect(text).toContain('Seed');
expect(text).toContain('Resources');
expect(text).toContain('Map');
expect(text).toContain('Factions');
expect(text).toContain('Flags');
expect(text).toContain('Story');
expect(text).toContain('Lore');
expect(text).toContain('Audio');
expect(text).toContain('Credits');
expect(text).toContain('Endings');
expect(text).toContain('References');
expect(text).toContain('Read-only planning panel');
expect(text).toContain('Future authoring area');
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();
});
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) {
if (routeName === 'toolkitCredits') {
continue;
}
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();
}
});
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');
expect(text).toContain('Build v0.0.1');
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();
});
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(
renderer.root.findByProps({ testID: 'toolkit-toolkitOpening-icon' }),
).toBeTruthy();
expect(
renderer.root.findByProps({ testID: 'toolkit-toolkitOpening-label' }),
).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()} />,
<PageScreen key="page" goHome={jest.fn()} routeName="newGame" />,
<PoliciesScreen key="policies" goHome={jest.fn()} navigate={jest.fn()} />,
<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) {
if (routeName === 'toolkit') {
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);
});
});