fix(jump-hop): isolate draft runs from public leaderboard

This commit is contained in:
kdletters
2026-06-05 01:19:49 +08:00
parent 1b39c0c5d7
commit cb08c9ad20
17 changed files with 167 additions and 34 deletions

View File

@@ -1,7 +1,7 @@
/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import { expect, test, vi } from 'vitest';
import { beforeEach, expect, test, vi } from 'vitest';
import type { JumpHopWorkProfileResponse } from '../../../packages/shared/src/contracts/jumpHop';
import { useJumpHopLeaderboard } from '../../services/jump-hop/useJumpHopLeaderboard';
@@ -11,6 +11,16 @@ vi.mock('../../services/jump-hop/useJumpHopLeaderboard', () => ({
useJumpHopLeaderboard: vi.fn(),
}));
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useJumpHopLeaderboard).mockReturnValue({
leaderboard: null,
isLoading: false,
error: null,
refresh: vi.fn(),
});
});
test('跳一跳结果页展示排行榜列表', () => {
vi.mocked(useJumpHopLeaderboard).mockReturnValue({
leaderboard: {
@@ -40,7 +50,7 @@ test('跳一跳结果页展示排行榜列表', () => {
render(
<JumpHopResultView
profile={buildProfile()}
profile={buildProfile({ publicationStatus: 'published' })}
onBack={() => {}}
onEdit={() => {}}
onStartTestRun={() => {}}
@@ -57,13 +67,6 @@ test('跳一跳结果页展示排行榜列表', () => {
});
test('跳一跳结果页默认角色预览使用陶泥儿透明 logo', () => {
vi.mocked(useJumpHopLeaderboard).mockReturnValue({
leaderboard: null,
isLoading: false,
error: null,
refresh: vi.fn(),
});
render(
<JumpHopResultView
profile={buildProfile()}
@@ -80,7 +83,27 @@ test('跳一跳结果页默认角色预览使用陶泥儿透明 logo', () => {
);
});
function buildProfile(): JumpHopWorkProfileResponse {
test('跳一跳草稿结果页不请求公开排行榜', () => {
render(
<JumpHopResultView
profile={buildProfile({ publicationStatus: 'draft' })}
onBack={() => {}}
onEdit={() => {}}
onStartTestRun={() => {}}
onPublish={() => {}}
onRegenerateTiles={() => {}}
/>,
);
expect(useJumpHopLeaderboard).not.toHaveBeenCalled();
expect(screen.queryByText('排行榜')).toBeNull();
});
function buildProfile(
options: {
publicationStatus?: JumpHopWorkProfileResponse['summary']['publicationStatus'];
} = {},
): JumpHopWorkProfileResponse {
return {
summary: {
runtimeKind: 'jump-hop',
@@ -95,7 +118,7 @@ function buildProfile(): JumpHopWorkProfileResponse {
difficulty: 'standard',
stylePreset: 'minimal-blocks',
coverImageSrc: null,
publicationStatus: 'draft',
publicationStatus: options.publicationStatus ?? 'draft',
playCount: 0,
updatedAt: '2026-05-27T00:00:00Z',
publishedAt: null,

View File

@@ -272,6 +272,8 @@ export function JumpHopResultView({
const profileId = isWorkProfile
? profile.summary.profileId
: safeDraft.profileId;
const canShowLeaderboard =
isWorkProfile && profile.summary.publicationStatus === 'published';
const titleSource = isWorkProfile
? profile.summary.workTitle
: profile.workTitle;
@@ -365,7 +367,9 @@ export function JumpHopResultView({
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</div>
<JumpHopResultLeaderboard profileId={profileId} />
{canShowLeaderboard ? (
<JumpHopResultLeaderboard profileId={profileId} />
) : null}
{error ? (
<div className="platform-banner platform-banner--danger mt-3 rounded-2xl text-sm leading-6">
{error}

View File

@@ -278,7 +278,7 @@ test('跳一跳运行态失败后在弹窗中展示排行榜', () => {
render(
<JumpHopRuntimeShell
profile={buildProfile()}
profile={buildProfile({ publicationStatus: 'published' })}
run={buildFailedRun()}
runtimeRequestOptions={runtimeRequestOptions}
onJump={vi.fn().mockResolvedValue(undefined)}
@@ -298,6 +298,21 @@ test('跳一跳运行态失败后在弹窗中展示排行榜', () => {
expect(within(leaderboard).getByText('00:08')).toBeTruthy();
});
test('跳一跳草稿运行失败后不请求公开排行榜', () => {
render(
<JumpHopRuntimeShell
profile={buildProfile({ publicationStatus: 'draft' })}
run={buildFailedRun()}
onJump={vi.fn().mockResolvedValue(undefined)}
onRestart={() => {}}
/>,
);
expect(useJumpHopLeaderboard).not.toHaveBeenCalled();
expect(screen.getByRole('dialog', { name: '失败' })).toBeTruthy();
expect(screen.queryByTestId('jump-hop-runtime-leaderboard')).toBeNull();
});
test('跳一跳角色层永远压在地块层之上', () => {
render(
<JumpHopRuntimeShell
@@ -943,6 +958,7 @@ function buildProfile(options: {
tileAssets?: JumpHopWorkProfileResponse['tileAssets'];
coverComposite?: string | null;
coverImageSrc?: string | null;
publicationStatus?: JumpHopWorkProfileResponse['summary']['publicationStatus'];
} = {}): JumpHopWorkProfileResponse {
const characterAsset = {
assetId: 'builtin',
@@ -968,7 +984,7 @@ function buildProfile(options: {
difficulty: 'standard',
stylePreset: 'minimal-blocks',
coverImageSrc: options.coverImageSrc ?? null,
publicationStatus: 'draft',
publicationStatus: options.publicationStatus ?? 'draft',
playCount: 0,
updatedAt: '2026-05-27T00:00:00Z',
publishedAt: null,

View File

@@ -843,7 +843,9 @@ export function JumpHopRuntimeShell({
const jumpFeedbackForDisplay = getJumpHopJumpFeedbackLabel(stageRun);
const isSettled =
stageRun?.status === 'failed' || stageRun?.status === 'cleared';
const shouldShowFailureLeaderboard = stageRun?.status === 'failed';
const shouldShowFailureLeaderboard =
stageRun?.status === 'failed' &&
profile?.summary.publicationStatus === 'published';
const successfulJumpCount = stageRun?.successfulJumpCount ?? 0;
const durationLabel = formatJumpHopDurationLabel(
getJumpHopRunDurationMs(stageRun, nowMs),