Match3D & Puzzle: runtime UI, assets, drag fix
Backend: stop treating background music as a required draft asset and remove auto-submit/plan for background music; load persisted generated UI/assets into Match3D agent session responses (added helpers to resolve profile id and fetch existing generated assets). Frontend: make Match3D result preview reuse runtime UI styles, unify runtime settings entry, update PuzzleRuntime to apply immediate pointermove transforms (disable drag transition), use SVG clipPath for merged piece rounding, ensure PuzzleRuntimeShell supplies platform theme classes, and adjust related tests. Docs & logs: update decision log, pitfalls and product docs to reflect these changes.
This commit is contained in:
@@ -6682,6 +6682,97 @@ test('first puzzle runtime back click can open remix result page', async () => {
|
||||
expect(screen.getByDisplayValue('改造后的雨夜猫塔')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('recommend puzzle remix return restarts recommendation instead of stale loading run', async () => {
|
||||
const user = userEvent.setup();
|
||||
const puzzleWork: PuzzleWorkSummary = {
|
||||
workId: 'puzzle-work-public-1',
|
||||
profileId: 'puzzle-profile-public-1',
|
||||
ownerUserId: 'user-2',
|
||||
sourceSessionId: null,
|
||||
authorDisplayName: '拼图作者',
|
||||
levelName: '雨夜猫塔',
|
||||
summary: '一张聚焦发光猫咪与遗迹台阶的雨夜拼图。',
|
||||
themeTags: ['雨夜', '猫咪', '遗迹'],
|
||||
coverImageSrc: null,
|
||||
coverAssetId: null,
|
||||
publicationStatus: 'published',
|
||||
updatedAt: '2026-04-25T12:10:00.000Z',
|
||||
publishedAt: '2026-04-25T12:10:00.000Z',
|
||||
playCount: 8,
|
||||
remixCount: 0,
|
||||
likeCount: 0,
|
||||
publishReady: true,
|
||||
};
|
||||
const anchorPack = buildPuzzleAnchorPack();
|
||||
const remixDraft: PuzzleResultDraft = {
|
||||
workTitle: '改造后的雨夜猫塔',
|
||||
workDescription: '准备改造的拼图草稿。',
|
||||
levelName: '改造后的雨夜猫塔',
|
||||
summary: '一只猫站在雨夜塔顶。',
|
||||
themeTags: ['雨夜', '猫咪', '塔'],
|
||||
forbiddenDirectives: [],
|
||||
creatorIntent: null,
|
||||
anchorPack,
|
||||
candidates: [],
|
||||
selectedCandidateId: null,
|
||||
coverImageSrc: null,
|
||||
coverAssetId: null,
|
||||
generationStatus: 'idle',
|
||||
levels: [],
|
||||
metadata: null,
|
||||
};
|
||||
const remixSession: PuzzleAgentSessionSnapshot = {
|
||||
sessionId: 'puzzle-session-remix-1',
|
||||
currentTurn: 1,
|
||||
progressPercent: 100,
|
||||
stage: 'ready_to_publish',
|
||||
anchorPack,
|
||||
draft: remixDraft,
|
||||
messages: [],
|
||||
lastAssistantReply: null,
|
||||
publishedProfileId: null,
|
||||
suggestedActions: [],
|
||||
resultPreview: null,
|
||||
updatedAt: '2026-04-25T12:12:00.000Z',
|
||||
};
|
||||
|
||||
vi.mocked(listPuzzleGallery).mockResolvedValue({
|
||||
items: [puzzleWork],
|
||||
});
|
||||
vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
|
||||
item: puzzleWork,
|
||||
});
|
||||
vi.mocked(remixPuzzleGalleryWork).mockResolvedValue({
|
||||
session: remixSession,
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('puzzle-board')).toBeTruthy();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '改造 0' }));
|
||||
|
||||
expect(await screen.findByText('拼图结果页')).toBeTruthy();
|
||||
expect(screen.getByDisplayValue('改造后的雨夜猫塔')).toBeTruthy();
|
||||
|
||||
vi.mocked(startPuzzleRun).mockClear();
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
await clickFirstButtonByName(user, '推荐');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(startPuzzleRun).toHaveBeenCalledWith(
|
||||
{
|
||||
profileId: 'puzzle-profile-public-1',
|
||||
levelId: null,
|
||||
},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(screen.queryByText('正在进入拼图关卡')).toBeNull();
|
||||
});
|
||||
|
||||
test('public code search opens a published puzzle by PZ code', async () => {
|
||||
const user = userEvent.setup();
|
||||
const puzzleWork: PuzzleWorkSummary = {
|
||||
|
||||
@@ -1526,6 +1526,23 @@ test('profile played works card shows count unit', () => {
|
||||
expect(within(playedCard).getByText('1个')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('profile stats cards are centered without update timestamp', () => {
|
||||
renderProfileView(vi.fn(), {
|
||||
updatedAt: '2026-05-03T08:01:00Z',
|
||||
});
|
||||
|
||||
const walletCard = screen.getByRole('button', { name: /泥点\s*0/u });
|
||||
const playTimeCard = screen.getByRole('button', { name: /游戏时长/u });
|
||||
const playedCard = screen.getByRole('button', { name: /玩过\s*0个/u });
|
||||
|
||||
for (const card of [walletCard, playTimeCard, playedCard]) {
|
||||
expect(card.className).toContain('items-center');
|
||||
expect(card.className).toContain('justify-center');
|
||||
expect(card.className).toContain('text-center');
|
||||
}
|
||||
expect(screen.queryByText(/更新于/u)).toBeNull();
|
||||
});
|
||||
|
||||
test('desktop account entry uses saved avatar image when available', () => {
|
||||
mockDesktopLayout();
|
||||
const avatarUrl = 'data:image/png;base64,AAAA';
|
||||
@@ -2815,9 +2832,7 @@ test('mobile game category filter dialog filters by play type', async () => {
|
||||
|
||||
await user.click(within(filterDialog).getByRole('button', { name: '抓鹅' }));
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /奇幻拼图,试玩/u }),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /奇幻拼图,试玩/u })).toBeNull();
|
||||
expect(screen.getByRole('button', { name: /奇幻抓鹅,进入/u })).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
@@ -2118,24 +2118,6 @@ function formatDashboardCount(value: number) {
|
||||
return normalizedValue.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function formatDashboardUpdatedAt(value: string | null | undefined) {
|
||||
if (!value) {
|
||||
return '暂无更新记录';
|
||||
}
|
||||
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function isWithinProfileInviteRedeemWindow(
|
||||
createdAt: string | null | undefined,
|
||||
) {
|
||||
@@ -2309,13 +2291,15 @@ function ProfileStatCard({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick ? () => onClick(cardKey) : undefined}
|
||||
className="platform-subpanel rounded-[1.35rem] px-4 py-3 text-left transition hover:border-[var(--platform-surface-hover-border)] hover:bg-[var(--platform-button-secondary-fill)]"
|
||||
className="platform-subpanel flex min-h-[5.75rem] flex-col items-center justify-center rounded-[1.35rem] px-3 py-3 text-center transition hover:border-[var(--platform-surface-hover-border)] hover:bg-[var(--platform-button-secondary-fill)]"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-[var(--platform-text-soft)]">
|
||||
<div className="flex w-full items-center justify-center gap-2 text-[var(--platform-text-soft)]">
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="text-[11px] tracking-[0.16em]">{label}</span>
|
||||
<span className="whitespace-nowrap text-[11px] tracking-[0.16em]">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 text-lg font-black text-[var(--platform-text-strong)]">
|
||||
<div className="mt-2 whitespace-nowrap text-lg font-black leading-none text-[var(--platform-text-strong)]">
|
||||
{value}
|
||||
</div>
|
||||
</button>
|
||||
@@ -2324,9 +2308,9 @@ function ProfileStatCard({
|
||||
|
||||
function ProfileStatCardSkeleton() {
|
||||
return (
|
||||
<div className="platform-subpanel rounded-[1.35rem] px-4 py-3">
|
||||
<div className="platform-subpanel flex min-h-[5.75rem] flex-col items-center justify-center rounded-[1.35rem] px-3 py-3 text-center">
|
||||
<div className="h-4 w-20 animate-pulse rounded-full bg-[var(--platform-subpanel-border)]" />
|
||||
<div className="mt-3 h-7 w-16 animate-pulse rounded-full bg-[var(--platform-line-soft)]" />
|
||||
<div className="mt-2 h-7 w-16 animate-pulse rounded-full bg-[var(--platform-line-soft)]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5656,11 +5640,6 @@ export function RpgEntryHomeView({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 text-[11px] text-[var(--platform-text-soft)]">
|
||||
{dashboardError
|
||||
? dashboardError
|
||||
: `更新于 ${formatDashboardUpdatedAt(profileDashboard?.updatedAt)}`}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
|
||||
Reference in New Issue
Block a user