Merge remote-tracking branch 'origin/master' into codex/bark-battle
This commit is contained in:
@@ -36,6 +36,7 @@ import type {
|
||||
CustomWorldLibraryEntry,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import { normalizeCustomWorldProfileRecord } from '../../data/customWorldLibrary';
|
||||
import {
|
||||
readPublicWorkCodeFromLocationSearch,
|
||||
resolveSelectionStageFromPath,
|
||||
@@ -276,6 +277,17 @@ const testCreationEntryConfig = {
|
||||
description: '先选玩法类型,再进入对应创作工作台。',
|
||||
},
|
||||
creationTypes: [
|
||||
{
|
||||
id: 'rpg',
|
||||
title: '文字冒险',
|
||||
subtitle: '经典 RPG 体验',
|
||||
badge: '可创建',
|
||||
imageSrc: '/creation-type-references/rpg.webp',
|
||||
visible: true,
|
||||
open: true,
|
||||
sortOrder: 10,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
{
|
||||
id: 'puzzle',
|
||||
title: '拼图',
|
||||
@@ -1993,6 +2005,14 @@ const compiledAgentDraftSession: CustomWorldAgentSessionSnapshot = {
|
||||
},
|
||||
};
|
||||
|
||||
const compiledAgentResultPreview = normalizeCustomWorldProfileRecord(
|
||||
compiledAgentDraftSession.resultPreview?.preview,
|
||||
);
|
||||
|
||||
if (!compiledAgentResultPreview) {
|
||||
throw new Error('failed to normalize compiled agent result preview');
|
||||
}
|
||||
|
||||
function buildResultViewForSession(
|
||||
session: CustomWorldAgentSessionSnapshot,
|
||||
): RpgCreationResultView {
|
||||
@@ -3336,8 +3356,8 @@ test('create tab shows template tabs and embeds puzzle form by default', async (
|
||||
screen.getByRole('tab', { name: '拼图' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/puzzle.webp');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: 'AIRP' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/airp.webp');
|
||||
screen.getByRole('tab', { name: '文字冒险' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/rpg.webp');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '抓大鹅' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/match3d.webp');
|
||||
@@ -8299,6 +8319,288 @@ test('agent draft result test button enters current draft without publish gate',
|
||||
).toBe(false);
|
||||
}, 10_000);
|
||||
|
||||
test('agent draft result test button enters the opened draft profile instead of a previous session', async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleCustomWorldSelect = vi.fn();
|
||||
|
||||
const previousDraftSession = {
|
||||
...compiledAgentDraftSession,
|
||||
sessionId: 'custom-world-agent-session-1',
|
||||
resultPreview: {
|
||||
...compiledAgentDraftSession.resultPreview!,
|
||||
publishReady: false,
|
||||
canEnterWorld: true,
|
||||
preview: {
|
||||
...compiledAgentResultPreview,
|
||||
id: 'agent-draft-custom-world-agent-session-1',
|
||||
name: '潮雾列岛',
|
||||
summary: '上一份草稿内容,不能被本次启动复用。',
|
||||
playableNpcs: [
|
||||
{
|
||||
...compiledAgentResultPreview.playableNpcs[0]!,
|
||||
id: 'playable-previous-1',
|
||||
name: '沈砺',
|
||||
},
|
||||
],
|
||||
sessionId: 'custom-world-agent-session-1',
|
||||
},
|
||||
},
|
||||
} satisfies CustomWorldAgentSessionSnapshot;
|
||||
const openedDraftSession = {
|
||||
...compiledAgentDraftSession,
|
||||
sessionId: 'custom-world-agent-session-2',
|
||||
resultPreview: {
|
||||
...compiledAgentDraftSession.resultPreview!,
|
||||
publishReady: false,
|
||||
canEnterWorld: true,
|
||||
preview: {
|
||||
...compiledAgentResultPreview,
|
||||
id: 'agent-draft-custom-world-agent-session-2',
|
||||
name: '星砂废都',
|
||||
subtitle: '坠星沙海与废都钟楼',
|
||||
summary: '本次从草稿架打开的目标草稿内容。',
|
||||
playerGoal: '找到废都钟楼下被星砂掩埋的旧约。',
|
||||
playableNpcs: [
|
||||
{
|
||||
...compiledAgentResultPreview.playableNpcs[0]!,
|
||||
id: 'playable-opened-1',
|
||||
name: '砂眠',
|
||||
title: '废都引路人',
|
||||
},
|
||||
],
|
||||
storyNpcs: [],
|
||||
landmarks: [
|
||||
{
|
||||
...compiledAgentResultPreview.landmarks[0]!,
|
||||
id: 'landmark-opened-1',
|
||||
name: '坠星钟楼',
|
||||
},
|
||||
],
|
||||
sessionId: 'custom-world-agent-session-2',
|
||||
},
|
||||
},
|
||||
} satisfies CustomWorldAgentSessionSnapshot;
|
||||
const sessionsById = new Map([
|
||||
[previousDraftSession.sessionId, previousDraftSession],
|
||||
[openedDraftSession.sessionId, openedDraftSession],
|
||||
]);
|
||||
|
||||
vi.mocked(getRpgCreationOperation).mockResolvedValue({
|
||||
operationId: 'operation-draft-foundation-1',
|
||||
type: 'draft_foundation',
|
||||
status: 'completed',
|
||||
phaseLabel: '世界底稿已生成',
|
||||
phaseDetail: '第一版世界底稿和草稿卡已经整理完成。',
|
||||
progress: 100,
|
||||
error: null,
|
||||
});
|
||||
vi.mocked(getRpgCreationSession).mockImplementation(async (sessionId) => {
|
||||
const session = sessionsById.get(sessionId);
|
||||
if (!session) {
|
||||
throw new Error(`Missing test session: ${sessionId}`);
|
||||
}
|
||||
return session;
|
||||
});
|
||||
vi.mocked(getRpgCreationResultView).mockImplementation(async (sessionId) => {
|
||||
const session = sessionsById.get(sessionId);
|
||||
if (!session) {
|
||||
throw new Error(`Missing test result view: ${sessionId}`);
|
||||
}
|
||||
return buildResultViewForSession(session);
|
||||
});
|
||||
vi.mocked(listRpgCreationWorks).mockResolvedValue([
|
||||
buildExistingRpgDraftWork({
|
||||
workId: 'draft:custom-world-agent-session-1',
|
||||
title: '潮雾列岛',
|
||||
summary: '上一份草稿内容,不能被本次启动复用。',
|
||||
sessionId: 'custom-world-agent-session-1',
|
||||
playableNpcCount: 1,
|
||||
landmarkCount: 1,
|
||||
}),
|
||||
buildExistingRpgDraftWork({
|
||||
workId: 'draft:custom-world-agent-session-2',
|
||||
title: '星砂废都',
|
||||
subtitle: '待完善草稿',
|
||||
summary: '本次从草稿架打开的目标草稿内容。',
|
||||
sessionId: 'custom-world-agent-session-2',
|
||||
playableNpcCount: 1,
|
||||
landmarkCount: 1,
|
||||
}),
|
||||
]);
|
||||
|
||||
render(<TestWrapper withAuth onSelectWorld={handleCustomWorldSelect} />);
|
||||
|
||||
await openDraftHub(user);
|
||||
const draftPanel = getPlatformTabPanel('saves');
|
||||
await user.click(
|
||||
await within(draftPanel).findByRole('button', {
|
||||
name: /继续完善《星砂废都》/u,
|
||||
}),
|
||||
);
|
||||
expect(await screen.findByText('世界档案', {}, { timeout: 5000 })).toBeTruthy();
|
||||
expect(screen.getByText('星砂废都')).toBeTruthy();
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '作品测试' }, { timeout: 5000 }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleCustomWorldSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'agent-draft-custom-world-agent-session-2',
|
||||
name: '星砂废都',
|
||||
summary: '本次从草稿架打开的目标草稿内容。',
|
||||
playableNpcs: [
|
||||
expect.objectContaining({
|
||||
id: 'playable-opened-1',
|
||||
name: '砂眠',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
mode: 'play',
|
||||
disablePersistence: true,
|
||||
returnStage: 'custom-world-result',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(
|
||||
vi
|
||||
.mocked(executeRpgCreationAction)
|
||||
.mock.calls.some(([, payload]) => payload?.action === 'publish_world'),
|
||||
).toBe(false);
|
||||
}, 10_000);
|
||||
|
||||
test('agent draft result start button enters the opened published draft profile instead of a previous session', async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleCustomWorldSelect = vi.fn();
|
||||
|
||||
const previousDraftSession = {
|
||||
...compiledAgentDraftSession,
|
||||
sessionId: 'custom-world-agent-session-1',
|
||||
stage: 'published',
|
||||
resultPreview: {
|
||||
...compiledAgentDraftSession.resultPreview!,
|
||||
publishReady: true,
|
||||
canEnterWorld: true,
|
||||
preview: {
|
||||
...compiledAgentResultPreview,
|
||||
id: 'agent-draft-custom-world-agent-session-1',
|
||||
name: '潮雾列岛',
|
||||
summary: '上一份已发布草稿内容,不能被本次启动复用。',
|
||||
sessionId: 'custom-world-agent-session-1',
|
||||
},
|
||||
},
|
||||
} satisfies CustomWorldAgentSessionSnapshot;
|
||||
const openedPublishedDraftSession = {
|
||||
...compiledAgentDraftSession,
|
||||
sessionId: 'custom-world-agent-session-2',
|
||||
stage: 'published',
|
||||
resultPreview: {
|
||||
...compiledAgentDraftSession.resultPreview!,
|
||||
publishReady: true,
|
||||
canEnterWorld: true,
|
||||
preview: {
|
||||
...compiledAgentResultPreview,
|
||||
id: 'agent-draft-custom-world-agent-session-2',
|
||||
name: '星砂废都',
|
||||
subtitle: '坠星沙海与废都钟楼',
|
||||
summary: '本次从草稿架打开且已发布的目标草稿内容。',
|
||||
playableNpcs: [
|
||||
{
|
||||
...compiledAgentResultPreview.playableNpcs[0]!,
|
||||
id: 'playable-opened-1',
|
||||
name: '砂眠',
|
||||
title: '废都引路人',
|
||||
},
|
||||
],
|
||||
sessionId: 'custom-world-agent-session-2',
|
||||
},
|
||||
},
|
||||
} satisfies CustomWorldAgentSessionSnapshot;
|
||||
const sessionsById = new Map([
|
||||
[previousDraftSession.sessionId, previousDraftSession],
|
||||
[openedPublishedDraftSession.sessionId, openedPublishedDraftSession],
|
||||
]);
|
||||
|
||||
vi.mocked(getRpgCreationSession).mockImplementation(async (sessionId) => {
|
||||
const session = sessionsById.get(sessionId);
|
||||
if (!session) {
|
||||
throw new Error(`Missing test session: ${sessionId}`);
|
||||
}
|
||||
return session;
|
||||
});
|
||||
vi.mocked(getRpgCreationResultView).mockImplementation(async (sessionId) => {
|
||||
const session = sessionsById.get(sessionId);
|
||||
if (!session) {
|
||||
throw new Error(`Missing test result view: ${sessionId}`);
|
||||
}
|
||||
return buildResultViewForSession(session);
|
||||
});
|
||||
vi.mocked(listRpgCreationWorks).mockResolvedValue([
|
||||
buildExistingRpgDraftWork({
|
||||
workId: 'draft:custom-world-agent-session-1',
|
||||
title: '潮雾列岛',
|
||||
summary: '上一份已发布草稿内容,不能被本次启动复用。',
|
||||
stage: 'published',
|
||||
stageLabel: '已发布',
|
||||
sessionId: 'custom-world-agent-session-1',
|
||||
playableNpcCount: 1,
|
||||
landmarkCount: 1,
|
||||
canEnterWorld: true,
|
||||
}),
|
||||
buildExistingRpgDraftWork({
|
||||
workId: 'draft:custom-world-agent-session-2',
|
||||
title: '星砂废都',
|
||||
subtitle: '已发布草稿',
|
||||
summary: '本次从草稿架打开且已发布的目标草稿内容。',
|
||||
stage: 'published',
|
||||
stageLabel: '已发布',
|
||||
sessionId: 'custom-world-agent-session-2',
|
||||
playableNpcCount: 1,
|
||||
landmarkCount: 1,
|
||||
canEnterWorld: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
render(<TestWrapper withAuth onSelectWorld={handleCustomWorldSelect} />);
|
||||
|
||||
await openDraftHub(user);
|
||||
const draftPanel = getPlatformTabPanel('saves');
|
||||
await user.click(
|
||||
await within(draftPanel).findByRole('button', {
|
||||
name: /继续完善《星砂废都》/u,
|
||||
}),
|
||||
);
|
||||
expect(await screen.findByText('世界档案', {}, { timeout: 5000 })).toBeTruthy();
|
||||
expect(screen.getByText('星砂废都')).toBeTruthy();
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '进入世界' }, { timeout: 5000 }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleCustomWorldSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'agent-draft-custom-world-agent-session-2',
|
||||
name: '星砂废都',
|
||||
summary: '本次从草稿架打开且已发布的目标草稿内容。',
|
||||
playableNpcs: [
|
||||
expect.objectContaining({
|
||||
id: 'playable-opened-1',
|
||||
name: '砂眠',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(
|
||||
vi
|
||||
.mocked(executeRpgCreationAction)
|
||||
.mock.calls.some(([, payload]) => payload?.action === 'publish_world'),
|
||||
).toBe(false);
|
||||
}, 10_000);
|
||||
|
||||
test('agent result view does not keep legacy publish blockers when preview uses anchorContent and sceneChapterBlueprints', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -8345,7 +8647,7 @@ test('agent result view does not keep legacy publish blockers when preview uses
|
||||
publishReady: true,
|
||||
blockers: [],
|
||||
preview: {
|
||||
...compiledAgentDraftSession.resultPreview!.preview,
|
||||
...compiledAgentResultPreview,
|
||||
settingText: '被海雾吞没的旧航路群岛',
|
||||
anchorContent: {
|
||||
worldPromise:
|
||||
@@ -8983,6 +9285,65 @@ test('save tab can resume a selected archive directly into the game', async () =
|
||||
});
|
||||
});
|
||||
|
||||
test('profile page exposes save archive picker as a direct entry', async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleContinueGame = vi.fn();
|
||||
|
||||
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 HydratedSavedGameSnapshot,
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth onContinueGame={handleContinueGame} />);
|
||||
|
||||
await clickFirstButtonByName(user, '我的');
|
||||
const shortcutRegion = await screen.findByRole('region', { name: '常用功能' });
|
||||
await user.click(within(shortcutRegion).getByRole('button', { name: /存档/u }));
|
||||
|
||||
const closeButton = await screen.findByLabelText('关闭存档');
|
||||
const modal = closeButton.closest('.fixed') as HTMLElement;
|
||||
expect(modal).toBeTruthy();
|
||||
expect(within(modal).getByText('SAVES')).toBeTruthy();
|
||||
|
||||
await user.click(within(modal).getByRole('button', { name: /潮雾列岛/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(resumeProfileSaveArchive).toHaveBeenCalledWith('custom:world-1');
|
||||
expect(handleContinueGame).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('creation hub published work can open detail view before deleting from detail page', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -9216,6 +9577,342 @@ test('creation hub published work experience button enters world directly', asyn
|
||||
expect(handleCustomWorldSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('creation hub published work start uses loaded detail profile instead of library summary', async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleCustomWorldSelect = vi.fn();
|
||||
const workProfileId = 'world-detail-launch-1';
|
||||
const summaryEntry = buildMockRpgGalleryDetail({
|
||||
ownerUserId: mockAuthUser.id,
|
||||
profileId: workProfileId,
|
||||
publicWorkCode: 'work-detail-launch-1',
|
||||
authorPublicUserCode: mockAuthUser.publicUserCode,
|
||||
visibility: 'published',
|
||||
publishedAt: '2026-04-20T10:00:00.000Z',
|
||||
updatedAt: '2026-04-20T10:00:00.000Z',
|
||||
authorDisplayName: mockAuthUser.displayName,
|
||||
worldName: '星砂废都',
|
||||
subtitle: '坠星沙海与废都钟楼',
|
||||
summaryText: '列表摘要只提供卡片信息,不能作为运行态 profile。',
|
||||
coverImageSrc: null,
|
||||
themeMode: 'tide',
|
||||
playableNpcCount: 1,
|
||||
landmarkCount: 1,
|
||||
likeCount: 0,
|
||||
});
|
||||
summaryEntry.profile = {
|
||||
...summaryEntry.profile,
|
||||
name: '默认档案',
|
||||
summary: '列表摘要不含运行态角色。',
|
||||
playableNpcs: [],
|
||||
storyNpcs: [],
|
||||
landmarks: [],
|
||||
};
|
||||
const detailEntry = buildMockRpgGalleryDetail({
|
||||
...summaryEntry,
|
||||
summaryText: '详情接口返回完整草稿内容。',
|
||||
});
|
||||
detailEntry.profile = {
|
||||
...detailEntry.profile,
|
||||
name: '星砂废都',
|
||||
subtitle: '坠星沙海与废都钟楼',
|
||||
summary: '详情接口返回完整草稿内容。',
|
||||
playableNpcs: [
|
||||
{
|
||||
...compiledAgentResultPreview.playableNpcs[0]!,
|
||||
id: 'playable-stardust-1',
|
||||
name: '砂眠',
|
||||
title: '废都引路人',
|
||||
},
|
||||
],
|
||||
landmarks: [
|
||||
{
|
||||
...compiledAgentResultPreview.landmarks[0]!,
|
||||
id: 'landmark-stardust-1',
|
||||
name: '坠星钟楼',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
vi.mocked(listRpgCreationWorks).mockResolvedValue([
|
||||
{
|
||||
workId: `published:${workProfileId}`,
|
||||
sourceType: 'published_profile',
|
||||
status: 'published',
|
||||
title: '星砂废都',
|
||||
subtitle: '坠星沙海与废都钟楼',
|
||||
summary: '详情接口返回完整草稿内容。',
|
||||
coverImageSrc: null,
|
||||
coverRenderMode: 'image',
|
||||
coverCharacterImageSrcs: [],
|
||||
updatedAt: '2026-04-20T10:00:00.000Z',
|
||||
publishedAt: '2026-04-20T10:00:00.000Z',
|
||||
stage: null,
|
||||
stageLabel: '已发布',
|
||||
playableNpcCount: 1,
|
||||
landmarkCount: 1,
|
||||
roleVisualReadyCount: 1,
|
||||
roleAnimationReadyCount: 0,
|
||||
roleAssetSummaryLabel: null,
|
||||
sessionId: null,
|
||||
profileId: workProfileId,
|
||||
canResume: false,
|
||||
canEnterWorld: true,
|
||||
},
|
||||
]);
|
||||
vi.mocked(listRpgEntryWorldLibrary).mockResolvedValue([summaryEntry]);
|
||||
vi.mocked(
|
||||
rpgEntryLibraryServiceMocks.getRpgEntryWorldLibraryDetail,
|
||||
).mockResolvedValue(detailEntry);
|
||||
|
||||
render(<TestWrapper withAuth onSelectWorld={handleCustomWorldSelect} />);
|
||||
|
||||
await openDraftHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: /查看详情/u }));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
rpgEntryLibraryServiceMocks.getRpgEntryWorldLibraryDetail,
|
||||
).toHaveBeenCalledWith(workProfileId);
|
||||
});
|
||||
await user.click(await screen.findByRole('button', { name: '启动' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleCustomWorldSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: workProfileId,
|
||||
name: '星砂废都',
|
||||
summary: '详情接口返回完整草稿内容。',
|
||||
playableNpcs: [
|
||||
expect.objectContaining({
|
||||
id: 'playable-stardust-1',
|
||||
name: '砂眠',
|
||||
}),
|
||||
],
|
||||
landmarks: [
|
||||
expect.objectContaining({
|
||||
id: 'landmark-stardust-1',
|
||||
name: '坠星钟楼',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(handleCustomWorldSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('creation hub published work edit keeps loaded detail profile assets instead of library summary', async () => {
|
||||
const user = userEvent.setup();
|
||||
const workProfileId = 'world-detail-edit-assets-1';
|
||||
const summaryEntry = buildMockRpgGalleryDetail({
|
||||
ownerUserId: mockAuthUser.id,
|
||||
profileId: workProfileId,
|
||||
publicWorkCode: 'work-detail-edit-assets-1',
|
||||
authorPublicUserCode: mockAuthUser.publicUserCode,
|
||||
visibility: 'published',
|
||||
publishedAt: '2026-04-20T10:00:00.000Z',
|
||||
updatedAt: '2026-04-20T10:00:00.000Z',
|
||||
authorDisplayName: mockAuthUser.displayName,
|
||||
worldName: '星砂废都',
|
||||
subtitle: '坠星沙海与废都钟楼',
|
||||
summaryText: '列表摘要字段齐全但不含详情资产。',
|
||||
coverImageSrc: null,
|
||||
themeMode: 'tide',
|
||||
playableNpcCount: 1,
|
||||
landmarkCount: 1,
|
||||
likeCount: 0,
|
||||
});
|
||||
summaryEntry.profile = {
|
||||
...summaryEntry.profile,
|
||||
name: '星砂废都',
|
||||
summary: '列表摘要字段齐全但不含详情资产。',
|
||||
playableNpcs: [
|
||||
{
|
||||
...compiledAgentResultPreview.playableNpcs[0]!,
|
||||
id: 'playable-stardust-1',
|
||||
name: '砂眠',
|
||||
imageSrc: undefined,
|
||||
},
|
||||
],
|
||||
storyNpcs: [
|
||||
{
|
||||
...compiledAgentResultPreview.storyNpcs[0]!,
|
||||
id: 'story-clock-keeper-1',
|
||||
name: '钟守',
|
||||
imageSrc: undefined,
|
||||
},
|
||||
],
|
||||
landmarks: [
|
||||
{
|
||||
...compiledAgentResultPreview.landmarks[0]!,
|
||||
id: 'landmark-stardust-1',
|
||||
name: '坠星钟楼',
|
||||
imageSrc: undefined,
|
||||
},
|
||||
],
|
||||
sceneChapterBlueprints: [
|
||||
{
|
||||
id: 'scene-chapter-stardust-1',
|
||||
sceneId: 'landmark-stardust-1',
|
||||
title: '坠星钟楼',
|
||||
summary: '星砂覆盖钟楼入口,钟守等待第一位访客。',
|
||||
sceneTaskDescription: '调查钟楼旧铃自鸣的原因。',
|
||||
linkedThreadIds: [],
|
||||
linkedLandmarkIds: ['landmark-stardust-1'],
|
||||
acts: [
|
||||
{
|
||||
id: 'act-stardust-opening-1',
|
||||
sceneId: 'landmark-stardust-1',
|
||||
title: '第一幕',
|
||||
summary: '砂眠带玩家进入坠星钟楼。',
|
||||
stageCoverage: ['opening'],
|
||||
backgroundImageSrc: undefined,
|
||||
encounterNpcIds: ['playable-stardust-1'],
|
||||
primaryNpcId: 'playable-stardust-1',
|
||||
oppositeNpcId: 'story-clock-keeper-1',
|
||||
eventDescription: '钟楼旧铃忽然自鸣。',
|
||||
linkedThreadIds: [],
|
||||
advanceRule: 'after_primary_contact',
|
||||
actGoal: '进入钟楼。',
|
||||
transitionHook: '星砂开始倒流。',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
cover: null,
|
||||
openingCg: null,
|
||||
};
|
||||
const detailEntry = buildMockRpgGalleryDetail({
|
||||
...summaryEntry,
|
||||
summaryText: '详情接口返回完整草稿内容。',
|
||||
});
|
||||
detailEntry.profile = {
|
||||
...summaryEntry.profile,
|
||||
summary: '详情接口返回完整草稿内容。',
|
||||
cover: {
|
||||
sourceType: 'generated',
|
||||
imageSrc: '/assets/custom-world/star-waste-cover.png',
|
||||
characterRoleIds: ['playable-stardust-1'],
|
||||
},
|
||||
openingCg: {
|
||||
id: 'opening-cg-stardust-1',
|
||||
status: 'ready',
|
||||
storyboardImageSrc: '/assets/custom-world/opening-storyboard.png',
|
||||
videoSrc: '/assets/custom-world/opening.mp4',
|
||||
imageModel: 'gpt-image-2',
|
||||
videoModel: 'doubao-seedance-2-0-fast-260128',
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '2k',
|
||||
videoResolution: '480p',
|
||||
durationSeconds: 15,
|
||||
pointCost: 80,
|
||||
estimatedWaitMinutes: 10,
|
||||
updatedAt: '2026-05-21T00:00:00.000Z',
|
||||
},
|
||||
camp: {
|
||||
id: 'camp-stardust-1',
|
||||
name: '废都营地',
|
||||
description: '钟楼阴影下的临时营地。',
|
||||
imageSrc: '/assets/custom-world/star-waste-camp.png',
|
||||
sceneNpcIds: ['playable-stardust-1'],
|
||||
connections: [],
|
||||
},
|
||||
playableNpcs: [
|
||||
{
|
||||
...summaryEntry.profile.playableNpcs[0]!,
|
||||
imageSrc: '/assets/custom-world/playable-stardust-1.png',
|
||||
},
|
||||
],
|
||||
storyNpcs: [
|
||||
{
|
||||
...summaryEntry.profile.storyNpcs[0]!,
|
||||
imageSrc: '/assets/custom-world/story-clock-keeper-1.png',
|
||||
},
|
||||
],
|
||||
landmarks: [
|
||||
{
|
||||
...summaryEntry.profile.landmarks[0]!,
|
||||
imageSrc: '/assets/custom-world/landmark-stardust-1.png',
|
||||
},
|
||||
],
|
||||
sceneChapterBlueprints: [
|
||||
{
|
||||
...summaryEntry.profile.sceneChapterBlueprints![0]!,
|
||||
acts: [
|
||||
{
|
||||
...summaryEntry.profile.sceneChapterBlueprints![0]!.acts[0]!,
|
||||
backgroundImageSrc:
|
||||
'/assets/custom-world/act-stardust-opening-1.png',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
vi.mocked(listRpgCreationWorks).mockResolvedValue([
|
||||
{
|
||||
workId: `published:${workProfileId}`,
|
||||
sourceType: 'published_profile',
|
||||
status: 'published',
|
||||
title: '星砂废都',
|
||||
subtitle: '坠星沙海与废都钟楼',
|
||||
summary: '列表摘要字段齐全但不含详情资产。',
|
||||
coverImageSrc: null,
|
||||
coverRenderMode: 'image',
|
||||
coverCharacterImageSrcs: [],
|
||||
updatedAt: '2026-04-20T10:00:00.000Z',
|
||||
publishedAt: '2026-04-20T10:00:00.000Z',
|
||||
stage: null,
|
||||
stageLabel: '已发布',
|
||||
playableNpcCount: 1,
|
||||
landmarkCount: 1,
|
||||
roleVisualReadyCount: 0,
|
||||
roleAnimationReadyCount: 0,
|
||||
roleAssetSummaryLabel: null,
|
||||
sessionId: null,
|
||||
profileId: workProfileId,
|
||||
canResume: false,
|
||||
canEnterWorld: true,
|
||||
},
|
||||
]);
|
||||
vi.mocked(listRpgEntryWorldLibrary).mockResolvedValue([summaryEntry]);
|
||||
vi.mocked(
|
||||
rpgEntryLibraryServiceMocks.getRpgEntryWorldLibraryDetail,
|
||||
).mockResolvedValue(detailEntry);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openDraftHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: /查看详情/u }));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
rpgEntryLibraryServiceMocks.getRpgEntryWorldLibraryDetail,
|
||||
).toHaveBeenCalledWith(workProfileId);
|
||||
});
|
||||
await user.click(await screen.findByRole('button', { name: '作品编辑' }));
|
||||
|
||||
expect(await screen.findByText('世界档案', {}, { timeout: 5000 })).toBeTruthy();
|
||||
expect(
|
||||
document.querySelector('video[src="/assets/custom-world/opening.mp4"]'),
|
||||
).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /场景\s+2/u }));
|
||||
expect((await screen.findByAltText('废都营地')).getAttribute('src')).toBe(
|
||||
'/assets/custom-world/star-waste-camp.png',
|
||||
);
|
||||
expect(screen.getByAltText('坠星钟楼-第一幕').getAttribute('src')).toBe(
|
||||
'/assets/custom-world/act-stardust-opening-1.png',
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /可扮演角色\s+1/u }));
|
||||
expect((await screen.findByAltText('砂眠')).getAttribute('src')).toBe(
|
||||
'/assets/custom-world/playable-stardust-1.png',
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /场景角色\s+1/u }));
|
||||
expect((await screen.findByAltText('钟守')).getAttribute('src')).toBe(
|
||||
'/assets/custom-world/story-clock-keeper-1.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('creation hub published work card reveals delete action after card action reveal', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user