feat(app): add web electron toolkit support
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
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();
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import { createPersistOptions } from '../src/state/createPersistOptions';
|
||||
|
||||
type TestState = {
|
||||
hydrated: boolean;
|
||||
transientValue: string;
|
||||
};
|
||||
|
||||
describe('createPersistOptions', () => {
|
||||
it('creates named options with JSON storage and preserves extra options', () => {
|
||||
const options = createPersistOptions<TestState, Pick<TestState, 'hydrated'>>({
|
||||
name: 'test-state',
|
||||
partialize: state => ({ hydrated: state.hydrated }),
|
||||
version: 2,
|
||||
});
|
||||
|
||||
expect(options.name).toBe('test-state');
|
||||
expect(options.version).toBe(2);
|
||||
expect(options.storage).toBeDefined();
|
||||
expect(options.partialize?.({ hydrated: true, transientValue: 'skip' })).toEqual(
|
||||
{
|
||||
hydrated: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
clampVolume,
|
||||
defaultAutosaveEnabled,
|
||||
defaultGameResolutionId,
|
||||
defaultVolumeSettings,
|
||||
formatResolutionLabel,
|
||||
gameResolutionOptions,
|
||||
} from '../src/settings/gameSettings';
|
||||
|
||||
describe('game settings definitions', () => {
|
||||
it('uses landscape phone dimensions for the default enforced resolution', () => {
|
||||
expect(defaultGameResolutionId).toBe('iphoneDefault');
|
||||
expect(formatResolutionLabel(defaultGameResolutionId)).toBe(
|
||||
'Phone Default (844 x 390)',
|
||||
);
|
||||
const boundedOptions = gameResolutionOptions.filter(
|
||||
option => !('unbounded' in option),
|
||||
);
|
||||
|
||||
expect(boundedOptions.map(option => option.width)).toEqual([
|
||||
812,
|
||||
844,
|
||||
915,
|
||||
932,
|
||||
]);
|
||||
expect(boundedOptions.every(option => option.width > option.height)).toBe(true);
|
||||
});
|
||||
|
||||
it('defines a browser resolution that fills available space', () => {
|
||||
expect(formatResolutionLabel('browser')).toBe(
|
||||
'Browser (Fill available space)',
|
||||
);
|
||||
});
|
||||
|
||||
it('clamps volumes between 0 and 100', () => {
|
||||
expect(clampVolume(-5)).toBe(0);
|
||||
expect(clampVolume(55)).toBe(55);
|
||||
expect(clampVolume(105)).toBe(100);
|
||||
});
|
||||
|
||||
it('defines autosave and every default volume setting', () => {
|
||||
expect(defaultAutosaveEnabled).toBe(true);
|
||||
expect(defaultVolumeSettings).toEqual({
|
||||
ambience: 70,
|
||||
master: 80,
|
||||
music: 70,
|
||||
speech: 80,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
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();
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import { act } from 'react-test-renderer';
|
||||
|
||||
import { defaultVolumeSettings } from '../src/settings/gameSettings';
|
||||
import { useGameSettingsStore } from '../src/state';
|
||||
|
||||
describe('useGameSettingsStore', () => {
|
||||
beforeEach(() => {
|
||||
useGameSettingsStore.setState({
|
||||
autosaveEnabled: true,
|
||||
resolutionId: 'iphoneDefault',
|
||||
volumes: defaultVolumeSettings,
|
||||
});
|
||||
});
|
||||
|
||||
it('captures all settings in persisted state', () => {
|
||||
const partialize = useGameSettingsStore.persist.getOptions().partialize;
|
||||
|
||||
expect(partialize?.(useGameSettingsStore.getState())).toEqual({
|
||||
autosaveEnabled: true,
|
||||
resolutionId: 'iphoneDefault',
|
||||
volumes: {
|
||||
ambience: 70,
|
||||
master: 80,
|
||||
music: 70,
|
||||
speech: 80,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('updates autosave, resolution, and volume settings', () => {
|
||||
act(() => {
|
||||
useGameSettingsStore.getState().toggleAutosave();
|
||||
useGameSettingsStore.getState().setResolution('phoneLarge');
|
||||
useGameSettingsStore.getState().setVolume('music', 105);
|
||||
});
|
||||
|
||||
expect(useGameSettingsStore.getState().autosaveEnabled).toBe(false);
|
||||
expect(useGameSettingsStore.getState().resolutionId).toBe('phoneLarge');
|
||||
expect(useGameSettingsStore.getState().volumes.music).toBe(100);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createMemoryStateStorage } from '../src/state/memoryStateStorage';
|
||||
|
||||
describe('createMemoryStateStorage', () => {
|
||||
it('stores, reads, and removes string values', () => {
|
||||
const storage = createMemoryStateStorage();
|
||||
|
||||
expect(storage.getItem('missing')).toBeNull();
|
||||
|
||||
storage.setItem('bone-king', 'awake');
|
||||
expect(storage.getItem('bone-king')).toBe('awake');
|
||||
|
||||
storage.removeItem('bone-king');
|
||||
expect(storage.getItem('bone-king')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
policyRoutes,
|
||||
primaryMenuRoutes,
|
||||
requiredPageRoutes,
|
||||
toolkitRoutes,
|
||||
routeDefinitions,
|
||||
} from '../src/navigation/routes';
|
||||
|
||||
describe('routeDefinitions', () => {
|
||||
it('defines every required page with labels and titles', () => {
|
||||
expect(requiredPageRoutes).toEqual([
|
||||
'home',
|
||||
'newGame',
|
||||
'saves',
|
||||
'settings',
|
||||
'policiesLegal',
|
||||
'policiesPrivacy',
|
||||
'policiesTerms',
|
||||
'policiesCookies',
|
||||
'credits',
|
||||
'toolkit',
|
||||
'toolkitOpeningSequence',
|
||||
'toolkitCredits',
|
||||
'toolkitAudio',
|
||||
'toolkitLore',
|
||||
'toolkitResources',
|
||||
]);
|
||||
|
||||
for (const routeName of requiredPageRoutes) {
|
||||
expect(routeDefinitions[routeName].label.length).toBeGreaterThan(0);
|
||||
expect(routeDefinitions[routeName].title.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('exposes home menu and policy submenu routes', () => {
|
||||
expect(primaryMenuRoutes).toEqual([
|
||||
'newGame',
|
||||
'saves',
|
||||
'settings',
|
||||
'policies',
|
||||
'credits',
|
||||
'toolkit',
|
||||
]);
|
||||
|
||||
expect(policyRoutes).toEqual([
|
||||
'policiesLegal',
|
||||
'policiesPrivacy',
|
||||
'policiesTerms',
|
||||
'policiesCookies',
|
||||
]);
|
||||
|
||||
expect(toolkitRoutes).toEqual([
|
||||
'toolkitOpeningSequence',
|
||||
'toolkitCredits',
|
||||
'toolkitAudio',
|
||||
'toolkitLore',
|
||||
'toolkitResources',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { act } from 'react-test-renderer';
|
||||
|
||||
import { useNavigationStore } from '../src/state';
|
||||
|
||||
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();
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('useNavigationStore', () => {
|
||||
beforeEach(() => {
|
||||
useNavigationStore.setState({
|
||||
currentRouteName: 'home',
|
||||
pendingRouteName: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('captures the current and pending routes in persisted state', () => {
|
||||
act(() => {
|
||||
useNavigationStore.getState().setPendingRoute('settings');
|
||||
});
|
||||
|
||||
const partialize = useNavigationStore.persist.getOptions().partialize;
|
||||
|
||||
expect(partialize?.(useNavigationStore.getState())).toEqual({
|
||||
currentRouteName: 'home',
|
||||
pendingRouteName: 'settings',
|
||||
});
|
||||
});
|
||||
|
||||
it('completes navigation by promoting the pending route to current route', () => {
|
||||
act(() => {
|
||||
useNavigationStore.getState().setPendingRoute('settings');
|
||||
useNavigationStore.getState().completeNavigation('settings');
|
||||
});
|
||||
|
||||
expect(useNavigationStore.getState().currentRouteName).toBe('settings');
|
||||
expect(useNavigationStore.getState().pendingRouteName).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { StateStorage } from 'zustand/middleware';
|
||||
|
||||
import { createWebStateStorage } from '../src/state/persistStorage.web';
|
||||
|
||||
function createThrowingStorage(): StateStorage<void> {
|
||||
return {
|
||||
getItem: () => null,
|
||||
removeItem: () => {
|
||||
throw new Error('Storage is unavailable.');
|
||||
},
|
||||
setItem: () => {
|
||||
throw new Error('Storage is unavailable.');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('createWebStateStorage', () => {
|
||||
it('uses provided storage when it is available', () => {
|
||||
const backingStore = new Map<string, string>();
|
||||
const providedStorage: StateStorage<void> = {
|
||||
getItem: (name: string) => backingStore.get(name) ?? null,
|
||||
removeItem: (name: string) => {
|
||||
backingStore.delete(name);
|
||||
},
|
||||
setItem: (name: string, value: string) => {
|
||||
backingStore.set(name, value);
|
||||
},
|
||||
};
|
||||
|
||||
const storage = createWebStateStorage(() => providedStorage);
|
||||
|
||||
storage.setItem('throne', 'claimed');
|
||||
expect(providedStorage.getItem('throne')).toBe('claimed');
|
||||
expect(storage.getItem('throne')).toBe('claimed');
|
||||
});
|
||||
|
||||
it('falls back to memory storage when local storage is unavailable', () => {
|
||||
const storage = createWebStateStorage(createThrowingStorage);
|
||||
|
||||
storage.setItem('throne', 'hidden');
|
||||
expect(storage.getItem('throne')).toBe('hidden');
|
||||
|
||||
storage.removeItem('throne');
|
||||
expect(storage.getItem('throne')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,426 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { act } from 'react-test-renderer';
|
||||
|
||||
import { useToolkitSocketStore } from '../src/state/toolkitSocketStore';
|
||||
|
||||
describe('useToolkitSocketStore', () => {
|
||||
beforeEach(() => {
|
||||
useToolkitSocketStore.setState({
|
||||
lastConnectedAt: null,
|
||||
socketId: null,
|
||||
status: 'disconnected',
|
||||
});
|
||||
});
|
||||
|
||||
it('tracks connecting, connected, and disconnected socket state', () => {
|
||||
act(() => {
|
||||
useToolkitSocketStore.getState().setConnecting();
|
||||
});
|
||||
|
||||
expect(useToolkitSocketStore.getState()).toMatchObject({
|
||||
socketId: null,
|
||||
status: 'connecting',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
useToolkitSocketStore.getState().setConnected('socket-1');
|
||||
});
|
||||
|
||||
expect(useToolkitSocketStore.getState()).toMatchObject({
|
||||
socketId: 'socket-1',
|
||||
status: 'connected',
|
||||
});
|
||||
expect(useToolkitSocketStore.getState().lastConnectedAt).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
useToolkitSocketStore.getState().setDisconnected();
|
||||
});
|
||||
|
||||
expect(useToolkitSocketStore.getState()).toMatchObject({
|
||||
socketId: null,
|
||||
status: 'disconnected',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user