This commit is contained in:
2026-04-19 20:33:18 +08:00
parent 692643136f
commit 67c584b4df
123 changed files with 11898 additions and 4082 deletions

View File

@@ -0,0 +1,162 @@
/* @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 { useGamePersistence } from './useGamePersistence';
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/storageService', () => ({
getSettings: storageMocks.getSettings,
putSettings: storageMocks.putSettings,
getSaveSnapshot: storageMocks.getSaveSnapshot,
putSaveSnapshot: storageMocks.putSaveSnapshot,
deleteSaveSnapshot: storageMocks.deleteSaveSnapshot,
}));
vi.mock('./story/runtimeStoryCoordinator', () => ({
resumeServerRuntimeStory: vi.fn(),
}));
function SettingsHarness({ authenticatedUserId }: { authenticatedUserId: string | null }) {
const settings = useGameSettings(authenticatedUserId);
return (
<div>
<div data-testid="music-volume">{settings.musicVolume.toFixed(2)}</div>
<button
type="button"
onClick={() => {
settings.setMusicVolume(0.6);
}}
>
</button>
</div>
);
}
function PersistenceHarness({
authenticatedUserId,
}: {
authenticatedUserId: string | null;
}) {
const persistence = useGamePersistence({
authenticatedUserId,
gameState: {} as GameState,
bottomTab: 'adventure' as BottomTab,
currentStory: null as StoryMoment | null,
isLoading: false,
setGameState: () => {},
setBottomTab: () => {},
hydrateStoryState: () => {},
resetStoryState: () => {},
});
return (
<div>
<div data-testid="saved-game">{persistence.hasSavedGame ? 'yes' : 'no'}</div>
<div data-testid="hydrating">
{persistence.isHydratingSnapshot ? 'yes' : 'no'}
</div>
</div>
);
}
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(<SettingsHarness authenticatedUserId={null} />);
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(<SettingsHarness authenticatedUserId="user-1" />);
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(<PersistenceHarness authenticatedUserId={null} />);
await waitFor(() => {
expect(screen.getByTestId('hydrating').textContent).toBe('no');
});
expect(screen.getByTestId('saved-game').textContent).toBe('no');
expect(storageMocks.getSaveSnapshot).not.toHaveBeenCalled();
});