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

@@ -17,15 +17,21 @@ import type { AuthUser } from '../../services/authService';
import {
clearProfileBrowseHistory,
deleteCustomWorldProfile,
getCustomWorldGalleryDetail,
getProfileDashboard,
listCustomWorldGallery,
listCustomWorldLibrary,
listProfileBrowseHistory,
listProfileSaveArchives,
resumeProfileSaveArchive,
upsertCustomWorldProfile,
upsertProfileBrowseHistory,
} from '../../services/storageService';
import type { GameState } from '../../types';
import { AuthUiContext } from '../auth/AuthUiContext';
import {
type PlatformSettingsSection,
AuthUiContext,
} from '../auth/AuthUiContext';
import {
PreGameSelectionFlow,
type SelectionStage,
@@ -48,7 +54,9 @@ vi.mock('../../services/storageService', () => ({
listCustomWorldGallery: vi.fn(),
listCustomWorldLibrary: vi.fn(),
listProfileBrowseHistory: vi.fn(),
listProfileSaveArchives: vi.fn(),
publishCustomWorldProfile: vi.fn(),
resumeProfileSaveArchive: vi.fn(),
syncProfileBrowseHistory: vi.fn(),
unpublishCustomWorldProfile: vi.fn(),
upsertProfileBrowseHistory: vi.fn(),
@@ -179,7 +187,32 @@ const mockAuthUser: AuthUser = {
wechatBound: false,
};
function TestWrapper({ withAuth = false }: { withAuth?: boolean } = {}) {
type TestAuthValue = {
user: AuthUser | null;
openLoginModal: (postLoginAction?: (() => void) | null) => void;
requireAuth: (action: () => void) => void;
openSettingsModal: (section?: PlatformSettingsSection) => void;
openAccountModal: () => void;
logout: () => Promise<void>;
setGlobalAccountActionsVisible: (visible: boolean) => void;
musicVolume: number;
setMusicVolume: (value: number) => void;
platformTheme: 'light' | 'dark';
setPlatformTheme: (theme: 'light' | 'dark') => void;
isHydratingSettings: boolean;
isPersistingSettings: boolean;
settingsError: string | null;
};
function TestWrapper({
withAuth = false,
authValue,
onContinueGame,
}: {
withAuth?: boolean;
authValue?: TestAuthValue;
onContinueGame?: (snapshot?: unknown) => void;
} = {}) {
const [selectionStage, setSelectionStage] =
useState<SelectionStage>('platform');
@@ -190,24 +223,36 @@ function TestWrapper({ withAuth = false }: { withAuth?: boolean } = {}) {
gameState={{} as GameState}
hasSavedGame={false}
savedSnapshot={null}
handleContinueGame={() => {}}
handleContinueGame={onContinueGame ?? (() => {})}
handleStartNewGame={() => {}}
handleCustomWorldSelect={() => {}}
/>
);
if (!withAuth) {
if (!withAuth && !authValue) {
return content;
}
return (
<AuthUiContext.Provider
value={{
user: mockAuthUser,
openAccountModal: () => {},
logout: async () => {},
setGlobalAccountActionsVisible: () => {},
}}
value={
authValue ?? {
user: mockAuthUser,
openLoginModal: () => {},
requireAuth: (action) => action(),
openSettingsModal: () => {},
openAccountModal: () => {},
logout: async () => {},
setGlobalAccountActionsVisible: () => {},
musicVolume: 0.42,
setMusicVolume: () => {},
platformTheme: 'light',
setPlatformTheme: () => {},
isHydratingSettings: false,
isPersistingSettings: false,
settingsError: null,
}
}
>
{content}
</AuthUiContext.Provider>
@@ -228,6 +273,27 @@ beforeEach(() => {
vi.mocked(listCustomWorldLibrary).mockResolvedValue([]);
vi.mocked(listCustomWorldGallery).mockResolvedValue([]);
vi.mocked(listProfileBrowseHistory).mockResolvedValue([]);
vi.mocked(listProfileSaveArchives).mockResolvedValue([]);
vi.mocked(resumeProfileSaveArchive).mockResolvedValue({
entry: {
worldKey: 'custom:world-archive-1',
ownerUserId: null,
profileId: 'world-archive-1',
worldType: 'CUSTOM',
worldName: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summaryText: '回到旧灯塔继续推进调查。',
coverImageSrc: null,
lastPlayedAt: '2026-04-19T12:00:00.000Z',
},
snapshot: {
version: 2,
savedAt: '2026-04-19T12:00:00.000Z',
bottomTab: 'adventure',
currentStory: null,
gameState: {} as GameState,
},
});
vi.mocked(upsertProfileBrowseHistory).mockResolvedValue([]);
vi.mocked(clearProfileBrowseHistory).mockResolvedValue([]);
vi.mocked(deleteCustomWorldProfile).mockResolvedValue([]);
@@ -309,6 +375,75 @@ test('create tab opens game type modal, keeps AIRP and visual novel locked, and
).toBeTruthy();
});
test('clicking a public work while logged out routes through requireAuth', async () => {
const user = userEvent.setup();
const requireAuth = vi.fn();
vi.mocked(listCustomWorldGallery).mockResolvedValue([
{
ownerUserId: 'author-1',
profileId: 'world-public-1',
visibility: 'published',
publishedAt: '2026-04-16T12:00:00.000Z',
updatedAt: '2026-04-16T12:00:00.000Z',
worldName: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summaryText: '最近公开发布的世界。',
coverImageSrc: null,
themeMode: 'tide',
authorDisplayName: '潮汐作者',
playableNpcCount: 3,
landmarkCount: 4,
},
]);
render(
<TestWrapper
authValue={{
user: null,
openLoginModal: () => {},
requireAuth,
openAccountModal: () => {},
logout: async () => {},
setGlobalAccountActionsVisible: () => {},
}}
/>,
);
const workCards = await screen.findAllByRole('button', {
name: //u,
});
await user.click(workCards[0]!);
expect(requireAuth).toHaveBeenCalledTimes(1);
expect(getCustomWorldGalleryDetail).not.toHaveBeenCalled();
});
test('selecting RPG creation while logged out routes through requireAuth', async () => {
const user = userEvent.setup();
const requireAuth = vi.fn();
render(
<TestWrapper
authValue={{
user: null,
openLoginModal: () => {},
requireAuth,
openAccountModal: () => {},
logout: async () => {},
setGlobalAccountActionsVisible: () => {},
}}
/>,
);
await user.click(screen.getByRole('button', { name: '创作' }));
await user.click(screen.getByRole('button', { name: //u }));
await user.click(screen.getByRole('button', { name: / RPG/u }));
expect(requireAuth).toHaveBeenCalledTimes(1);
expect(createCustomWorldAgentSession).not.toHaveBeenCalled();
});
test('starting draft generation leaves the agent workspace and shows the generation progress view', async () => {
const user = userEvent.setup();
@@ -472,40 +607,78 @@ test('existing draft sessions enter the legacy result layout directly', async ()
expect(screen.getByText('技能')).toBeTruthy();
});
test('profile tab loads server browse history and can clear it after confirmation', async () => {
test('authenticated users with save archives default into the saves tab', async () => {
const user = userEvent.setup();
vi.mocked(listProfileBrowseHistory).mockResolvedValue([
vi.mocked(listProfileSaveArchives).mockResolvedValue([
{
ownerUserId: 'author-1',
worldKey: 'custom:world-1',
ownerUserId: null,
profileId: 'world-1',
worldType: 'CUSTOM',
worldName: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summaryText: '最近浏览过的公开作品。',
summaryText: '回到旧灯塔继续推进调查。',
coverImageSrc: null,
themeMode: 'tide',
authorDisplayName: '潮汐作者',
visitedAt: '2026-04-16T12:00:00.000Z',
lastPlayedAt: '2026-04-19T12:00:00.000Z',
},
]);
vi.mocked(clearProfileBrowseHistory).mockResolvedValue([]);
vi.spyOn(window, 'confirm').mockReturnValue(true);
render(<TestWrapper withAuth />);
await user.click(screen.getByRole('button', { name: '我的' }));
expect(await screen.findByText('全部存档')).toBeTruthy();
expect(await screen.findByText('潮雾列岛')).toBeTruthy();
expect(screen.getByText('最近更新时间排序')).toBeTruthy();
});
await user.click(screen.getByRole('button', { name: '清空' }));
test('save tab can resume a selected archive directly into the game', async () => {
const user = userEvent.setup();
const handleContinueGame = vi.fn();
await waitFor(() => {
expect(clearProfileBrowseHistory).toHaveBeenCalledTimes(1);
vi.mocked(listProfileSaveArchives).mockResolvedValue([
{
worldKey: 'custom:world-1',
ownerUserId: null,
profileId: 'world-1',
worldType: 'CUSTOM',
worldName: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summaryText: '回到旧灯塔继续推进调查。',
coverImageSrc: null,
lastPlayedAt: '2026-04-19T12:00:00.000Z',
},
]);
vi.mocked(resumeProfileSaveArchive).mockResolvedValue({
entry: {
worldKey: 'custom:world-1',
ownerUserId: null,
profileId: 'world-1',
worldType: 'CUSTOM',
worldName: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summaryText: '回到旧灯塔继续推进调查。',
coverImageSrc: null,
lastPlayedAt: '2026-04-19T12:00:00.000Z',
},
snapshot: {
version: 2,
savedAt: '2026-04-19T12:00:00.000Z',
bottomTab: 'adventure',
currentStory: null,
gameState: {
worldType: 'CUSTOM',
} as GameState,
},
});
expect(
screen.getByText('你最近还没有浏览过作品详情,去首页逛一逛吧。'),
).toBeTruthy();
render(<TestWrapper withAuth onContinueGame={handleContinueGame} />);
await user.click(await screen.findByRole('button', { name: //u }));
await waitFor(() => {
expect(resumeProfileSaveArchive).toHaveBeenCalledWith('custom:world-1');
expect(handleContinueGame).toHaveBeenCalledTimes(1);
});
});
test('owned world detail can delete a work and return to the create tab list', async () => {
@@ -544,10 +717,10 @@ test('owned world detail can delete a work and return to the create tab list', a
]);
vi.mocked(deleteCustomWorldProfile).mockResolvedValue([]);
render(<TestWrapper />);
render(<TestWrapper withAuth />);
await user.click(screen.getByRole('button', { name: '创作' }));
await user.click(await screen.findByText('潮雾列岛'));
await user.click(await screen.findByRole('button', { name: //u }));
await user.click(await screen.findByRole('button', { name: '删除作品' }));
await waitFor(() => {