1
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user