统一推荐页游客运行态与切换队列
统一推荐页各玩法正式 runtime 的游客鉴权透传。 收口推荐页首页展示队列和嵌入运行态切换队列。 补齐未登录读档、签名资产和个人数据读取的游客态处理。 新增运行态 HUD 小尺寸 logo 资源并更新拼图与抓鹅展示。 补充推荐切换、runtime guest 启动和客户端请求回归测试。 更新玩法链路、后端契约和团队记忆文档。
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import jumpHopRuntimeLevelLogo from '../../../media/logo.png';
|
||||
import jumpHopRuntimeLevelLogo from '../../../media/logo-runtime-hud.webp';
|
||||
import type {
|
||||
JumpHopRuntimeRunSnapshotResponse,
|
||||
JumpHopTileFaceAsset,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import match3DRuntimeLevelLogo from '../../../media/logo.png';
|
||||
import match3DRuntimeLevelLogo from '../../../media/logo-runtime-hud.webp';
|
||||
import type {
|
||||
Match3DClickItemRequest,
|
||||
Match3DClickItemResult,
|
||||
|
||||
@@ -318,6 +318,7 @@ import {
|
||||
submitRpgProfileFeedback,
|
||||
} from '../../services/rpg-entry/rpgProfileClient';
|
||||
import { requestRpgRuntimeJson } from '../../services/rpg-runtime/rpgRuntimeRequest';
|
||||
import { type RuntimeGuestRequestOptions } from '../../services/runtimeGuestAuth';
|
||||
import { squareHoleCreationClient } from '../../services/square-hole-creation';
|
||||
import {
|
||||
dropSquareHoleShape,
|
||||
@@ -372,13 +373,16 @@ import {
|
||||
type CreationWorkShelfItem,
|
||||
isPersistedBarkBattleDraftGenerating,
|
||||
} from '../custom-world-home/creationWorkShelf';
|
||||
import { selectAdjacentPlatformRecommendEntry } from '../rpg-entry/rpgEntryPublicGalleryViewModel';
|
||||
import {
|
||||
buildPlatformRecommendFeedEntries,
|
||||
selectAdjacentPlatformRecommendEntry,
|
||||
} from '../rpg-entry/rpgEntryPublicGalleryViewModel';
|
||||
import {
|
||||
isBigFishGalleryEntry,
|
||||
isEdutainmentGalleryEntry,
|
||||
isJumpHopGalleryEntry,
|
||||
isPuzzleGalleryEntry,
|
||||
isPuzzleClearGalleryEntry,
|
||||
isPuzzleGalleryEntry,
|
||||
mapPuzzleClearWorkToPlatformGalleryCard,
|
||||
mapPuzzleWorkToPlatformGalleryCard,
|
||||
type PlatformPublicGalleryCard,
|
||||
@@ -492,7 +496,6 @@ import {
|
||||
import {
|
||||
canExposePublicWork,
|
||||
EDUTAINMENT_HIDDEN_MESSAGE,
|
||||
filterGeneralPublicWorks,
|
||||
} from './platformEdutainmentVisibility';
|
||||
import { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal';
|
||||
import type { PlatformCreationTypeId } from './platformEntryCreationTypes';
|
||||
@@ -523,7 +526,6 @@ import { PlatformEntryWorldDetailView } from './PlatformEntryWorldDetailView';
|
||||
import { PlatformErrorDialog } from './PlatformErrorDialog';
|
||||
import { PlatformFeedbackView } from './PlatformFeedbackView';
|
||||
import { resolvePlatformGenerationProgressTickDecision } from './platformGenerationProgressTickModel';
|
||||
import { buildPlatformRecommendedEntries } from './platformRecommendation';
|
||||
import {
|
||||
buildMatch3DProfileFromSession,
|
||||
hasMatch3DRuntimeAsset,
|
||||
@@ -1476,6 +1478,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
useState<MiniGameDraftGenerationState | null>(null);
|
||||
const [jumpHopError, setJumpHopError] = useState<string | null>(null);
|
||||
const [isJumpHopBusy, setIsJumpHopBusy] = useState(false);
|
||||
const [barkBattleRuntimeRequestOptions, setBarkBattleRuntimeRequestOptions] =
|
||||
useState<RuntimeGuestRequestOptions | null>(null);
|
||||
const [puzzleClearSession, setPuzzleClearSession] =
|
||||
useState<PuzzleClearSessionSnapshotResponse | null>(null);
|
||||
const [puzzleClearRun, setPuzzleClearRun] = useState<
|
||||
@@ -1579,6 +1583,34 @@ export function PlatformEntryFlowShellImpl({
|
||||
useState<PuzzleRuntimeReturnStage>('puzzle-gallery-detail');
|
||||
const [puzzleRuntimeAuthMode, setPuzzleRuntimeAuthMode] =
|
||||
useState<PuzzleRuntimeAuthMode>('default');
|
||||
const buildRecommendRuntimeRequestOptions = useCallback(
|
||||
async (
|
||||
input: {
|
||||
kind?: RecommendRuntimeKind;
|
||||
embedded?: boolean;
|
||||
forcePublicRuntime?: boolean;
|
||||
} = {},
|
||||
) => {
|
||||
const shouldUseRuntimeOptions = Boolean(
|
||||
input.forcePublicRuntime ||
|
||||
input.embedded ||
|
||||
(input.kind && activeRecommendRuntimeKind === input.kind),
|
||||
);
|
||||
|
||||
return shouldUseRuntimeOptions
|
||||
? buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: {};
|
||||
},
|
||||
[activeRecommendRuntimeKind, authUi],
|
||||
);
|
||||
const buildPuzzleRuntimeRequestOptions = useCallback(
|
||||
() =>
|
||||
buildRecommendRuntimeRequestOptions({
|
||||
kind: 'puzzle',
|
||||
forcePublicRuntime: puzzleRuntimeAuthMode === 'isolated',
|
||||
}),
|
||||
[buildRecommendRuntimeRequestOptions, puzzleRuntimeAuthMode],
|
||||
);
|
||||
const [isPuzzleLeaderboardBusy, setIsPuzzleLeaderboardBusy] = useState(false);
|
||||
const submittedPuzzleLeaderboardKeysRef = useRef(new Set<string>());
|
||||
const puzzleStartInFlightKeyRef = useRef<string | null>(null);
|
||||
@@ -2910,10 +2942,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
} = publicGalleryFeeds;
|
||||
const recommendRuntimeEntries = useMemo(
|
||||
() =>
|
||||
buildPlatformRecommendedEntries({
|
||||
featuredEntries: filterGeneralPublicWorks(featuredGalleryEntries),
|
||||
latestEntries: filterGeneralPublicWorks(latestGalleryEntries),
|
||||
}),
|
||||
buildPlatformRecommendFeedEntries(
|
||||
featuredGalleryEntries,
|
||||
latestGalleryEntries,
|
||||
),
|
||||
[featuredGalleryEntries, latestGalleryEntries],
|
||||
);
|
||||
|
||||
@@ -6971,15 +7003,17 @@ export function PlatformEntryFlowShellImpl({
|
||||
profileId: targetProfileId,
|
||||
mode: 'play' as const,
|
||||
};
|
||||
const runtimeGuestOptions =
|
||||
options.embedded || workDetail.summary.publishStatus === 'draft'
|
||||
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: {};
|
||||
const runtimeRequestOptions = await buildRecommendRuntimeRequestOptions(
|
||||
{
|
||||
kind: 'visual-novel',
|
||||
embedded: options.embedded,
|
||||
},
|
||||
);
|
||||
const { run } = options.embedded
|
||||
? await startVisualNovelRun(
|
||||
targetProfileId,
|
||||
startRunPayload,
|
||||
runtimeGuestOptions,
|
||||
runtimeRequestOptions,
|
||||
)
|
||||
: await startVisualNovelRun(targetProfileId, startRunPayload);
|
||||
setVisualNovelWork(workDetail);
|
||||
@@ -7005,7 +7039,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
},
|
||||
[
|
||||
authUi,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
resolvePuzzleErrorMessage,
|
||||
setIsVisualNovelBusy,
|
||||
setSelectionStage,
|
||||
@@ -7027,14 +7061,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
setVisualNovelError(null);
|
||||
setIsVisualNovelBusy(true);
|
||||
try {
|
||||
const runtimeGuestOptions =
|
||||
activeRecommendRuntimeKind === 'visual-novel'
|
||||
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: {};
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'visual-novel',
|
||||
});
|
||||
const nextRun = await streamVisualNovelRuntimeAction(
|
||||
visualNovelRun.runId,
|
||||
payload,
|
||||
runtimeGuestOptions,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setVisualNovelRun(nextRun);
|
||||
} catch (error) {
|
||||
@@ -7046,8 +7080,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
},
|
||||
[
|
||||
activeRecommendRuntimeKind,
|
||||
authUi,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
isVisualNovelBusy,
|
||||
resolvePuzzleErrorMessage,
|
||||
setIsVisualNovelBusy,
|
||||
@@ -7496,22 +7529,15 @@ export function PlatformEntryFlowShellImpl({
|
||||
setJumpHopError(null);
|
||||
setJumpHopRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||
try {
|
||||
const runtimeGuestOptions =
|
||||
options.embedded || shouldUseRecommendRuntimeGuestAuth(authUi)
|
||||
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'jump-hop',
|
||||
embedded: options.embedded,
|
||||
forcePublicRuntime: shouldUseRecommendRuntimeGuestAuth(authUi),
|
||||
});
|
||||
setJumpHopRuntimeRequestOptions(
|
||||
runtimeGuestOptions.runtimeGuestToken?.trim()
|
||||
? {
|
||||
runtimeGuestToken: runtimeGuestOptions.runtimeGuestToken,
|
||||
authImpact: runtimeGuestOptions.authImpact,
|
||||
skipAuth: runtimeGuestOptions.skipAuth,
|
||||
skipRefresh: runtimeGuestOptions.skipRefresh,
|
||||
notifyAuthStateChange:
|
||||
runtimeGuestOptions.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized:
|
||||
runtimeGuestOptions.clearAuthOnUnauthorized,
|
||||
}
|
||||
Object.keys(runtimeRequestOptions).length > 0
|
||||
? runtimeRequestOptions
|
||||
: null,
|
||||
);
|
||||
const [detail, runResponse] = await Promise.all([
|
||||
@@ -7521,7 +7547,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
.getWorkDetail(normalizedProfileId)
|
||||
.catch(() => null),
|
||||
jumpHopClient.startRun(normalizedProfileId, {
|
||||
...runtimeGuestOptions,
|
||||
...runtimeRequestOptions,
|
||||
runtimeMode: 'published',
|
||||
}),
|
||||
]);
|
||||
@@ -7548,7 +7574,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsJumpHopBusy(false);
|
||||
}
|
||||
},
|
||||
[authUi, setSelectionStage],
|
||||
[authUi, buildRecommendRuntimeRequestOptions, setSelectionStage],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -7989,15 +8015,16 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleClearError(null);
|
||||
setPuzzleClearRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'puzzle-clear',
|
||||
embedded: options.embedded,
|
||||
});
|
||||
const [detail, runResponse] = await Promise.all([
|
||||
puzzleClearClient
|
||||
.getRuntimeWorkDetail(normalizedProfileId)
|
||||
.catch(() => null),
|
||||
puzzleClearClient.startRun(normalizedProfileId, runtimeGuestOptions),
|
||||
puzzleClearClient.startRun(normalizedProfileId, runtimeRequestOptions),
|
||||
]);
|
||||
if (detail?.item) {
|
||||
setPuzzleClearWork(detail.item);
|
||||
@@ -8022,7 +8049,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsPuzzleClearBusy(false);
|
||||
}
|
||||
},
|
||||
[authUi, setSelectionStage],
|
||||
[buildRecommendRuntimeRequestOptions, setSelectionStage],
|
||||
);
|
||||
|
||||
const retryPuzzleClearLevelRun = useCallback(async () => {
|
||||
@@ -8047,7 +8074,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsPuzzleClearBusy(true);
|
||||
setPuzzleClearError(null);
|
||||
try {
|
||||
const response = await puzzleClearClient.retryLevel(runId);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'puzzle-clear',
|
||||
});
|
||||
const response = await puzzleClearClient.retryLevel(
|
||||
runId,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setPuzzleClearRun(response.run);
|
||||
} catch (error) {
|
||||
setPuzzleClearError(
|
||||
@@ -8059,6 +8093,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}, [
|
||||
puzzleClearRun,
|
||||
puzzleClearWork,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
setSelectionStage,
|
||||
startPuzzleClearTestRunFromProfile,
|
||||
]);
|
||||
@@ -8084,7 +8119,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsPuzzleClearBusy(true);
|
||||
setPuzzleClearError(null);
|
||||
try {
|
||||
const response = await puzzleClearClient.advanceNextLevel(runId);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'puzzle-clear',
|
||||
});
|
||||
const response = await puzzleClearClient.advanceNextLevel(
|
||||
runId,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setPuzzleClearRun(response.run);
|
||||
} catch (error) {
|
||||
setPuzzleClearError(
|
||||
@@ -8093,7 +8135,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
} finally {
|
||||
setIsPuzzleClearBusy(false);
|
||||
}
|
||||
}, [puzzleClearRun, puzzleClearWork, setSelectionStage]);
|
||||
}, [
|
||||
puzzleClearRun,
|
||||
puzzleClearWork,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
setSelectionStage,
|
||||
]);
|
||||
|
||||
const markPuzzleClearLevelTimeUp = useCallback(async () => {
|
||||
const runId = puzzleClearRun?.runId;
|
||||
@@ -8107,14 +8154,21 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await puzzleClearClient.markTimeUp(runId);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'puzzle-clear',
|
||||
});
|
||||
const response = await puzzleClearClient.markTimeUp(
|
||||
runId,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setPuzzleClearRun(response.run);
|
||||
} catch (error) {
|
||||
setPuzzleClearError(
|
||||
resolveRpgCreationErrorMessage(error, '同步拼消消倒计时失败。'),
|
||||
);
|
||||
}
|
||||
}, [puzzleClearRun]);
|
||||
}, [puzzleClearRun, buildRecommendRuntimeRequestOptions]);
|
||||
|
||||
const swapPuzzleClearCardsInRun = useCallback(
|
||||
async (payload: {
|
||||
@@ -8133,7 +8187,15 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await puzzleClearClient.swapCards(runId, payload);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'puzzle-clear',
|
||||
});
|
||||
const response = await puzzleClearClient.swapCards(
|
||||
runId,
|
||||
payload,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setPuzzleClearRun(response.run);
|
||||
} catch (error) {
|
||||
setPuzzleClearError(
|
||||
@@ -8141,7 +8203,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
}
|
||||
},
|
||||
[puzzleClearRun],
|
||||
[puzzleClearRun, buildRecommendRuntimeRequestOptions],
|
||||
);
|
||||
|
||||
const compileWoodenFishSession = useCallback(
|
||||
@@ -8493,16 +8555,17 @@ export function PlatformEntryFlowShellImpl({
|
||||
setWoodenFishError(null);
|
||||
setWoodenFishRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'wooden-fish',
|
||||
embedded: options.embedded,
|
||||
});
|
||||
const [detail, runResponse] = await Promise.all([
|
||||
woodenFishClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
||||
options.embedded
|
||||
? woodenFishClient.startRun(
|
||||
normalizedProfileId,
|
||||
runtimeGuestOptions,
|
||||
runtimeRequestOptions,
|
||||
)
|
||||
: woodenFishClient.startRun(normalizedProfileId),
|
||||
]);
|
||||
@@ -8529,7 +8592,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsWoodenFishBusy(false);
|
||||
}
|
||||
},
|
||||
[authUi, setSelectionStage],
|
||||
[buildRecommendRuntimeRequestOptions, setSelectionStage],
|
||||
);
|
||||
|
||||
const checkpointWoodenFishRuntimeRun = useCallback(
|
||||
@@ -8541,10 +8604,18 @@ export function PlatformEntryFlowShellImpl({
|
||||
if (!runId) {
|
||||
return;
|
||||
}
|
||||
const response = await woodenFishClient.checkpointRun(runId, payload);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'wooden-fish',
|
||||
});
|
||||
const response = await woodenFishClient.checkpointRun(
|
||||
runId,
|
||||
payload,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setWoodenFishRun(response.run);
|
||||
},
|
||||
[woodenFishRun?.runId],
|
||||
[buildRecommendRuntimeRequestOptions, woodenFishRun?.runId],
|
||||
);
|
||||
|
||||
const executePuzzleAction = puzzleFlow.executeAction;
|
||||
@@ -9015,9 +9086,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
try {
|
||||
let runtimeProfile: Match3DWorkProfile | Match3DWorkSummary = profile;
|
||||
const canReadProtectedMatch3DDetail =
|
||||
!options.embedded || !shouldUseRecommendRuntimeGuestAuth(authUi);
|
||||
if (
|
||||
!hasMatch3DRuntimeAsset(profile.generatedItemAssets) ||
|
||||
!hasMatch3DRuntimeBackgroundAsset(profile)
|
||||
canReadProtectedMatch3DDetail &&
|
||||
(!hasMatch3DRuntimeAsset(profile.generatedItemAssets) ||
|
||||
!hasMatch3DRuntimeBackgroundAsset(profile))
|
||||
) {
|
||||
try {
|
||||
const { item } = await getMatch3DWorkDetail(profile.profileId);
|
||||
@@ -9043,12 +9117,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
runtimeProfile.generatedBackgroundAsset,
|
||||
{ expireSeconds: 300 },
|
||||
);
|
||||
const runtimeGuestOptions =
|
||||
options.authMode === 'isolated'
|
||||
? RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS
|
||||
: await buildRecommendRuntimeAuthOptions(authUi, options.embedded);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'match3d',
|
||||
embedded: options.embedded,
|
||||
forcePublicRuntime: options.authMode === 'isolated',
|
||||
});
|
||||
const runtimeOptions = {
|
||||
...runtimeGuestOptions,
|
||||
...runtimeRequestOptions,
|
||||
...(typeof options.itemTypeCountOverride === 'number'
|
||||
? { itemTypeCountOverride: options.itemTypeCountOverride }
|
||||
: {}),
|
||||
@@ -9097,6 +9173,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
[
|
||||
isMatch3DBusy,
|
||||
authUi,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
match3dFlow,
|
||||
resolveMatch3DErrorMessage,
|
||||
resolveMatch3DRuntimeAdapter,
|
||||
@@ -9120,12 +9197,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSquareHoleError(null);
|
||||
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'square-hole',
|
||||
embedded: options.embedded,
|
||||
});
|
||||
const { run } = options.embedded
|
||||
? await startSquareHoleRun(profile.profileId, runtimeGuestOptions)
|
||||
? await startSquareHoleRun(profile.profileId, runtimeRequestOptions)
|
||||
: await startSquareHoleRun(profile.profileId);
|
||||
setSquareHoleRun(run);
|
||||
setSquareHoleRuntimeReturnStage(returnStage);
|
||||
@@ -9157,7 +9235,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
},
|
||||
[
|
||||
isSquareHoleBusy,
|
||||
authUi,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
resolveSquareHoleErrorMessage,
|
||||
setSelectionStage,
|
||||
setSquareHoleError,
|
||||
@@ -9275,14 +9353,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
bigFishInputInFlightRef.current = true;
|
||||
try {
|
||||
const runtimeGuestOptions =
|
||||
activeRecommendRuntimeKind === 'big-fish'
|
||||
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: {};
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'big-fish',
|
||||
});
|
||||
const { run } = await submitBigFishRuntimeInput(
|
||||
bigFishRun.runId,
|
||||
payload,
|
||||
runtimeGuestOptions,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setBigFishRun(run);
|
||||
} catch (error) {
|
||||
@@ -9294,8 +9372,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
},
|
||||
[
|
||||
activeRecommendRuntimeKind,
|
||||
authUi,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
bigFishRun,
|
||||
resolveBigFishErrorMessage,
|
||||
setBigFishError,
|
||||
@@ -9310,21 +9387,18 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
const elapsedMs = Math.max(1_000, Date.now() - bigFishRuntimeStartedAt);
|
||||
setBigFishRuntimeStartedAt(null);
|
||||
const reportPromise =
|
||||
activeRecommendRuntimeKind === 'big-fish'
|
||||
? buildRecommendRuntimeAuthOptions(authUi, true).then(
|
||||
(runtimeAuthOptions) =>
|
||||
recordBigFishPlay(sessionId, { elapsedMs }, runtimeAuthOptions),
|
||||
)
|
||||
: recordBigFishPlay(sessionId, { elapsedMs });
|
||||
const reportPromise = buildRecommendRuntimeRequestOptions({
|
||||
kind: 'big-fish',
|
||||
}).then((runtimeRequestOptions) =>
|
||||
recordBigFishPlay(sessionId, { elapsedMs }, runtimeRequestOptions),
|
||||
);
|
||||
void reportPromise.catch((error) => {
|
||||
setBigFishError(
|
||||
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩时长失败。'),
|
||||
);
|
||||
});
|
||||
}, [
|
||||
activeRecommendRuntimeKind,
|
||||
authUi,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
bigFishRun?.sessionId,
|
||||
bigFishRuntimeStartedAt,
|
||||
resolveBigFishErrorMessage,
|
||||
@@ -9642,14 +9716,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
profileId: currentLevel.profileId,
|
||||
levelId: resolvePuzzleRestartLevelId(currentRun, detailItem),
|
||||
};
|
||||
const runtimeGuestOptions =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const { run } =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? await startPuzzleRun(startRunPayload, runtimeGuestOptions)
|
||||
: await startPuzzleRun(startRunPayload);
|
||||
const runtimeRequestOptions = await buildPuzzleRuntimeRequestOptions();
|
||||
const { run } = await startPuzzleRun(
|
||||
startRunPayload,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setSelectedPuzzleDetail(detailItem);
|
||||
puzzleRunRef.current = run;
|
||||
setPuzzleRun(run);
|
||||
@@ -9663,7 +9734,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}, [
|
||||
isPuzzleBusy,
|
||||
puzzleRun,
|
||||
puzzleRuntimeAuthMode,
|
||||
buildPuzzleRuntimeRequestOptions,
|
||||
resolvePuzzleErrorMessage,
|
||||
selectedPuzzleDetail,
|
||||
setIsPuzzleBusy,
|
||||
@@ -9775,16 +9846,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
const submitLeaderboardPromise =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? buildRecommendRuntimeGuestOptions().then((runtimeGuestOptions) =>
|
||||
submitPuzzleLeaderboard(
|
||||
puzzleRun.runId,
|
||||
payload,
|
||||
runtimeGuestOptions,
|
||||
),
|
||||
)
|
||||
: submitPuzzleLeaderboard(puzzleRun.runId, payload);
|
||||
const submitLeaderboardPromise = buildPuzzleRuntimeRequestOptions().then(
|
||||
(runtimeRequestOptions) =>
|
||||
submitPuzzleLeaderboard(puzzleRun.runId, payload, runtimeRequestOptions),
|
||||
);
|
||||
|
||||
void submitLeaderboardPromise
|
||||
.then(({ run }) => {
|
||||
@@ -9808,7 +9873,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
authUi?.user?.displayName,
|
||||
platformBootstrap,
|
||||
puzzleRun,
|
||||
puzzleRuntimeAuthMode,
|
||||
buildPuzzleRuntimeRequestOptions,
|
||||
resolvePuzzleErrorMessage,
|
||||
setPuzzleError,
|
||||
]);
|
||||
@@ -9838,10 +9903,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeGuestOptions =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const runtimeRequestOptions = await buildPuzzleRuntimeRequestOptions();
|
||||
const targetProfileId = _target?.profileId?.trim() ?? '';
|
||||
if (puzzleRun.nextLevelMode === 'similarWorks' && targetProfileId) {
|
||||
const itemPromise =
|
||||
@@ -9850,18 +9912,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
: getPuzzleGalleryDetail(targetProfileId).then(
|
||||
(response) => response.item,
|
||||
);
|
||||
const advancePromise =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? advancePuzzleNextLevel(
|
||||
puzzleRun.runId,
|
||||
{
|
||||
targetProfileId,
|
||||
},
|
||||
runtimeGuestOptions,
|
||||
)
|
||||
: advancePuzzleNextLevel(puzzleRun.runId, {
|
||||
targetProfileId,
|
||||
});
|
||||
const advancePromise = advancePuzzleNextLevel(
|
||||
puzzleRun.runId,
|
||||
{
|
||||
targetProfileId,
|
||||
},
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
const [{ run }, item] = await Promise.all([
|
||||
advancePromise,
|
||||
itemPromise,
|
||||
@@ -9878,14 +9935,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
const { run } =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? await advancePuzzleNextLevel(
|
||||
puzzleRun.runId,
|
||||
{},
|
||||
runtimeGuestOptions,
|
||||
)
|
||||
: await advancePuzzleNextLevel(puzzleRun.runId, {});
|
||||
const { run } = await advancePuzzleNextLevel(
|
||||
puzzleRun.runId,
|
||||
{},
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setPuzzleRun(run);
|
||||
} catch (error) {
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
|
||||
@@ -9898,7 +9952,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
isPuzzleBusy,
|
||||
isPuzzleLeaderboardBusy,
|
||||
puzzleRun,
|
||||
puzzleRuntimeAuthMode,
|
||||
buildPuzzleRuntimeRequestOptions,
|
||||
resolvePuzzleErrorMessage,
|
||||
selectedPuzzleDetail,
|
||||
setIsPuzzleBusy,
|
||||
@@ -12436,12 +12490,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
setBigFishRuntimeReturnStage(returnStage);
|
||||
setBigFishRun(null);
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'big-fish',
|
||||
embedded: options.embedded,
|
||||
});
|
||||
const { run } = options.embedded
|
||||
? await startBigFishRuntimeRun(sessionId, runtimeGuestOptions)
|
||||
? await startBigFishRuntimeRun(sessionId, runtimeRequestOptions)
|
||||
: await startBigFishRuntimeRun(sessionId);
|
||||
setBigFishRuntimeStartedAt(Date.now());
|
||||
setBigFishRun(run);
|
||||
@@ -12452,7 +12507,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
}
|
||||
const recordPlayPromise = options.embedded
|
||||
? recordBigFishPlay(sessionId, { elapsedMs: 0 }, runtimeGuestOptions)
|
||||
? recordBigFishPlay(sessionId, { elapsedMs: 0 }, runtimeRequestOptions)
|
||||
: recordBigFishPlay(sessionId, { elapsedMs: 0 });
|
||||
void recordPlayPromise.catch((error) => {
|
||||
setBigFishError(
|
||||
@@ -12468,7 +12523,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
},
|
||||
[
|
||||
authUi,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
bigFishFlow,
|
||||
resolveBigFishErrorMessage,
|
||||
setBigFishError,
|
||||
@@ -12495,12 +12550,18 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
setBarkBattleRuntimeReturnStage(returnStage);
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'bark-battle',
|
||||
embedded: options.embedded,
|
||||
});
|
||||
setBarkBattleRuntimeRequestOptions(
|
||||
Object.keys(runtimeRequestOptions).length > 0
|
||||
? runtimeRequestOptions
|
||||
: null,
|
||||
);
|
||||
const runResponse = options.embedded
|
||||
? await startBarkBattleRun(item.workId, {}, runtimeGuestOptions)
|
||||
? await startBarkBattleRun(item.workId, {}, runtimeRequestOptions)
|
||||
: await startBarkBattleRun(item.workId);
|
||||
void runResponse;
|
||||
selectionStageRef.current = 'bark-battle-runtime';
|
||||
@@ -12521,7 +12582,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[authUi, resolveBarkBattleErrorMessage, setSelectionStage],
|
||||
[
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
resolveBarkBattleErrorMessage,
|
||||
setSelectionStage,
|
||||
],
|
||||
);
|
||||
|
||||
const startSelectedPublicWork = useCallback(() => {
|
||||
@@ -12975,10 +13040,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
match3dFlow.setIsBusy(true);
|
||||
setMatch3DError(null);
|
||||
void resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
)
|
||||
.restartRun(match3dRun.runId)
|
||||
void buildRecommendRuntimeRequestOptions({ kind: 'match3d' })
|
||||
.then((runtimeRequestOptions) =>
|
||||
resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
).restartRun(match3dRun.runId, runtimeRequestOptions),
|
||||
)
|
||||
.then(({ run }) => {
|
||||
setMatch3DRun(run);
|
||||
})
|
||||
@@ -12992,24 +13059,28 @@ export function PlatformEntryFlowShellImpl({
|
||||
});
|
||||
}}
|
||||
onOptimisticRunChange={setMatch3DRun}
|
||||
onClickItem={(payload) => {
|
||||
onClickItem={async (payload) => {
|
||||
const runId = payload.runId ?? match3dRun?.runId;
|
||||
if (!runId) {
|
||||
return Promise.reject(new Error('抓大鹅运行态缺少 runId。'));
|
||||
}
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({ kind: 'match3d' });
|
||||
return resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
).clickItem(runId, payload);
|
||||
).clickItem(runId, payload, runtimeRequestOptions);
|
||||
}}
|
||||
onTimeExpired={() => {
|
||||
if (!match3dRun?.runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
void resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
)
|
||||
.finishTimeUp(match3dRun.runId)
|
||||
void buildRecommendRuntimeRequestOptions({ kind: 'match3d' })
|
||||
.then((runtimeRequestOptions) =>
|
||||
resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
).finishTimeUp(match3dRun.runId, runtimeRequestOptions),
|
||||
)
|
||||
.then(({ run }) => {
|
||||
setMatch3DRun(run);
|
||||
})
|
||||
@@ -13143,9 +13214,17 @@ export function PlatformEntryFlowShellImpl({
|
||||
squareHoleRun?.runId &&
|
||||
squareHoleRun.status.toLowerCase() === 'running'
|
||||
) {
|
||||
void stopSquareHoleRun(squareHoleRun.runId).catch(
|
||||
() => undefined,
|
||||
);
|
||||
void buildRecommendRuntimeRequestOptions({
|
||||
kind: 'square-hole',
|
||||
})
|
||||
.then((runtimeRequestOptions) =>
|
||||
stopSquareHoleRun(
|
||||
squareHoleRun.runId,
|
||||
undefined,
|
||||
runtimeRequestOptions,
|
||||
),
|
||||
)
|
||||
.catch(() => undefined);
|
||||
}
|
||||
setActiveRecommendRuntimeKind(null);
|
||||
}}
|
||||
@@ -13156,7 +13235,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
squareHoleFlow.setIsBusy(true);
|
||||
setSquareHoleError(null);
|
||||
void restartSquareHoleRun(squareHoleRun.runId)
|
||||
void buildRecommendRuntimeRequestOptions({
|
||||
kind: 'square-hole',
|
||||
})
|
||||
.then((runtimeRequestOptions) =>
|
||||
restartSquareHoleRun(squareHoleRun.runId, runtimeRequestOptions),
|
||||
)
|
||||
.then(({ run }) => {
|
||||
setSquareHoleRun(run);
|
||||
})
|
||||
@@ -13178,14 +13262,26 @@ export function PlatformEntryFlowShellImpl({
|
||||
if (!runId) {
|
||||
return Promise.reject(new Error('方洞挑战运行态缺少 runId。'));
|
||||
}
|
||||
return dropSquareHoleShape(runId, payload);
|
||||
return buildRecommendRuntimeRequestOptions({
|
||||
kind: 'square-hole',
|
||||
}).then((runtimeRequestOptions) =>
|
||||
dropSquareHoleShape(runId, payload, runtimeRequestOptions),
|
||||
);
|
||||
}}
|
||||
onTimeExpired={() => {
|
||||
if (!squareHoleRun?.runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
void finishSquareHoleTimeUp(squareHoleRun.runId)
|
||||
void buildRecommendRuntimeRequestOptions({
|
||||
kind: 'square-hole',
|
||||
})
|
||||
.then((runtimeRequestOptions) =>
|
||||
finishSquareHoleTimeUp(
|
||||
squareHoleRun.runId,
|
||||
runtimeRequestOptions,
|
||||
),
|
||||
)
|
||||
.then(({ run }) => {
|
||||
setSquareHoleRun(run);
|
||||
})
|
||||
@@ -13230,6 +13326,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
workId={barkBattlePublishedConfig.workId}
|
||||
publishedConfig={barkBattlePublishedConfig}
|
||||
runtimeMode="published"
|
||||
runtimeRequestOptions={barkBattleRuntimeRequestOptions ?? undefined}
|
||||
onExit={() => {
|
||||
setActiveRecommendRuntimeKind(null);
|
||||
}}
|
||||
@@ -13260,7 +13357,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
activeRecommendEntryKey,
|
||||
activeRecommendRuntimeKind,
|
||||
barkBattlePublishedConfig,
|
||||
barkBattleRuntimeRequestOptions,
|
||||
babyObjectMatchDraft,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
bigFishError,
|
||||
bigFishRun,
|
||||
bigFishRuntimeShare,
|
||||
@@ -13297,6 +13396,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
recommendRuntimeEntries,
|
||||
remodelCurrentPuzzleRuntimeWork,
|
||||
resolveMatch3DErrorMessage,
|
||||
resolveMatch3DRuntimeAdapter,
|
||||
resolveSquareHoleErrorMessage,
|
||||
reportBigFishObservedPlayTime,
|
||||
restartBigFishRun,
|
||||
@@ -16663,6 +16763,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
workId={barkBattlePublishedConfig.workId}
|
||||
publishedConfig={barkBattlePublishedConfig}
|
||||
runtimeMode={barkBattleRuntimeMode}
|
||||
runtimeRequestOptions={
|
||||
barkBattleRuntimeRequestOptions ?? undefined
|
||||
}
|
||||
onExit={() => {
|
||||
if (
|
||||
barkBattleRuntimeReturnStage === 'bark-battle-result' &&
|
||||
|
||||
@@ -724,7 +724,7 @@ test('顶部不显示作者,关卡标题和倒计时使用游戏铭牌结构',
|
||||
const levelLogo = screen.getByTestId(
|
||||
'puzzle-runtime-level-logo',
|
||||
) as HTMLImageElement;
|
||||
expect(levelLogo.getAttribute('src')).toContain('logo.png');
|
||||
expect(levelLogo.getAttribute('src')).toContain('logo-runtime-hud.webp');
|
||||
expect(levelLogo.closest('.puzzle-runtime-level-logo')).toBeTruthy();
|
||||
expect(document.querySelector('.puzzle-runtime-level-mascot')).toBeNull();
|
||||
expect(timer.closest('.puzzle-runtime-timer-card')).toBeTruthy();
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import puzzleLevelLogo from '../../../media/logo.png';
|
||||
import puzzleLevelLogo from '../../../media/logo-runtime-hud.webp';
|
||||
import type {
|
||||
DragPuzzlePieceRequest,
|
||||
PuzzleBoardSnapshot,
|
||||
|
||||
@@ -7810,6 +7810,113 @@ test('logged out home recommendation next starts the next puzzle work', async ()
|
||||
});
|
||||
});
|
||||
|
||||
test('home recommendation next follows the same scored queue shown in preview', async () => {
|
||||
const user = userEvent.setup();
|
||||
const quietWork = {
|
||||
workId: 'puzzle-work-public-quiet',
|
||||
profileId: 'puzzle-profile-public-quiet',
|
||||
ownerUserId: 'user-2',
|
||||
sourceSessionId: 'puzzle-session-public-quiet',
|
||||
authorDisplayName: '拼图作者',
|
||||
levelName: '安静拼图',
|
||||
summary: '列表里排在前面但热度较低。',
|
||||
themeTags: ['安静', '拼图'],
|
||||
coverImageSrc: null,
|
||||
coverAssetId: null,
|
||||
publicationStatus: 'published',
|
||||
updatedAt: '2026-04-25T10:00:00.000Z',
|
||||
publishedAt: '2026-04-25T10:00:00.000Z',
|
||||
playCount: 40,
|
||||
likeCount: 0,
|
||||
publishReady: true,
|
||||
} satisfies PuzzleWorkSummary;
|
||||
const hotWork = {
|
||||
...quietWork,
|
||||
workId: 'puzzle-work-public-hot',
|
||||
profileId: 'puzzle-profile-public-hot',
|
||||
sourceSessionId: 'puzzle-session-public-hot',
|
||||
levelName: '热门拼图',
|
||||
summary: '推荐评分更高,应该先展示。',
|
||||
playCount: 120,
|
||||
updatedAt: '2026-04-25T09:00:00.000Z',
|
||||
publishedAt: '2026-04-25T09:00:00.000Z',
|
||||
} satisfies PuzzleWorkSummary;
|
||||
const middleWork = {
|
||||
...quietWork,
|
||||
workId: 'puzzle-work-public-middle',
|
||||
profileId: 'puzzle-profile-public-middle',
|
||||
sourceSessionId: 'puzzle-session-public-middle',
|
||||
levelName: '中间拼图',
|
||||
summary: '推荐评分排在后面。',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-04-25T08:00:00.000Z',
|
||||
publishedAt: '2026-04-25T08:00:00.000Z',
|
||||
} satisfies PuzzleWorkSummary;
|
||||
|
||||
vi.mocked(listPuzzleGallery).mockResolvedValue({
|
||||
items: [quietWork, hotWork, middleWork],
|
||||
});
|
||||
vi.mocked(getPuzzleGalleryDetail).mockImplementation(async (profileId) => ({
|
||||
item:
|
||||
profileId === hotWork.profileId
|
||||
? hotWork
|
||||
: profileId === middleWork.profileId
|
||||
? middleWork
|
||||
: quietWork,
|
||||
}));
|
||||
vi.mocked(startPuzzleRun).mockImplementation(async (payload) => ({
|
||||
run: buildMockPuzzleRun(
|
||||
payload.profileId,
|
||||
payload.profileId === hotWork.profileId
|
||||
? hotWork.levelName
|
||||
: payload.profileId === middleWork.profileId
|
||||
? middleWork.levelName
|
||||
: quietWork.levelName,
|
||||
),
|
||||
}));
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(startPuzzleRun).toHaveBeenCalledWith(
|
||||
{
|
||||
profileId: hotWork.profileId,
|
||||
levelId: null,
|
||||
},
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
await screen.findByLabelText('热门拼图 作品信息', undefined, {
|
||||
timeout: 3000,
|
||||
}),
|
||||
).toBeTruthy();
|
||||
const nextPreview = document.querySelector(
|
||||
'.platform-recommend-swipe-page--next',
|
||||
);
|
||||
expect(nextPreview).toBeTruthy();
|
||||
expect(
|
||||
within(nextPreview as HTMLElement).getByLabelText('安静拼图 作品信息'),
|
||||
).toBeTruthy();
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: '下一个' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(startPuzzleRun).toHaveBeenCalledWith(
|
||||
{
|
||||
profileId: quietWork.profileId,
|
||||
levelId: null,
|
||||
},
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
await screen.findByLabelText('安静拼图 作品信息', undefined, {
|
||||
timeout: 3000,
|
||||
}),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('home recommendation keeps cover while switching during a pending puzzle start', async () => {
|
||||
const user = userEvent.setup();
|
||||
const firstWork = {
|
||||
@@ -8142,6 +8249,54 @@ test('home recommendation Match3D runtime keeps profile generated models when ca
|
||||
});
|
||||
});
|
||||
|
||||
test('logged out home recommendation Match3D runtime skips protected detail and starts with guest auth', async () => {
|
||||
const match3dCard: Match3DWorkSummary = {
|
||||
workId: 'match3d-work-card-guest',
|
||||
profileId: 'match3d-profile-card-guest',
|
||||
ownerUserId: 'user-2',
|
||||
sourceSessionId: 'match3d-session-card-guest',
|
||||
gameName: '游客抓大鹅',
|
||||
themeText: '游客果园',
|
||||
summary: '游客可直接游玩。',
|
||||
tags: ['果园', '抓大鹅'],
|
||||
coverImageSrc: null,
|
||||
referenceImageSrc: null,
|
||||
clearCount: 3,
|
||||
difficulty: 5,
|
||||
publicationStatus: 'published',
|
||||
playCount: 3,
|
||||
updatedAt: '2026-04-25T10:30:00.000Z',
|
||||
publishedAt: '2026-04-25T10:30:00.000Z',
|
||||
publishReady: true,
|
||||
generatedItemAssets: [],
|
||||
};
|
||||
|
||||
vi.mocked(listMatch3DGallery).mockResolvedValue({
|
||||
items: [match3dCard],
|
||||
});
|
||||
vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
|
||||
item: match3dCard,
|
||||
});
|
||||
match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
|
||||
run: buildMockMatch3DRun(match3dCard.profileId),
|
||||
});
|
||||
|
||||
render(<TestWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
|
||||
'match3d-profile-card-guest',
|
||||
expect.objectContaining({
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
skipRefresh: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(getMatch3DWorkDetail).not.toHaveBeenCalledWith(
|
||||
'match3d-profile-card-guest',
|
||||
);
|
||||
});
|
||||
|
||||
test('home recommendation Match3D runtime keeps image, music and UI assets without requiring models', async () => {
|
||||
const match3dCard: Match3DWorkSummary = {
|
||||
workId: 'match3d-work-card-image-only',
|
||||
@@ -9357,6 +9512,7 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
||||
elapsedMs: 18_000,
|
||||
nickname: '测试玩家',
|
||||
},
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9377,6 +9533,7 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
||||
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
|
||||
clearedFirstLevel.runId,
|
||||
{},
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
@@ -9539,6 +9696,7 @@ test('formal puzzle similar work keeps current run level progression', async ()
|
||||
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
|
||||
clearedThirdLevel.runId,
|
||||
{ targetProfileId: 'puzzle-profile-similar-2' },
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(startPuzzleRun).not.toHaveBeenCalled();
|
||||
|
||||
@@ -189,6 +189,27 @@ test('public gallery ViewModel builds recommend feed from general public entries
|
||||
).toEqual([latestPuzzle]);
|
||||
});
|
||||
|
||||
test('public gallery ViewModel keeps recommend feed in scored runtime order', () => {
|
||||
const quietPuzzle = buildPuzzleEntry({
|
||||
profileId: 'quiet',
|
||||
worldName: '安静拼图',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-03T00:00:00.000Z',
|
||||
publishedAt: '2026-05-03T00:00:00.000Z',
|
||||
});
|
||||
const hotPuzzle = buildPuzzleEntry({
|
||||
profileId: 'hot',
|
||||
worldName: '热门拼图',
|
||||
playCount: 120,
|
||||
updatedAt: '2026-05-02T00:00:00.000Z',
|
||||
publishedAt: '2026-05-02T00:00:00.000Z',
|
||||
});
|
||||
|
||||
expect(buildPlatformRecommendFeedEntries([], [quietPuzzle, hotPuzzle])).toEqual(
|
||||
[hotPuzzle, quietPuzzle],
|
||||
);
|
||||
});
|
||||
|
||||
test('public gallery ViewModel selects recommend feed window with wraparound neighbors', () => {
|
||||
const firstEntry = buildPuzzleEntry({ profileId: 'first' });
|
||||
const secondEntry = buildJumpHopEntry({ profileId: 'second' });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { filterGeneralPublicWorks } from '../platform-entry/platformEdutainmentVisibility';
|
||||
import { getPlatformPublicGalleryEntryKey } from '../platform-entry/platformPublicGalleryFlow';
|
||||
import { buildPlatformRecommendedEntries } from '../platform-entry/platformRecommendation';
|
||||
import {
|
||||
buildPlatformWorldDisplayTags,
|
||||
isBarkBattleGalleryEntry,
|
||||
@@ -146,9 +147,10 @@ export function buildPlatformRecommendFeedEntries(
|
||||
featuredEntries: PlatformPublicGalleryCard[],
|
||||
latestEntries: PlatformPublicGalleryCard[],
|
||||
) {
|
||||
return dedupePlatformPublicGalleryEntries(
|
||||
filterGeneralPublicWorks([...featuredEntries, ...latestEntries]),
|
||||
);
|
||||
return buildPlatformRecommendedEntries({
|
||||
featuredEntries: filterGeneralPublicWorks(featuredEntries),
|
||||
latestEntries: filterGeneralPublicWorks(latestEntries),
|
||||
});
|
||||
}
|
||||
|
||||
export function selectAdjacentPlatformRecommendEntry(
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import woodenFishRuntimeLogo from '../../../media/logo.png';
|
||||
import woodenFishRuntimeLogo from '../../../media/logo-runtime-hud.webp';
|
||||
import type {
|
||||
WoodenFishRuntimeRunSnapshotResponse,
|
||||
WoodenFishWordCounter,
|
||||
|
||||
Reference in New Issue
Block a user