427 lines
13 KiB
TypeScript
427 lines
13 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,
|
||
|
|
useNavigationStore,
|
||
|
|
useToolkitSocketStore,
|
||
|
|
} from '../src/state';
|
||
|
|
|
||
|
|
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({
|
||
|
|
autosaveEnabled: true,
|
||
|
|
resolutionId: 'iphoneDefault',
|
||
|
|
volumes: defaultVolumeSettings,
|
||
|
|
});
|
||
|
|
useNavigationStore.setState({
|
||
|
|
currentRouteName: 'home',
|
||
|
|
pendingRouteName: null,
|
||
|
|
});
|
||
|
|
useToolkitSocketStore.setState({
|
||
|
|
lastConnectedAt: null,
|
||
|
|
socketId: null,
|
||
|
|
status: 'disconnected',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
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).not.toContain('Toolkit');
|
||
|
|
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({
|
||
|
|
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({
|
||
|
|
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 Volume');
|
||
|
|
expect(text).toContain('Music Volume');
|
||
|
|
expect(text).toContain('Ambience Volume');
|
||
|
|
expect(text).toContain('Speech Volume');
|
||
|
|
expect(text).toContain('Autosave');
|
||
|
|
});
|
||
|
|
|
||
|
|
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 Sequence');
|
||
|
|
expect(text).toContain('Credits');
|
||
|
|
expect(text).toContain('Audio');
|
||
|
|
expect(text).toContain('Lore');
|
||
|
|
expect(text).toContain('Resources');
|
||
|
|
|
||
|
|
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 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-toolkitOpeningSequence-icon' }),
|
||
|
|
).toBeTruthy();
|
||
|
|
expect(
|
||
|
|
renderer.root.findByProps({ testID: 'toolkit-toolkitOpeningSequence-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);
|
||
|
|
});
|
||
|
|
});
|