/* @vitest-environment jsdom */ import { act, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, expect, test, vi } from 'vitest'; import { readSavedSettings } from '../persistence/gameSettingsStorage'; import type { GameState, StoryMoment } from '../types'; import type { BottomTab } from '../types/navigation'; import { useRpgSessionPersistence } from './rpg-session'; import { useGameSettings } from './useGameSettings'; const storageMocks = vi.hoisted(() => ({ getSettings: vi.fn(), putSettings: vi.fn(), getSaveSnapshot: vi.fn(), putSaveSnapshot: vi.fn(), deleteSaveSnapshot: vi.fn(), })); vi.mock('../services/rpg-entry', () => ({ getRpgProfileSettings: storageMocks.getSettings, putRpgProfileSettings: storageMocks.putSettings, })); vi.mock('../services/rpg-runtime', () => ({ rpgSnapshotClient: { getSnapshot: storageMocks.getSaveSnapshot, putSnapshot: storageMocks.putSaveSnapshot, deleteSnapshot: storageMocks.deleteSaveSnapshot, }, })); function SettingsHarness({ authenticatedUserId }: { authenticatedUserId: string | null }) { const settings = useGameSettings(authenticatedUserId); return (
{settings.musicVolume.toFixed(2)}
); } function PersistenceHarness({ authenticatedUserId, }: { authenticatedUserId: string | null; }) { const persistence = useRpgSessionPersistence({ authenticatedUserId, gameState: {} as GameState, bottomTab: 'adventure' as BottomTab, currentStory: null as StoryMoment | null, isLoading: false, setGameState: () => {}, setBottomTab: () => {}, hydrateStoryState: () => {}, resetStoryState: () => {}, }); return (
{persistence.hasSavedGame ? 'yes' : 'no'}
{persistence.isHydratingSnapshot ? 'yes' : 'no'}
); } beforeEach(() => { vi.clearAllMocks(); vi.useRealTimers(); window.localStorage.clear(); storageMocks.getSettings.mockResolvedValue({ musicVolume: 0.42, platformTheme: 'light', }); storageMocks.putSettings.mockResolvedValue({ musicVolume: 0.6, platformTheme: 'light', }); storageMocks.getSaveSnapshot.mockResolvedValue(null); storageMocks.putSaveSnapshot.mockResolvedValue(null); storageMocks.deleteSaveSnapshot.mockResolvedValue({ ok: true, }); }); test('unauthenticated settings use local cache and skip remote runtime settings requests', async () => { window.localStorage.setItem( 'tavernrealms.settings.v1', JSON.stringify({ version: 1, musicVolume: 0.33, platformTheme: 'dark', }), ); render(); expect(screen.getByTestId('music-volume').textContent).toBe('0.33'); expect(storageMocks.getSettings).not.toHaveBeenCalled(); act(() => { screen.getByRole('button', { name: '设置音量' }).click(); }); expect(storageMocks.putSettings).not.toHaveBeenCalled(); expect(readSavedSettings().musicVolume).toBeCloseTo(0.6); }); test('authenticated settings hydrate from remote settings and sync later changes back to the server', async () => { storageMocks.getSettings.mockResolvedValue({ musicVolume: 0.8, platformTheme: 'dark', }); render(); await waitFor(() => { expect(storageMocks.getSettings).toHaveBeenCalledTimes(1); }); expect(screen.getByTestId('music-volume').textContent).toBe('0.80'); vi.useFakeTimers(); act(() => { screen.getByRole('button', { name: '设置音量' }).click(); }); await act(async () => { vi.advanceTimersByTime(200); await Promise.resolve(); }); expect(storageMocks.putSettings).toHaveBeenCalledTimes(1); expect(storageMocks.putSettings).toHaveBeenCalledWith( { musicVolume: 0.6, platformTheme: 'dark' }, expect.objectContaining({ signal: expect.any(AbortSignal), }), ); expect(readSavedSettings().musicVolume).toBeCloseTo(0.6); }); test('unauthenticated runtime skips remote snapshot hydration', async () => { render(); await waitFor(() => { expect(screen.getByTestId('hydrating').textContent).toBe('no'); }); expect(screen.getByTestId('saved-game').textContent).toBe('no'); expect(storageMocks.getSaveSnapshot).not.toHaveBeenCalled(); });