implement gameboard v1

This commit is contained in:
seavor 2026-04-19 23:21:42 -05:00
parent b103db681b
commit 0d7336edc2
177 changed files with 16995 additions and 139 deletions

View file

@ -1,4 +1,10 @@
export { withMockLocation } from './globalGuards';
export { renderWithProviders } from './renderWithProviders';
export { createMockWebClient } from './mockWebClient';
export { disconnectedState, connectedState, connectedWithRoomsState, makeUser } from './storeFixtures';
export {
disconnectedState,
connectedState,
connectedWithRoomsState,
makeStoreState,
makeUser,
} from './storeFixtures';

View file

@ -34,10 +34,43 @@ export function createMockWebClient() {
leaveRoom: vi.fn(),
roomSay: vi.fn(),
createGame: vi.fn(),
joinGame: vi.fn(),
},
game: {
joinGame: vi.fn(),
leaveGame: vi.fn(),
kickFromGame: vi.fn(),
gameSay: vi.fn(),
readyStart: vi.fn(),
concede: vi.fn(),
unconcede: vi.fn(),
judge: vi.fn(),
nextTurn: vi.fn(),
setActivePhase: vi.fn(),
reverseTurn: vi.fn(),
moveCard: vi.fn(),
flipCard: vi.fn(),
attachCard: vi.fn(),
createToken: vi.fn(),
setCardAttr: vi.fn(),
setCardCounter: vi.fn(),
incCardCounter: vi.fn(),
drawCards: vi.fn(),
undoDraw: vi.fn(),
createArrow: vi.fn(),
deleteArrow: vi.fn(),
createCounter: vi.fn(),
setCounter: vi.fn(),
incCounter: vi.fn(),
delCounter: vi.fn(),
shuffle: vi.fn(),
dumpZone: vi.fn(),
revealCards: vi.fn(),
changeZoneProperties: vi.fn(),
deckSelect: vi.fn(),
setSideboardPlan: vi.fn(),
setSideboardLock: vi.fn(),
mulligan: vi.fn(),
rollDie: vi.fn(),
},
admin: {
adjustMod: vi.fn(),

View file

@ -6,13 +6,25 @@ import { MemoryRouter } from 'react-router-dom';
import { I18nextProvider } from 'react-i18next';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { DndContext } from '@dnd-kit/core';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { gamesReducer } from '../store/game';
import { roomsReducer } from '../store/rooms';
import { serverReducer } from '../store/server';
import { actionReducer } from '../store/actions';
// Disables MUI's ripple animation in tests. The ripple fires a deferred
// state update after clicks/focus that would otherwise trigger a noisy
// "update to ForwardRef(TouchRipple) was not wrapped in act(...)" warning.
const testTheme = createTheme({
components: {
MuiButtonBase: { defaultProps: { disableRipple: true } },
},
});
import { WebClientContext } from '../hooks/useWebClient';
import type { WebClient } from '../websocket';
import rootReducer from '../store/rootReducer';
import { ToastProvider } from '../components/Toast/ToastContext';
import { storeMiddlewareOptions } from '../store/store';
import type { RootState } from '../store/store';
import { createMockWebClient } from './mockWebClient';
// Non-empty `resources` registers en-US so `resolvedLanguage` is defined;
// without it MUI warns about out-of-range Select values.
@ -24,15 +36,18 @@ testI18n.use(initReactI18next).init({
interpolation: { escapeValue: false },
});
// `configureStore`'s `preloadedState` wants `PreloadedState<CombinedState<…>>`
// which narrows collection types past our slice interfaces. A single cast
// here keeps the test harness loose (each test injects only the slices it
// cares about) while specs themselves stay strict via `makeStoreState`.
function createTestStore(preloadedState?: Partial<RootState>) {
return configureStore({
reducer: {
games: gamesReducer,
rooms: roomsReducer,
server: serverReducer,
action: actionReducer,
},
preloadedState: preloadedState as any,
reducer: rootReducer,
preloadedState: preloadedState as Parameters<typeof configureStore>[0]['preloadedState'],
// Share the production middleware config so the serializableCheck
// tolerates protobuf messages (isMessage) the same way the real store
// does — otherwise every proto-payload dispatch in tests spams stderr.
middleware: (getDefaultMiddleware) => getDefaultMiddleware(storeMiddlewareOptions),
});
}
@ -40,6 +55,7 @@ interface ExtendedRenderOptions extends Omit<RenderOptions, 'wrapper'> {
preloadedState?: Partial<RootState>;
store?: EnhancedStore;
route?: string;
webClient?: WebClient;
}
export function renderWithProviders(
@ -48,6 +64,7 @@ export function renderWithProviders(
preloadedState,
store = createTestStore(preloadedState),
route = '/',
webClient = createMockWebClient(),
...renderOptions
}: ExtendedRenderOptions = {},
) {
@ -55,11 +72,21 @@ export function renderWithProviders(
return (
<Provider store={store}>
<I18nextProvider i18n={testI18n}>
<ToastProvider>
<MemoryRouter initialEntries={[route]}>
{children}
</MemoryRouter>
</ToastProvider>
<ThemeProvider theme={testTheme}>
<ToastProvider>
<MemoryRouter initialEntries={[route]}>
<WebClientContext value={webClient}>
<DndContext
accessibility={{
screenReaderInstructions: { draggable: '' },
}}
>
{children}
</DndContext>
</WebClientContext>
</MemoryRouter>
</ToastProvider>
</ThemeProvider>
</I18nextProvider>
</Provider>
);
@ -67,6 +94,7 @@ export function renderWithProviders(
return {
store,
webClient,
...render(ui, { wrapper: Wrapper, ...renderOptions }),
};
}

View file

@ -63,6 +63,8 @@ export const disconnectedState: Partial<RootState> = {
messages: {},
sortGamesBy: { field: App.GameSortField.START_TIME, order: App.SortDirection.DESC },
sortUsersBy: { field: App.UserSortField.NAME, order: App.SortDirection.ASC },
selectedGameIds: {},
gameFilters: {},
},
games: { games: {} },
action: { type: null, payload: null, meta: null, error: false, count: 0 },
@ -122,3 +124,27 @@ export const connectedWithRoomsState: Partial<RootState> = {
};
export { makeUser };
/**
* Deep-partial of a root state. Let specs pass partial slice shapes
* (typically just `games: { games: { ... } }`) without the ~60 fields of
* server/rooms that the test doesn't care about.
*/
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
/**
* Wraps a partial root-state literal with a safe single `as`-cast so specs
* don't need to sprinkle `as any` on every `preloadedState` argument. The
* runtime value is the exact same literal; the only thing this helper buys
* is deleting the `as any` cast from call sites.
*
* @example
* renderWithProviders(<MyComponent />, {
* preloadedState: makeStoreState({
* games: { games: { 1: makeGameEntry({ ... }) } },
* }),
* });
*/
export function makeStoreState(partial: DeepPartial<RootState>): Partial<RootState> {
return partial as Partial<RootState>;
}