收口创作流程统一总计划并修复等待页窄屏裁切
This commit is contained in:
@@ -17,6 +17,10 @@ import type {
|
||||
BabyObjectMatchDraft,
|
||||
CreateBabyObjectMatchDraftRequest,
|
||||
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type {
|
||||
JumpHopWorkDetailResponse,
|
||||
JumpHopWorkProfileResponse,
|
||||
} from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent';
|
||||
import type { Match3DRunSnapshot } from '../../../packages/shared/src/contracts/match3dRuntime';
|
||||
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
@@ -82,6 +86,7 @@ import {
|
||||
regenerateBabyObjectMatchDraftAssets,
|
||||
saveBabyObjectMatchDraft,
|
||||
} from '../../services/edutainment-baby-object';
|
||||
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
|
||||
import { match3dCreationClient } from '../../services/match3d-creation';
|
||||
import {
|
||||
createServerMatch3DRuntimeAdapter,
|
||||
@@ -625,6 +630,22 @@ vi.mock('../../services/edutainment-baby-object', () => ({
|
||||
saveBabyObjectMatchDraft: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/jump-hop/jumpHopClient', () => ({
|
||||
jumpHopClient: {
|
||||
createSession: vi.fn(),
|
||||
executeAction: vi.fn(),
|
||||
getGalleryDetail: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
getWorkDetail: vi.fn(),
|
||||
listGallery: vi.fn(),
|
||||
listWorks: vi.fn(),
|
||||
publishWork: vi.fn(),
|
||||
restartRun: vi.fn(),
|
||||
startRun: vi.fn(),
|
||||
submitJump: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../services/match3d-creation', () => ({
|
||||
match3dCreationClient: {
|
||||
createSession: vi.fn(),
|
||||
@@ -1465,6 +1486,139 @@ function buildMockBabyObjectMatchDraft(
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockJumpHopWork(
|
||||
overrides: Partial<JumpHopWorkProfileResponse> = {},
|
||||
): JumpHopWorkProfileResponse {
|
||||
const profileId = overrides.summary?.profileId ?? 'jump-hop-profile-1';
|
||||
const path = overrides.path ?? {
|
||||
seed: 'jump-hop-seed',
|
||||
difficulty: 'standard' as const,
|
||||
platforms: [
|
||||
{
|
||||
platformId: 'platform-start',
|
||||
tileType: 'start' as const,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 48,
|
||||
height: 36,
|
||||
landingRadius: 22,
|
||||
perfectRadius: 12,
|
||||
scoreValue: 1,
|
||||
},
|
||||
{
|
||||
platformId: 'platform-finish',
|
||||
tileType: 'finish' as const,
|
||||
x: 16,
|
||||
y: 18,
|
||||
width: 60,
|
||||
height: 42,
|
||||
landingRadius: 22,
|
||||
perfectRadius: 12,
|
||||
scoreValue: 2,
|
||||
},
|
||||
],
|
||||
finishIndex: 1,
|
||||
cameraPreset: 'default',
|
||||
scoring: {
|
||||
chargeToDistanceRatio: 1.2,
|
||||
maxChargeMs: 1800,
|
||||
hitBonus: 20,
|
||||
perfectBonus: 50,
|
||||
},
|
||||
};
|
||||
const characterAsset = overrides.characterAsset ?? {
|
||||
assetId: 'jump-hop-character-1',
|
||||
imageSrc: 'data:image/png;base64,character',
|
||||
imageObjectKey: 'jump-hop/character.png',
|
||||
assetObjectId: 'asset-jump-hop-character',
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '纸片小兔',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
};
|
||||
const tileAtlasAsset = overrides.tileAtlasAsset ?? {
|
||||
assetId: 'jump-hop-tiles-1',
|
||||
imageSrc: 'data:image/png;base64,tiles',
|
||||
imageObjectKey: 'jump-hop/tiles.png',
|
||||
assetObjectId: 'asset-jump-hop-tiles',
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '柔软云朵平台',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
};
|
||||
const tileAssets = overrides.tileAssets ?? [
|
||||
{
|
||||
tileType: 'start' as const,
|
||||
imageSrc: 'data:image/png;base64,tile-start',
|
||||
imageObjectKey: 'jump-hop/tile-start.png',
|
||||
assetObjectId: 'asset-jump-hop-tile-start',
|
||||
sourceAtlasCell: 'A1',
|
||||
visualWidth: 128,
|
||||
visualHeight: 96,
|
||||
topSurfaceRadius: 24,
|
||||
landingRadius: 28,
|
||||
},
|
||||
{
|
||||
tileType: 'finish' as const,
|
||||
imageSrc: 'data:image/png;base64,tile-finish',
|
||||
imageObjectKey: 'jump-hop/tile-finish.png',
|
||||
assetObjectId: 'asset-jump-hop-tile-finish',
|
||||
sourceAtlasCell: 'A2',
|
||||
visualWidth: 128,
|
||||
visualHeight: 96,
|
||||
topSurfaceRadius: 24,
|
||||
landingRadius: 28,
|
||||
},
|
||||
];
|
||||
const draft = overrides.draft ?? {
|
||||
templateId: 'jump-hop',
|
||||
templateName: '跳一跳',
|
||||
profileId,
|
||||
workTitle: '云端跳台',
|
||||
workDescription: '一路跳到星星。',
|
||||
themeTags: ['云朵', '星空'],
|
||||
difficulty: 'standard' as const,
|
||||
stylePreset: 'paper-toy' as const,
|
||||
characterPrompt: '纸片小兔',
|
||||
tilePrompt: '柔软云朵平台',
|
||||
endMoodPrompt: '星光门',
|
||||
characterAsset,
|
||||
tileAtlasAsset,
|
||||
tileAssets,
|
||||
path,
|
||||
coverComposite: 'data:image/png;base64,cover',
|
||||
generationStatus: 'ready' as const,
|
||||
};
|
||||
|
||||
return {
|
||||
summary: {
|
||||
runtimeKind: 'jump-hop',
|
||||
workId: 'jump-hop-work-1',
|
||||
profileId,
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'jump-hop-session-1',
|
||||
workTitle: draft.workTitle,
|
||||
workDescription: draft.workDescription,
|
||||
themeTags: draft.themeTags,
|
||||
difficulty: draft.difficulty,
|
||||
stylePreset: draft.stylePreset,
|
||||
coverImageSrc: draft.coverComposite,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-30T10:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: true,
|
||||
generationStatus: 'ready',
|
||||
...overrides.summary,
|
||||
},
|
||||
draft,
|
||||
path,
|
||||
characterAsset,
|
||||
tileAtlasAsset,
|
||||
tileAssets,
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockBarkBattleWork(
|
||||
overrides: Partial<BarkBattleWorkSummary> = {},
|
||||
): BarkBattleWorkSummary {
|
||||
@@ -2520,6 +2674,18 @@ beforeEach(() => {
|
||||
vi.mocked(listVisualNovelWorks).mockResolvedValue({ works: [] });
|
||||
vi.mocked(listLocalBabyObjectMatchDrafts).mockResolvedValue([]);
|
||||
vi.mocked(deleteLocalBabyObjectMatchDraft).mockResolvedValue([]);
|
||||
vi.mocked(jumpHopClient.listGallery).mockResolvedValue({
|
||||
items: [],
|
||||
hasMore: false,
|
||||
nextCursor: null,
|
||||
});
|
||||
vi.mocked(jumpHopClient.listWorks).mockResolvedValue({ items: [] });
|
||||
vi.mocked(jumpHopClient.getSession).mockRejectedValue(
|
||||
new Error('未找到跳一跳会话'),
|
||||
);
|
||||
vi.mocked(jumpHopClient.getWorkDetail).mockRejectedValue(
|
||||
new Error('未找到跳一跳作品'),
|
||||
);
|
||||
vi.mocked(saveBabyObjectMatchDraft).mockImplementation(async (payload) => ({
|
||||
draft: payload.draft,
|
||||
}));
|
||||
@@ -7215,6 +7381,58 @@ test('refreshing RPG agent path restores stored agent workspace pointer', async
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('direct jump hop result route shows recovery panel when no draft pointer exists', async () => {
|
||||
window.history.replaceState(null, '', '/creation/jump-hop/result');
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
expect(await screen.findByText('跳一跳草稿未恢复')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '返回创作' })).toBeTruthy();
|
||||
expect(jumpHopClient.getWorkDetail).not.toHaveBeenCalled();
|
||||
expect(jumpHopClient.getSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('direct jump hop result route restores work detail by profile id', async () => {
|
||||
const work = buildMockJumpHopWork({
|
||||
summary: {
|
||||
runtimeKind: 'jump-hop',
|
||||
workId: 'jump-hop-work-restore-1',
|
||||
profileId: 'jump-hop-profile-restore-1',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: null,
|
||||
workTitle: '恢复后的云端跳台',
|
||||
workDescription: '从 profileId 回读完整跳一跳结果。',
|
||||
themeTags: ['云朵'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'paper-toy',
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-30T10:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: true,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
});
|
||||
vi.mocked(jumpHopClient.getWorkDetail).mockResolvedValueOnce({
|
||||
item: work,
|
||||
} satisfies JumpHopWorkDetailResponse);
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/creation/jump-hop/result?profileId=jump-hop-profile-restore-1',
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
expect(await screen.findByText('恢复后的云端跳台')).toBeTruthy();
|
||||
expect(screen.queryByText('跳一跳草稿未恢复')).toBeNull();
|
||||
expect(jumpHopClient.getWorkDetail).toHaveBeenCalledWith(
|
||||
'jump-hop-profile-restore-1',
|
||||
);
|
||||
expect(jumpHopClient.getSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('embedded puzzle form maps raw bearer token errors to user-facing auth copy', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
@@ -6038,6 +6038,11 @@ export function RpgEntryHomeView({
|
||||
<div className={MOBILE_PAGE_STAGE_CLASS}>
|
||||
<section>
|
||||
<SectionHeader title="我的创作" detail="草稿与已发布" />
|
||||
{platformError ? (
|
||||
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-700">
|
||||
{platformError}
|
||||
</div>
|
||||
) : null}
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取你的作品..." />
|
||||
) : myEntries.length > 0 ? (
|
||||
@@ -6074,7 +6079,16 @@ export function RpgEntryHomeView({
|
||||
const createContent: ReactNode =
|
||||
createTabContent ?? fallbackCreateStartContent;
|
||||
|
||||
const savesContent: ReactNode = draftTabContent ?? fallbackDraftContent;
|
||||
const savesContent: ReactNode = (
|
||||
<>
|
||||
{platformError ? (
|
||||
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-700">
|
||||
{platformError}
|
||||
</div>
|
||||
) : null}
|
||||
{draftTabContent ?? fallbackDraftContent}
|
||||
</>
|
||||
);
|
||||
|
||||
const profileContent: ReactNode = (
|
||||
<div className={`${MOBILE_PROFILE_PAGE_STAGE_CLASS} platform-profile-page`}>
|
||||
|
||||
@@ -870,6 +870,10 @@ export function resolvePlatformWorkAuthorDisplayName(
|
||||
const displayName = authorSummary?.displayName?.trim();
|
||||
const publicUserCode = authorSummary?.publicUserCode?.trim();
|
||||
|
||||
if (displayName && publicUserCode) {
|
||||
return `${displayName} · ${publicUserCode}`;
|
||||
}
|
||||
|
||||
return displayName || publicUserCode || entry.authorDisplayName.trim() || '玩家';
|
||||
}
|
||||
|
||||
@@ -1079,4 +1083,4 @@ function buildBarkBattleThemeTags(work: BarkBattleWorkSummary) {
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 3);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user