fix: stabilize match3d demo discovery
This commit is contained in:
@@ -1,13 +1,6 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { useEffect } from 'react';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
|
||||
@@ -235,35 +228,58 @@ test('顶部 HUD 对齐拼图样式展示关卡名和倒计时', () => {
|
||||
expect(screen.getByText('第 1 关')).toBeTruthy();
|
||||
expect(screen.getByText('水果抓大鹅')).toBeTruthy();
|
||||
expect(screen.getByText('10:00')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '打开抓大鹅设置' })).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: '重新开始' })).toBeNull();
|
||||
expect(screen.getByTestId('match3d-runtime-level-logo')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText('水果抓大鹅').closest('.puzzle-runtime-level-title-card'),
|
||||
).toBeTruthy();
|
||||
const timerCard = screen.getByText('10:00').closest('.puzzle-runtime-timer-card');
|
||||
expect(timerCard).toBeTruthy();
|
||||
expect(timerCard?.className).toContain('puzzle-runtime-timer');
|
||||
expect(screen.queryByRole('button', { name: '打开抓大鹅设置' })).toBeNull();
|
||||
expect(screen.getByRole('button', { name: '返回' })).toBeTruthy();
|
||||
expect(screen.queryByTestId('match3d-ui-sprite-settings')).toBeNull();
|
||||
});
|
||||
|
||||
test('抓大鹅右上角设置面板内置重新开始', () => {
|
||||
const run = startLocalMatch3DRun(4);
|
||||
const onRestart = vi.fn();
|
||||
test('抓大鹅运行态不再渲染设置入口', () => {
|
||||
render(
|
||||
<Match3DRuntimeShell
|
||||
run={run}
|
||||
levelName="水果抓大鹅"
|
||||
run={startLocalMatch3DRun(4)}
|
||||
onBack={vi.fn()}
|
||||
onRestart={onRestart}
|
||||
onRestart={vi.fn()}
|
||||
onOptimisticRunChange={vi.fn()}
|
||||
onClickItem={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开抓大鹅设置' }));
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '抓大鹅设置' });
|
||||
expect(within(dialog).getByText('水果抓大鹅')).toBeTruthy();
|
||||
expect(within(dialog).getByText('已清除 0/12')).toBeTruthy();
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '重新开始' }));
|
||||
|
||||
expect(onRestart).toHaveBeenCalledTimes(1);
|
||||
expect(screen.queryByRole('button', { name: '打开抓大鹅设置' })).toBeNull();
|
||||
expect(screen.queryByRole('dialog', { name: '抓大鹅设置' })).toBeNull();
|
||||
});
|
||||
|
||||
test('抓大鹅顶部和底部保留交互边界但不显示旧半透底', () => {
|
||||
render(
|
||||
<Match3DRuntimeShell
|
||||
run={startLocalMatch3DRun(4)}
|
||||
onBack={vi.fn()}
|
||||
onRestart={vi.fn()}
|
||||
onOptimisticRunChange={vi.fn()}
|
||||
onClickItem={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('match3d-board').className).toContain(
|
||||
'bg-transparent',
|
||||
);
|
||||
expect(screen.getAllByTestId('match3d-tray-slot')[0]!.className).toContain(
|
||||
'bg-transparent',
|
||||
);
|
||||
expect(
|
||||
screen.getByRole('button', { name: '移出' }).className,
|
||||
).toContain('bg-transparent');
|
||||
expect(screen.getByRole('button', { name: '返回' }).className).toContain(
|
||||
'bg-transparent',
|
||||
);
|
||||
});
|
||||
|
||||
test('推荐页抓大鹅运行态隐藏返回按钮和结算返回入口', () => {
|
||||
const run: Match3DRunSnapshot = {
|
||||
...startLocalMatch3DRun(4),
|
||||
@@ -1548,9 +1564,7 @@ test('运行态从UI spritesheet裁切按钮并映射到原UI位置', async () =
|
||||
expect(
|
||||
screen.getByTestId('match3d-ui-sprite-back').getAttribute('src'),
|
||||
).toBe('data:image/png;base64,返回');
|
||||
expect(
|
||||
screen.getByTestId('match3d-ui-sprite-settings').getAttribute('src'),
|
||||
).toBe('data:image/png;base64,设置');
|
||||
expect(screen.queryByTestId('match3d-ui-sprite-settings')).toBeNull();
|
||||
expect(
|
||||
screen.getByTestId('match3d-ui-sprite-prop-remove').getAttribute('src'),
|
||||
).toBe('data:image/png;base64,移出');
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {
|
||||
ArrowLeft,
|
||||
CheckCircle2,
|
||||
Clock3,
|
||||
RotateCcw,
|
||||
Settings,
|
||||
Clock,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
@@ -17,6 +15,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import match3DRuntimeLevelLogo from '../../../media/logo.png';
|
||||
import type {
|
||||
Match3DClickItemRequest,
|
||||
Match3DClickItemResult,
|
||||
@@ -71,19 +70,9 @@ import {
|
||||
} from './match3dRuntimePresentation';
|
||||
import {
|
||||
MATCH3D_RUNTIME_BOARD_BASE_CLASS,
|
||||
MATCH3D_RUNTIME_BOARD_FALLBACK_CLASS,
|
||||
MATCH3D_RUNTIME_BOARD_WIDTH,
|
||||
MATCH3D_RUNTIME_BOARD_WITH_CONTAINER_CLASS,
|
||||
MATCH3D_RUNTIME_CONTAINER_IMAGE_CLASS,
|
||||
MATCH3D_RUNTIME_CONTAINER_PLACEHOLDER_CLASS,
|
||||
MATCH3D_RUNTIME_GLASS_ICON_BUTTON_CLASS,
|
||||
MATCH3D_RUNTIME_GLASS_TRAY_CLASS,
|
||||
MATCH3D_RUNTIME_GLASS_TRAY_SLOT_CLASS,
|
||||
MATCH3D_RUNTIME_HEADER_CARD_CLASS,
|
||||
MATCH3D_RUNTIME_LEVEL_BADGE_CLASS,
|
||||
MATCH3D_RUNTIME_STAGE_CLASS,
|
||||
MATCH3D_RUNTIME_TIMER_CLASS,
|
||||
MATCH3D_RUNTIME_TIMER_URGENT_CLASS,
|
||||
} from './match3dRuntimeUiStyles';
|
||||
import { Match3DVisualIcon, resolveVisualSeed } from './match3dVisualAssets';
|
||||
|
||||
@@ -769,7 +758,7 @@ function Match3DTrayToken({
|
||||
}) {
|
||||
if (!slot.visualKey) {
|
||||
return (
|
||||
<span className="h-full w-full rounded-xl border border-dashed border-slate-300/35 bg-white/8" />
|
||||
<span className="h-full w-full rounded-none border border-dashed border-white/18 bg-transparent" />
|
||||
);
|
||||
}
|
||||
const visualSeed = resolveVisualSeed(slot.visualKey);
|
||||
@@ -1030,7 +1019,6 @@ export function Match3DRuntimeShell({
|
||||
const [timeLeftMs, setTimeLeftMs] = useState(run?.remainingMs ?? 0);
|
||||
const [resolvedBackgroundImageSrc, setResolvedBackgroundImageSrc] =
|
||||
useState('');
|
||||
const [isSettingsPanelOpen, setIsSettingsPanelOpen] = useState(false);
|
||||
const musicVolume = authUi?.musicVolume ?? DEFAULT_MATCH3D_MUSIC_VOLUME;
|
||||
const levelAudioConfig = DEFAULT_RUNTIME_LEVEL_AUDIO_CONFIG;
|
||||
const runtimeGeneratedItemAssets = useMemo(
|
||||
@@ -1366,10 +1354,6 @@ export function Match3DRuntimeShell({
|
||||
useState('');
|
||||
const [resolvedContainerImageSrc, setResolvedContainerImageSrc] =
|
||||
useState('');
|
||||
const [isContainerImageLoaded, setIsContainerImageLoaded] = useState(false);
|
||||
const hasRenderedContainerAsset = Boolean(
|
||||
resolvedContainerImageSrc && isContainerImageLoaded,
|
||||
);
|
||||
const clickSoundByTypeId = useMemo(() => {
|
||||
if (!run) {
|
||||
return new Map<string, string>();
|
||||
@@ -1489,7 +1473,6 @@ export function Match3DRuntimeShell({
|
||||
let cancelled = false;
|
||||
const controller = new AbortController();
|
||||
setResolvedContainerImageSrc('');
|
||||
setIsContainerImageLoaded(false);
|
||||
if (!isGeneratedLegacyPath(containerAssetSrc)) {
|
||||
setResolvedContainerImageSrc(containerAssetSrc);
|
||||
return undefined;
|
||||
@@ -1501,7 +1484,6 @@ export function Match3DRuntimeShell({
|
||||
.then((resolvedSrc) => {
|
||||
if (!cancelled) {
|
||||
setResolvedContainerImageSrc(resolvedSrc);
|
||||
setIsContainerImageLoaded(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -1511,7 +1493,6 @@ export function Match3DRuntimeShell({
|
||||
? ''
|
||||
: MATCH3D_CONTAINER_REFERENCE_SRC,
|
||||
);
|
||||
setIsContainerImageLoaded(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1937,9 +1918,8 @@ export function Match3DRuntimeShell({
|
||||
const timerClassName =
|
||||
timeLeftMs <= levelAudioConfig.countdownWarningThresholdMs &&
|
||||
isRunState(run.status, 'running')
|
||||
? MATCH3D_RUNTIME_TIMER_URGENT_CLASS
|
||||
: MATCH3D_RUNTIME_TIMER_CLASS;
|
||||
const canRestartRun = Boolean(run?.runId) && !isBusy;
|
||||
? 'puzzle-runtime-timer--urgent'
|
||||
: 'puzzle-runtime-timer';
|
||||
|
||||
return (
|
||||
<main
|
||||
@@ -1978,7 +1958,7 @@ export function Match3DRuntimeShell({
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={MATCH3D_RUNTIME_GLASS_ICON_BUTTON_CLASS}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full border border-transparent bg-transparent text-white shadow-none transition hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30 sm:h-11 sm:w-11"
|
||||
onClick={onBack}
|
||||
aria-label="返回"
|
||||
>
|
||||
@@ -1992,43 +1972,38 @@ export function Match3DRuntimeShell({
|
||||
) : null}
|
||||
</button>
|
||||
)}
|
||||
<div className={`${MATCH3D_RUNTIME_HEADER_CARD_CLASS} mx-auto`}>
|
||||
<div className="flex max-w-full items-center justify-center gap-1.5">
|
||||
<span className={MATCH3D_RUNTIME_LEVEL_BADGE_CLASS}>第 1 关</span>
|
||||
<span className="min-w-0 truncate text-sm font-black sm:text-base">
|
||||
<div className="puzzle-runtime-header-card mx-auto flex max-w-[min(18.5rem,calc(100vw_-_6.5rem))] min-w-0 flex-col items-center text-center sm:max-w-[22rem]">
|
||||
<div className="puzzle-runtime-level-title-card flex max-w-full items-center justify-center gap-2 px-3.5 py-1.5 pr-4 sm:px-4 sm:pr-5">
|
||||
<span aria-hidden="true" className="puzzle-runtime-level-logo">
|
||||
<img
|
||||
src={match3DRuntimeLevelLogo}
|
||||
alt=""
|
||||
data-testid="match3d-runtime-level-logo"
|
||||
className="puzzle-runtime-level-logo__image"
|
||||
draggable={false}
|
||||
/>
|
||||
</span>
|
||||
<span className="puzzle-runtime-level-badge shrink-0 text-[0.92rem] font-black sm:text-base">
|
||||
第 1 关
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-[0.92rem] font-black sm:text-base">
|
||||
{displayLevelName}
|
||||
</span>
|
||||
</div>
|
||||
<div className={timerClassName}>
|
||||
<Clock3 className="h-4 w-4 sm:h-5 sm:w-5" />
|
||||
<div
|
||||
className={`puzzle-runtime-timer-card -mt-px inline-flex items-center gap-1.5 px-3.5 py-1.5 font-mono text-lg font-black leading-none sm:text-xl ${timerClassName}`}
|
||||
>
|
||||
<Clock className="h-4 w-4 sm:h-5 sm:w-5" />
|
||||
{formatTimer(timeLeftMs)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={MATCH3D_RUNTIME_GLASS_ICON_BUTTON_CLASS}
|
||||
onClick={() => setIsSettingsPanelOpen(true)}
|
||||
aria-label="打开抓大鹅设置"
|
||||
>
|
||||
<Match3DSpriteImage
|
||||
region={uiSpritesheetRegionByLabel.get('设置')}
|
||||
testId="match3d-ui-sprite-settings"
|
||||
className="h-7 w-7 object-contain"
|
||||
/>
|
||||
{!uiSpritesheetRegionByLabel.get('设置') ? (
|
||||
<Settings size={18} />
|
||||
) : null}
|
||||
</button>
|
||||
<div aria-hidden="true" />
|
||||
</header>
|
||||
|
||||
<section className={MATCH3D_RUNTIME_STAGE_CLASS}>
|
||||
<div
|
||||
ref={stageRef}
|
||||
className={`${MATCH3D_RUNTIME_BOARD_BASE_CLASS} ${
|
||||
hasRenderedContainerAsset
|
||||
? MATCH3D_RUNTIME_BOARD_WITH_CONTAINER_CLASS
|
||||
: MATCH3D_RUNTIME_BOARD_FALLBACK_CLASS
|
||||
}`}
|
||||
className={`${MATCH3D_RUNTIME_BOARD_BASE_CLASS} overflow-hidden rounded-[50%] border border-white/14 bg-transparent shadow-[inset_0_0_0_1px_rgba(255,255,255,0.08)]`}
|
||||
style={{
|
||||
width: MATCH3D_RUNTIME_BOARD_WIDTH,
|
||||
}}
|
||||
@@ -2043,22 +2018,11 @@ export function Match3DRuntimeShell({
|
||||
src={resolvedContainerImageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className={`${MATCH3D_RUNTIME_CONTAINER_IMAGE_CLASS} ${
|
||||
isContainerImageLoaded ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
className={`${MATCH3D_RUNTIME_CONTAINER_IMAGE_CLASS} opacity-0`}
|
||||
data-testid="match3d-container-image"
|
||||
onLoad={() => setIsContainerImageLoaded(true)}
|
||||
onError={() => {
|
||||
setIsContainerImageLoaded(false);
|
||||
setResolvedContainerImageSrc((currentSrc) =>
|
||||
currentSrc && currentSrc !== MATCH3D_CONTAINER_REFERENCE_SRC
|
||||
? MATCH3D_CONTAINER_REFERENCE_SRC
|
||||
: '',
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className={MATCH3D_RUNTIME_CONTAINER_PLACEHOLDER_CLASS} />
|
||||
<div className="pointer-events-none absolute inset-0 z-0 rounded-full border border-white/10 bg-transparent" />
|
||||
)}
|
||||
{run.items.map((item) =>
|
||||
hasPendingMatch3DGeneratedImageForItem(
|
||||
@@ -2091,7 +2055,7 @@ export function Match3DRuntimeShell({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={MATCH3D_RUNTIME_GLASS_TRAY_CLASS}>
|
||||
<section className="mt-3 w-full min-w-0">
|
||||
<div
|
||||
className="relative grid grid-cols-7 gap-1.5"
|
||||
data-testid="match3d-tray"
|
||||
@@ -2107,7 +2071,7 @@ export function Match3DRuntimeShell({
|
||||
return (
|
||||
<div
|
||||
key={slot.slotIndex}
|
||||
className={MATCH3D_RUNTIME_GLASS_TRAY_SLOT_CLASS}
|
||||
className="relative z-0 h-14 min-w-0 rounded-none border border-transparent bg-transparent p-0 sm:h-16"
|
||||
data-testid="match3d-tray-slot"
|
||||
data-slot-index={slot.slotIndex}
|
||||
ref={(element) => {
|
||||
@@ -2170,7 +2134,7 @@ export function Match3DRuntimeShell({
|
||||
<button
|
||||
key={label}
|
||||
type="button"
|
||||
className="flex min-h-12 items-center justify-center overflow-hidden rounded-[1rem] border border-white/58 bg-white/50 px-2 py-2 text-sm font-black text-slate-800 shadow-[0_10px_24px_rgba(15,23,42,0.14)] backdrop-blur-md"
|
||||
className="flex min-h-12 items-center justify-center overflow-hidden rounded-none border border-transparent bg-transparent px-1 py-2 text-sm font-black text-white shadow-none transition hover:bg-white/10"
|
||||
aria-label={label}
|
||||
>
|
||||
<Match3DSpriteImage
|
||||
@@ -2219,84 +2183,6 @@ export function Match3DRuntimeShell({
|
||||
onBack={onBack}
|
||||
onRestart={onRestart}
|
||||
/>
|
||||
|
||||
{isSettingsPanelOpen ? (
|
||||
<div
|
||||
className="absolute inset-0 z-[85] flex items-center justify-center bg-slate-950/42 px-4 py-6 backdrop-blur-sm"
|
||||
onClick={() => setIsSettingsPanelOpen(false)}
|
||||
>
|
||||
<section
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="match3d-settings-title"
|
||||
className="w-full max-w-[20.5rem] overflow-hidden rounded-[1.35rem] border border-white/18 bg-white/95 text-slate-950 shadow-[0_26px_70px_rgba(15,23,42,0.34)]"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<header className="border-b border-slate-200 px-5 py-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h2
|
||||
id="match3d-settings-title"
|
||||
className="text-base font-black"
|
||||
>
|
||||
抓大鹅设置
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="关闭设置"
|
||||
className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-600 transition hover:bg-slate-50 hover:text-slate-900"
|
||||
onClick={() => setIsSettingsPanelOpen(false)}
|
||||
>
|
||||
<XCircle size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="space-y-3 px-5 py-4">
|
||||
<div className="rounded-[1rem] border border-slate-200 bg-slate-50 px-4 py-3">
|
||||
<div className="text-[10px] font-bold uppercase tracking-[0.16em] text-slate-500">
|
||||
当前进度
|
||||
</div>
|
||||
<div className="mt-2 text-sm font-black text-slate-900">
|
||||
{displayLevelName}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-500">
|
||||
已清除 {run.clearedItemCount}/{run.totalItemCount}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[1rem] border border-slate-200 bg-slate-50 px-4 py-3">
|
||||
<div className="text-[10px] font-bold uppercase tracking-[0.16em] text-slate-500">
|
||||
本局时间
|
||||
</div>
|
||||
<div className="mt-2 font-mono text-xl font-black text-slate-900">
|
||||
{formatTimer(timeLeftMs)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer className="grid gap-3 border-t border-slate-200 px-5 py-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex min-h-12 items-center justify-center gap-2 rounded-[1rem] bg-slate-950 px-4 py-3 text-sm font-black text-white transition hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
disabled={!canRestartRun}
|
||||
onClick={() => {
|
||||
setIsSettingsPanelOpen(false);
|
||||
onRestart();
|
||||
}}
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
重新开始
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex min-h-12 items-center justify-center rounded-[1rem] border border-slate-200 bg-white px-4 py-3 text-sm font-bold text-slate-700 transition hover:bg-slate-50"
|
||||
onClick={() => setIsSettingsPanelOpen(false)}
|
||||
>
|
||||
继续抓大鹅
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,6 +107,12 @@ import type {
|
||||
VisualNovelWorkSummary,
|
||||
} from '../../../packages/shared/src/contracts/visualNovel';
|
||||
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
||||
import {
|
||||
MATCH3D_DEMO_GALLERY_CARD,
|
||||
MATCH3D_DEMO_PROFILE_ID,
|
||||
MATCH3D_DEMO_WORK_PROFILE,
|
||||
isMatch3DDemoProfileId,
|
||||
} from '../../data/match3dDemoGalleryCard';
|
||||
import {
|
||||
buildPublicWorkStagePath,
|
||||
pushAppHistoryPath,
|
||||
@@ -181,7 +187,10 @@ import {
|
||||
type JumpHopWorkspaceCreateRequest,
|
||||
} from '../../services/jump-hop/jumpHopClient';
|
||||
import { match3dCreationClient } from '../../services/match3d-creation';
|
||||
import { createServerMatch3DRuntimeAdapter } from '../../services/match3d-runtime';
|
||||
import {
|
||||
createLocalMatch3DRuntimeAdapter,
|
||||
createServerMatch3DRuntimeAdapter,
|
||||
} from '../../services/match3d-runtime';
|
||||
import {
|
||||
deleteMatch3DWork,
|
||||
getMatch3DWorkDetail,
|
||||
@@ -3096,6 +3105,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSelectedDetailEntry,
|
||||
});
|
||||
const { setPlatformTab } = platformBootstrap;
|
||||
const returnPlatformHomeAfterMissingWork = useCallback(() => {
|
||||
setPlatformTab('home');
|
||||
setSelectionStage('platform');
|
||||
if (!maybeAlertWorkNotFoundAndReturnHome()) {
|
||||
pushAppHistoryPath('/');
|
||||
}
|
||||
}, [setPlatformTab, setSelectionStage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectionStage === 'profile-feedback') {
|
||||
@@ -3711,6 +3727,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
return '服务端预览';
|
||||
}, [agentResultPreview]);
|
||||
const match3dDemoProfile = MATCH3D_DEMO_WORK_PROFILE;
|
||||
const match3dDemoGalleryCard = MATCH3D_DEMO_GALLERY_CARD;
|
||||
|
||||
const featuredGalleryEntries = useMemo(() => {
|
||||
const bigFishPublicEntries = isBigFishCreationVisible
|
||||
@@ -3750,6 +3768,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
[
|
||||
...bigFishPublicEntries,
|
||||
...match3dPublicEntries,
|
||||
match3dDemoGalleryCard,
|
||||
...puzzlePublicEntries,
|
||||
...barkBattlePublicEntries,
|
||||
...squareHolePublicEntries,
|
||||
@@ -3774,6 +3793,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
squareHoleGalleryEntries,
|
||||
visualNovelGalleryEntries,
|
||||
woodenFishGalleryEntries,
|
||||
match3dDemoGalleryCard,
|
||||
]);
|
||||
const latestGalleryEntries = useMemo(
|
||||
() =>
|
||||
@@ -3784,6 +3804,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
? bigFishGalleryEntries.map(mapBigFishWorkToPlatformGalleryCard)
|
||||
: []),
|
||||
...match3dGalleryEntries.map(mapMatch3DWorkToPublicWorkDetail),
|
||||
match3dDemoGalleryCard,
|
||||
...puzzleGalleryEntries.map(mapPuzzleWorkToPlatformGalleryCard),
|
||||
...barkBattleGalleryEntries.map(mapBarkBattleWorkToPlatformGalleryCard),
|
||||
...jumpHopGalleryEntries.map(mapJumpHopWorkToPlatformGalleryCard),
|
||||
@@ -3825,6 +3846,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
barkBattleGalleryEntries,
|
||||
barkBattleWorks,
|
||||
woodenFishGalleryEntries,
|
||||
match3dDemoGalleryCard,
|
||||
],
|
||||
);
|
||||
const recommendRuntimeEntries = useMemo(() => {
|
||||
@@ -3832,9 +3854,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
filterGeneralPublicWorks([
|
||||
...featuredGalleryEntries,
|
||||
...latestGalleryEntries,
|
||||
]).forEach((entry) => {
|
||||
entryMap.set(getPlatformPublicGalleryEntryKey(entry), entry);
|
||||
});
|
||||
])
|
||||
.filter((entry) => !isMatch3DDemoProfileId(entry.profileId))
|
||||
.forEach((entry) => {
|
||||
entryMap.set(getPlatformPublicGalleryEntryKey(entry), entry);
|
||||
});
|
||||
return Array.from(entryMap.values());
|
||||
}, [featuredGalleryEntries, latestGalleryEntries]);
|
||||
|
||||
@@ -4344,6 +4368,21 @@ export function PlatformEntryFlowShellImpl({
|
||||
() => createServerMatch3DRuntimeAdapter(),
|
||||
[],
|
||||
);
|
||||
const match3dDemoRuntimeAdapter = useMemo(
|
||||
() =>
|
||||
createLocalMatch3DRuntimeAdapter({
|
||||
clearCount: 21,
|
||||
profileId: MATCH3D_DEMO_PROFILE_ID,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const resolveMatch3DRuntimeAdapter = useCallback(
|
||||
(profileId: string | null | undefined) =>
|
||||
isMatch3DDemoProfileId(profileId)
|
||||
? match3dDemoRuntimeAdapter
|
||||
: match3dRuntimeAdapter,
|
||||
[match3dDemoRuntimeAdapter, match3dRuntimeAdapter],
|
||||
);
|
||||
const match3dFlow = usePlatformCreationAgentFlowController<
|
||||
Match3DAgentSessionSnapshot,
|
||||
CreateMatch3DSessionRequest,
|
||||
@@ -8287,13 +8326,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleDetailReturnTarget(null);
|
||||
setPuzzleRun(null);
|
||||
setPuzzleRuntimeAuthMode('default');
|
||||
setPuzzleGalleryEntries((current) =>
|
||||
current.filter((entry) => entry.profileId !== profileId),
|
||||
);
|
||||
setPuzzleError(null);
|
||||
setPublicWorkDetailError(null);
|
||||
setPlatformTab('home');
|
||||
setSelectionStage('platform');
|
||||
if (!maybeAlertWorkNotFoundAndReturnHome()) {
|
||||
pushAppHistoryPath('/');
|
||||
}
|
||||
returnPlatformHomeAfterMissingWork();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -8310,9 +8348,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
[
|
||||
isPuzzleBusy,
|
||||
resolvePuzzleErrorMessage,
|
||||
returnPlatformHomeAfterMissingWork,
|
||||
setIsPuzzleBusy,
|
||||
setPuzzleError,
|
||||
setPlatformTab,
|
||||
setSelectionStage,
|
||||
],
|
||||
);
|
||||
@@ -8332,10 +8370,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
setMatch3DError(null);
|
||||
|
||||
try {
|
||||
let runtimeProfile = profile;
|
||||
const isDemoProfile = isMatch3DDemoProfileId(profile.profileId);
|
||||
let runtimeProfile: Match3DWorkProfile | Match3DWorkSummary =
|
||||
isDemoProfile ? match3dDemoProfile : profile;
|
||||
if (
|
||||
!hasMatch3DRuntimeAsset(profile.generatedItemAssets) ||
|
||||
!hasMatch3DRuntimeBackgroundAsset(profile)
|
||||
!isDemoProfile &&
|
||||
(!hasMatch3DRuntimeAsset(profile.generatedItemAssets) ||
|
||||
!hasMatch3DRuntimeBackgroundAsset(profile))
|
||||
) {
|
||||
try {
|
||||
const { item } = await getMatch3DWorkDetail(profile.profileId);
|
||||
@@ -8369,7 +8410,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
? { itemTypeCountOverride: options.itemTypeCountOverride }
|
||||
: {}),
|
||||
};
|
||||
const { run } = await match3dRuntimeAdapter.startRun(
|
||||
const activeRuntimeAdapter = resolveMatch3DRuntimeAdapter(
|
||||
runtimeProfile.profileId,
|
||||
);
|
||||
const { run } = await activeRuntimeAdapter.startRun(
|
||||
runtimeProfile.profileId,
|
||||
runtimeOptions,
|
||||
);
|
||||
@@ -8409,9 +8453,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
},
|
||||
[
|
||||
isMatch3DBusy,
|
||||
match3dDemoProfile,
|
||||
match3dFlow,
|
||||
match3dRuntimeAdapter,
|
||||
resolveMatch3DErrorMessage,
|
||||
resolveMatch3DRuntimeAdapter,
|
||||
setMatch3DError,
|
||||
setSelectionStage,
|
||||
],
|
||||
@@ -9945,13 +9990,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleDetailReturnTarget(null);
|
||||
setPuzzleRun(null);
|
||||
setPuzzleRuntimeAuthMode('default');
|
||||
setPuzzleGalleryEntries((current) =>
|
||||
current.filter((entry) => entry.profileId !== profileId),
|
||||
);
|
||||
setPuzzleError(null);
|
||||
setPublicWorkDetailError(null);
|
||||
setPlatformTab('home');
|
||||
setSelectionStage('platform');
|
||||
if (!maybeAlertWorkNotFoundAndReturnHome()) {
|
||||
pushAppHistoryPath('/');
|
||||
}
|
||||
returnPlatformHomeAfterMissingWork();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9969,7 +10013,6 @@ export function PlatformEntryFlowShellImpl({
|
||||
resolvePuzzleErrorMessage,
|
||||
setIsPuzzleBusy,
|
||||
setPuzzleError,
|
||||
setPlatformTab,
|
||||
setSelectionStage,
|
||||
],
|
||||
);
|
||||
@@ -9984,8 +10027,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
try {
|
||||
const entries =
|
||||
match3dGalleryEntries.length > 0
|
||||
? match3dGalleryEntries
|
||||
: await refreshMatch3DGallery();
|
||||
? [...match3dGalleryEntries, match3dDemoProfile]
|
||||
: await refreshMatch3DGallery().then((items) => [
|
||||
...items,
|
||||
match3dDemoProfile,
|
||||
]);
|
||||
const matchedEntry = entries.find(
|
||||
(entry) => entry.profileId === profileId,
|
||||
);
|
||||
@@ -10005,6 +10051,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
},
|
||||
[
|
||||
match3dGalleryEntries,
|
||||
match3dDemoProfile,
|
||||
openPublicWorkDetail,
|
||||
refreshMatch3DGallery,
|
||||
resolveMatch3DErrorMessage,
|
||||
@@ -10254,8 +10301,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
resolvePuzzleErrorMessage,
|
||||
setIsPuzzleBusy,
|
||||
setPuzzleError,
|
||||
setPlatformTab,
|
||||
setSelectionStage,
|
||||
returnPlatformHomeAfterMissingWork,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -11338,7 +11384,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
match3dFlow.setIsBusy(true);
|
||||
setMatch3DError(null);
|
||||
void match3dRuntimeAdapter
|
||||
void resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
)
|
||||
.restartRun(match3dRun.runId)
|
||||
.then(({ run }) => {
|
||||
setMatch3DRun(run);
|
||||
@@ -11358,14 +11406,18 @@ export function PlatformEntryFlowShellImpl({
|
||||
if (!runId) {
|
||||
return Promise.reject(new Error('抓大鹅运行态缺少 runId。'));
|
||||
}
|
||||
return match3dRuntimeAdapter.clickItem(runId, payload);
|
||||
return resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
).clickItem(runId, payload);
|
||||
}}
|
||||
onTimeExpired={() => {
|
||||
if (!match3dRun?.runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
void match3dRuntimeAdapter
|
||||
void resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
)
|
||||
.finishTimeUp(match3dRun.runId)
|
||||
.then(({ run }) => {
|
||||
setMatch3DRun(run);
|
||||
@@ -12176,8 +12228,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
const tryOpenMatch3DGalleryEntry = async () => {
|
||||
const entries =
|
||||
match3dGalleryEntries.length > 0
|
||||
? match3dGalleryEntries
|
||||
: await refreshMatch3DGallery();
|
||||
? [...match3dGalleryEntries, match3dDemoProfile]
|
||||
: await refreshMatch3DGallery().then((items) => [
|
||||
...items,
|
||||
match3dDemoProfile,
|
||||
]);
|
||||
const matchedEntry = entries.find((entry) => {
|
||||
const detailEntry = mapMatch3DWorkToPublicWorkDetail(entry);
|
||||
return (
|
||||
@@ -12383,11 +12438,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleRuntimeAuthMode('default');
|
||||
setPuzzleError(null);
|
||||
setPublicWorkDetailError(null);
|
||||
setPlatformTab('home');
|
||||
setSelectionStage('platform');
|
||||
if (!maybeAlertWorkNotFoundAndReturnHome()) {
|
||||
pushAppHistoryPath('/');
|
||||
}
|
||||
returnPlatformHomeAfterMissingWork();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12416,6 +12467,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
refreshSquareHoleGallery,
|
||||
refreshVisualNovelGallery,
|
||||
squareHoleGalleryEntries,
|
||||
returnPlatformHomeAfterMissingWork,
|
||||
selectionStage,
|
||||
setPlatformTab,
|
||||
setPuzzleError,
|
||||
@@ -12597,6 +12649,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
refreshBigFishGallery,
|
||||
resolveBigFishErrorMessage,
|
||||
setBigFishError,
|
||||
match3dDemoProfile,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -13775,7 +13828,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
match3dRun?.runId &&
|
||||
match3dRun.status === 'running'
|
||||
) {
|
||||
void match3dRuntimeAdapter
|
||||
void resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
)
|
||||
.stopRun(match3dRun.runId)
|
||||
.catch(() => undefined);
|
||||
}
|
||||
@@ -13788,7 +13843,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
match3dFlow.setIsBusy(true);
|
||||
setMatch3DError(null);
|
||||
void match3dRuntimeAdapter
|
||||
void resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
)
|
||||
.restartRun(match3dRun.runId)
|
||||
.then(({ run }) => {
|
||||
setMatch3DRun(run);
|
||||
@@ -13813,14 +13870,18 @@ export function PlatformEntryFlowShellImpl({
|
||||
new Error('抓大鹅运行态缺少 runId。'),
|
||||
);
|
||||
}
|
||||
return match3dRuntimeAdapter.clickItem(runId, payload);
|
||||
return resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
).clickItem(runId, payload);
|
||||
}}
|
||||
onTimeExpired={() => {
|
||||
if (!match3dRun?.runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
void match3dRuntimeAdapter
|
||||
void resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
)
|
||||
.finishTimeUp(match3dRun.runId)
|
||||
.then(({ run }) => {
|
||||
setMatch3DRun(run);
|
||||
|
||||
@@ -82,7 +82,10 @@ import {
|
||||
saveBabyObjectMatchDraft,
|
||||
} from '../../services/edutainment-baby-object';
|
||||
import { match3dCreationClient } from '../../services/match3d-creation';
|
||||
import { createServerMatch3DRuntimeAdapter } from '../../services/match3d-runtime';
|
||||
import {
|
||||
createLocalMatch3DRuntimeAdapter,
|
||||
createServerMatch3DRuntimeAdapter,
|
||||
} from '../../services/match3d-runtime';
|
||||
import {
|
||||
deleteMatch3DWork,
|
||||
getMatch3DWorkDetail,
|
||||
@@ -556,6 +559,7 @@ vi.mock('../../services/match3dGeneratedModelCache', () => ({
|
||||
}));
|
||||
|
||||
const match3dRuntimeServiceMocks = vi.hoisted(() => ({
|
||||
createLocalMatch3DRuntimeAdapter: vi.fn(),
|
||||
createServerMatch3DRuntimeAdapter: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -568,6 +572,15 @@ const match3dServerRuntimeAdapterMock = vi.hoisted(() => ({
|
||||
stopRun: vi.fn(),
|
||||
}));
|
||||
|
||||
const match3dLocalRuntimeAdapterMock = vi.hoisted(() => ({
|
||||
clickItem: vi.fn(),
|
||||
finishTimeUp: vi.fn(),
|
||||
getRun: vi.fn(),
|
||||
restartRun: vi.fn(),
|
||||
startRun: vi.fn(),
|
||||
stopRun: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/match3d-runtime', async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import('../../services/match3d-runtime')
|
||||
@@ -2264,6 +2277,9 @@ beforeEach(() => {
|
||||
vi.mocked(createServerMatch3DRuntimeAdapter).mockReturnValue(
|
||||
match3dServerRuntimeAdapterMock,
|
||||
);
|
||||
vi.mocked(createLocalMatch3DRuntimeAdapter).mockReturnValue(
|
||||
match3dLocalRuntimeAdapterMock,
|
||||
);
|
||||
match3dServerRuntimeAdapterMock.startRun.mockRejectedValue(
|
||||
new Error('未启动抓大鹅运行态'),
|
||||
);
|
||||
@@ -2279,6 +2295,21 @@ beforeEach(() => {
|
||||
match3dServerRuntimeAdapterMock.stopRun.mockResolvedValue({
|
||||
run: buildMockMatch3DRun('match3d-profile-stopped'),
|
||||
});
|
||||
match3dLocalRuntimeAdapterMock.startRun.mockResolvedValue({
|
||||
run: buildMockMatch3DRun('match3d-demo-20260525'),
|
||||
});
|
||||
match3dLocalRuntimeAdapterMock.clickItem.mockRejectedValue(
|
||||
new Error('未执行本地抓大鹅点击'),
|
||||
);
|
||||
match3dLocalRuntimeAdapterMock.restartRun.mockResolvedValue({
|
||||
run: buildMockMatch3DRun('match3d-demo-20260525'),
|
||||
});
|
||||
match3dLocalRuntimeAdapterMock.finishTimeUp.mockResolvedValue({
|
||||
run: buildMockMatch3DRun('match3d-demo-20260525'),
|
||||
});
|
||||
match3dLocalRuntimeAdapterMock.stopRun.mockResolvedValue({
|
||||
run: buildMockMatch3DRun('match3d-demo-20260525'),
|
||||
});
|
||||
window.history.replaceState(null, '', '/');
|
||||
window.sessionStorage.clear();
|
||||
window.localStorage.clear();
|
||||
@@ -7683,6 +7714,38 @@ test('public code search opens a published Match3D work by M3 code and starts ru
|
||||
expect(getRpgEntryWorldGalleryDetailByCode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('public code search opens the local Match3D demo and starts local runtime', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(listMatch3DGallery).mockResolvedValue({ items: [] });
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
await openDiscoverHub(user);
|
||||
|
||||
const searchInput =
|
||||
await screen.findByPlaceholderText('搜索作品号、名称、作者、描述');
|
||||
await user.type(searchInput, 'M3-20260525');
|
||||
await user.click(screen.getByRole('button', { name: '搜索' }));
|
||||
|
||||
expect(await screen.findByText('详情')).toBeTruthy();
|
||||
expect(screen.getByText('海底糖果集市')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '启动' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(match3dLocalRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
|
||||
'match3d-demo-20260525',
|
||||
{},
|
||||
);
|
||||
});
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).not.toHaveBeenCalled();
|
||||
expect(getMatch3DWorkDetail).not.toHaveBeenCalledWith(
|
||||
'match3d-demo-20260525',
|
||||
);
|
||||
expect(
|
||||
await screen.findByText('抓大鹅运行态:match3d-run-match3d-demo-20260525'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('published Match3D runtime receives persisted generated models', async () => {
|
||||
const user = userEvent.setup();
|
||||
const match3dWork: Match3DWorkSummary = {
|
||||
|
||||
@@ -67,6 +67,7 @@ import type {
|
||||
WechatMiniProgramPayParams,
|
||||
WechatNativePayment,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import { isMatch3DDemoProfileId } from '../../data/match3dDemoGalleryCard';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
|
||||
import type { AuthUser } from '../../services/authService';
|
||||
@@ -3888,6 +3889,7 @@ export function RpgEntryHomeView({
|
||||
const [mobileCenteredCardKey, setMobileCenteredCardKey] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const hasManualCategoryTagSelectionRef = useRef(false);
|
||||
const pendingPublicAuthorKeysRef = useRef<Set<string>>(new Set());
|
||||
const [publicAuthorSummariesByKey, setPublicAuthorSummariesByKey] = useState<
|
||||
Record<string, PublicUserSummary | null>
|
||||
@@ -4111,16 +4113,33 @@ export function RpgEntryHomeView({
|
||||
useEffect(() => {
|
||||
if (categoryGroups.length === 0) {
|
||||
setSelectedCategoryTag(null);
|
||||
hasManualCategoryTagSelectionRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const firstCategoryGroup = categoryGroups[0];
|
||||
const firstCategoryGroup =
|
||||
categoryGroups.find((group) =>
|
||||
group.entries.some((entry) => !isMatch3DDemoProfileId(entry.profileId)),
|
||||
) ?? categoryGroups[0];
|
||||
const selectedCategoryGroup =
|
||||
categoryGroups.find((group) => group.tag === selectedCategoryTag) ?? null;
|
||||
if (
|
||||
firstCategoryGroup &&
|
||||
!categoryGroups.some((group) => group.tag === selectedCategoryTag)
|
||||
(!selectedCategoryGroup ||
|
||||
(!hasManualCategoryTagSelectionRef.current &&
|
||||
selectedCategoryGroup.entries.every((entry) =>
|
||||
isMatch3DDemoProfileId(entry.profileId),
|
||||
) &&
|
||||
firstCategoryGroup.tag !== selectedCategoryGroup.tag))
|
||||
) {
|
||||
setSelectedCategoryTag(firstCategoryGroup.tag);
|
||||
}
|
||||
if (
|
||||
selectedCategoryTag &&
|
||||
!categoryGroups.some((group) => group.tag === selectedCategoryTag)
|
||||
) {
|
||||
hasManualCategoryTagSelectionRef.current = false;
|
||||
}
|
||||
}, [categoryGroups, selectedCategoryTag]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -5442,7 +5461,10 @@ export function RpgEntryHomeView({
|
||||
<button
|
||||
key={group.tag}
|
||||
type="button"
|
||||
onClick={() => setSelectedCategoryTag(group.tag)}
|
||||
onClick={() => {
|
||||
hasManualCategoryTagSelectionRef.current = true;
|
||||
setSelectedCategoryTag(group.tag);
|
||||
}}
|
||||
className={`platform-category-chip ${active ? 'platform-category-chip--active' : ''}`}
|
||||
>
|
||||
{group.tag}
|
||||
@@ -5640,7 +5662,10 @@ export function RpgEntryHomeView({
|
||||
<button
|
||||
key={`${group.tag}:desktop-discover-category`}
|
||||
type="button"
|
||||
onClick={() => setSelectedCategoryTag(group.tag)}
|
||||
onClick={() => {
|
||||
hasManualCategoryTagSelectionRef.current = true;
|
||||
setSelectedCategoryTag(group.tag);
|
||||
}}
|
||||
className={`platform-category-chip shrink-0 ${active ? 'platform-category-chip--active' : ''}`}
|
||||
>
|
||||
{group.tag}
|
||||
@@ -6398,7 +6423,10 @@ export function RpgEntryHomeView({
|
||||
<button
|
||||
key={`${group.tag}:desktop-category`}
|
||||
type="button"
|
||||
onClick={() => setSelectedCategoryTag(group.tag)}
|
||||
onClick={() => {
|
||||
hasManualCategoryTagSelectionRef.current = true;
|
||||
setSelectedCategoryTag(group.tag);
|
||||
}}
|
||||
className={`platform-category-chip shrink-0 ${active ? 'platform-category-chip--active' : ''}`}
|
||||
>
|
||||
{group.tag}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
formatPlatformWorldTime,
|
||||
isBarkBattleGalleryEntry,
|
||||
isEdutainmentGalleryEntry,
|
||||
isMatch3DGalleryEntry,
|
||||
isVisualNovelGalleryEntry,
|
||||
isWoodenFishGalleryEntry,
|
||||
mapBabyObjectMatchDraftToPlatformGalleryCard,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
resolvePlatformPublicWorkCode,
|
||||
resolvePlatformWorldFallbackCoverImage,
|
||||
} from './rpgEntryWorldPresentation';
|
||||
import { buildMatch3DDemoGalleryCard } from '../../data/match3dDemoGalleryCard';
|
||||
|
||||
test('formatPlatformWorldTime formats backend seconds timestamp text as date', () => {
|
||||
expect(formatPlatformWorldTime('1777110165.990127Z')).toBe('2026-04-25');
|
||||
@@ -78,6 +80,24 @@ test('platform public cards use play type reference images as cover fallback', (
|
||||
);
|
||||
});
|
||||
|
||||
test('builds local Match3D demo gallery card with generated runtime assets intact', () => {
|
||||
const card = buildMatch3DDemoGalleryCard();
|
||||
|
||||
expect(isMatch3DGalleryEntry(card)).toBe(true);
|
||||
expect(card.publicWorkCode).toBe('M3-20260525');
|
||||
expect(resolvePlatformPublicWorkCode(card)).toBe('M3-20260525');
|
||||
expect(card.coverImageSrc).toBe(
|
||||
'/match3d-demo/undersea-candy-market/level-scene.png',
|
||||
);
|
||||
expect(card.generatedBackgroundAsset?.uiSpritesheetImageSrc).toBe(
|
||||
'/match3d-demo/undersea-candy-market/ui-spritesheet.png',
|
||||
);
|
||||
expect(card.generatedBackgroundAsset?.containerImageSrc).toBeNull();
|
||||
expect(card.generatedItemAssets?.[0]?.imageViews?.[0]?.imageSrc).toBe(
|
||||
'/match3d-demo/undersea-candy-market/item-slices/item-01/view-01.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('buildPuzzleWorkCoverSlides prefers each level formal image', () => {
|
||||
const slides = buildPuzzleWorkCoverSlides({
|
||||
workId: 'work-1',
|
||||
|
||||
Reference in New Issue
Block a user