Integrate unfinished server-rs refactor worklists
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { BigFishRuntimeSnapshotResponse } from '../../../packages/shared/src/contracts/bigFish';
|
||||
import { BigFishRuntimeShell } from './BigFishRuntimeShell';
|
||||
@@ -49,10 +49,6 @@ function dispatchPointerEvent(
|
||||
}
|
||||
|
||||
describe('BigFishRuntimeShell', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('renders restart and exit actions after a failed run', () => {
|
||||
const onBack = vi.fn();
|
||||
const onRestart = vi.fn();
|
||||
@@ -111,8 +107,7 @@ describe('BigFishRuntimeShell', () => {
|
||||
expect(screen.queryByRole('dialog', { name: '玩法规则' })).toBeNull();
|
||||
});
|
||||
|
||||
test('keeps moving in the last sampled direction after drag ends', () => {
|
||||
vi.useFakeTimers();
|
||||
test('keeps moving in the last sampled direction after drag ends', async () => {
|
||||
const onSubmitInput = vi.fn();
|
||||
|
||||
const { container } = render(
|
||||
@@ -141,9 +136,10 @@ describe('BigFishRuntimeShell', () => {
|
||||
clientY: 100,
|
||||
});
|
||||
});
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(100);
|
||||
await waitFor(() => {
|
||||
expect(onSubmitInput).toHaveBeenCalledWith({ x: 1, y: 0 });
|
||||
});
|
||||
const callCountAfterDrag = onSubmitInput.mock.calls.length;
|
||||
act(() => {
|
||||
dispatchPointerEvent(stage, 'pointerup', {
|
||||
pointerId: 1,
|
||||
@@ -151,8 +147,9 @@ describe('BigFishRuntimeShell', () => {
|
||||
clientY: 100,
|
||||
});
|
||||
});
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(220);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmitInput.mock.calls.length).toBeGreaterThan(callCountAfterDrag);
|
||||
});
|
||||
|
||||
expect(onSubmitInput).toHaveBeenLastCalledWith({ x: 1, y: 0 });
|
||||
|
||||
@@ -56,9 +56,9 @@ import {
|
||||
} from '../../services/big-fish-creation';
|
||||
import { listBigFishGallery } from '../../services/big-fish-gallery';
|
||||
import {
|
||||
advanceLocalBigFishRuntimeRun,
|
||||
recordBigFishPlay,
|
||||
startLocalBigFishRuntimeRun,
|
||||
startBigFishRun as startBigFishRuntimeRun,
|
||||
submitBigFishInput as submitBigFishRuntimeInput,
|
||||
} from '../../services/big-fish-runtime';
|
||||
import {
|
||||
deleteBigFishWork,
|
||||
@@ -456,6 +456,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
>(null);
|
||||
const [bigFishRuntimeSessionSource, setBigFishRuntimeSessionSource] =
|
||||
useState<BigFishRuntimeSessionSource>(null);
|
||||
const bigFishInputInFlightRef = useRef(false);
|
||||
const [isBigFishLoadingLibrary, setIsBigFishLoadingLibrary] = useState(false);
|
||||
const [bigFishGenerationState, setBigFishGenerationState] =
|
||||
useState<MiniGameDraftGenerationState | null>(null);
|
||||
@@ -1182,7 +1183,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
}, [bigFishRun, bigFishSession, selectionStage, setSelectionStage]);
|
||||
|
||||
const startBigFishRun = useCallback(() => {
|
||||
const startBigFishRun = useCallback(async () => {
|
||||
if (!bigFishSession) {
|
||||
return;
|
||||
}
|
||||
@@ -1191,16 +1192,25 @@ export function PlatformEntryFlowShellImpl({
|
||||
setBigFishError(null);
|
||||
setBigFishRuntimeShare(null);
|
||||
setBigFishRuntimeWork(null);
|
||||
setBigFishRuntimeStartedAt(Date.now());
|
||||
setBigFishRuntimeSessionSource('draft');
|
||||
setBigFishRun(startLocalBigFishRuntimeRun({ session: bigFishSession }));
|
||||
setSelectionStage('big-fish-runtime');
|
||||
void recordBigFishPlay(sessionId, { elapsedMs: 0 }).catch((error) => {
|
||||
setBigFishRuntimeStartedAt(null);
|
||||
setBigFishRun(null);
|
||||
try {
|
||||
const { run } = await startBigFishRuntimeRun(sessionId);
|
||||
setBigFishRuntimeStartedAt(Date.now());
|
||||
setBigFishRuntimeSessionSource('draft');
|
||||
setBigFishRun(run);
|
||||
setSelectionStage('big-fish-runtime');
|
||||
void recordBigFishPlay(sessionId, { elapsedMs: 0 }).catch((error) => {
|
||||
setBigFishError(
|
||||
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
|
||||
);
|
||||
});
|
||||
void refreshBigFishShelf();
|
||||
} catch (error) {
|
||||
setBigFishError(
|
||||
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
|
||||
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'),
|
||||
);
|
||||
});
|
||||
void refreshBigFishShelf();
|
||||
}
|
||||
}, [
|
||||
bigFishSession,
|
||||
refreshBigFishShelf,
|
||||
@@ -1208,7 +1218,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSelectionStage,
|
||||
]);
|
||||
|
||||
const restartBigFishRun = useCallback(() => {
|
||||
const restartBigFishRun = useCallback(async () => {
|
||||
if (!bigFishSession && !bigFishRun) {
|
||||
return;
|
||||
}
|
||||
@@ -1222,23 +1232,26 @@ export function PlatformEntryFlowShellImpl({
|
||||
if (bigFishSession) {
|
||||
setBigFishRuntimeShare(null);
|
||||
}
|
||||
setBigFishRuntimeStartedAt(Date.now());
|
||||
setBigFishRuntimeSessionSource(bigFishSession ? 'draft' : 'work');
|
||||
setBigFishRun(
|
||||
startLocalBigFishRuntimeRun({
|
||||
session: bigFishSession,
|
||||
work: bigFishRuntimeWork,
|
||||
}),
|
||||
);
|
||||
setSelectionStage('big-fish-runtime');
|
||||
void recordBigFishPlay(sessionId, { elapsedMs: 0 }).catch((error) => {
|
||||
setBigFishRuntimeStartedAt(null);
|
||||
setBigFishRun(null);
|
||||
try {
|
||||
const { run } = await startBigFishRuntimeRun(sessionId);
|
||||
setBigFishRuntimeStartedAt(Date.now());
|
||||
setBigFishRuntimeSessionSource(bigFishSession ? 'draft' : 'work');
|
||||
setBigFishRun(run);
|
||||
setSelectionStage('big-fish-runtime');
|
||||
void recordBigFishPlay(sessionId, { elapsedMs: 0 }).catch((error) => {
|
||||
setBigFishError(
|
||||
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
setBigFishError(
|
||||
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
|
||||
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'),
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [
|
||||
bigFishRun,
|
||||
bigFishRuntimeWork,
|
||||
bigFishSession,
|
||||
resolveBigFishErrorMessage,
|
||||
setSelectionStage,
|
||||
@@ -1327,18 +1340,31 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
|
||||
const submitBigFishInput = useCallback(
|
||||
(payload: SubmitBigFishInputRequest) => {
|
||||
if (!bigFishRun || bigFishRun.status !== 'running') {
|
||||
async (payload: SubmitBigFishInputRequest) => {
|
||||
if (
|
||||
!bigFishRun ||
|
||||
bigFishRun.status !== 'running' ||
|
||||
bigFishInputInFlightRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setBigFishRun((currentRun) =>
|
||||
currentRun
|
||||
? advanceLocalBigFishRuntimeRun(currentRun, payload)
|
||||
: currentRun,
|
||||
);
|
||||
bigFishInputInFlightRef.current = true;
|
||||
try {
|
||||
const { run } = await submitBigFishRuntimeInput(
|
||||
bigFishRun.runId,
|
||||
payload,
|
||||
);
|
||||
setBigFishRun(run);
|
||||
} catch (error) {
|
||||
setBigFishError(
|
||||
resolveBigFishErrorMessage(error, '同步大鱼吃小鱼输入失败。'),
|
||||
);
|
||||
} finally {
|
||||
bigFishInputInFlightRef.current = false;
|
||||
}
|
||||
},
|
||||
[bigFishRun],
|
||||
[bigFishRun, resolveBigFishErrorMessage, setBigFishError],
|
||||
);
|
||||
|
||||
const reportBigFishObservedPlayTime = useCallback(() => {
|
||||
@@ -1793,7 +1819,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
|
||||
const startBigFishRunFromWork = useCallback(
|
||||
(item: BigFishWorkSummary) => {
|
||||
async (item: BigFishWorkSummary) => {
|
||||
const sessionId = item.sourceSessionId?.trim();
|
||||
if (!sessionId) {
|
||||
setBigFishError('当前作品缺少会话信息,暂时无法进入玩法。');
|
||||
@@ -1808,18 +1834,27 @@ export function PlatformEntryFlowShellImpl({
|
||||
title: item.title,
|
||||
publicWorkCode,
|
||||
});
|
||||
setBigFishRuntimeStartedAt(Date.now());
|
||||
setBigFishRuntimeStartedAt(null);
|
||||
setBigFishRuntimeSessionSource('work');
|
||||
setBigFishRun(startLocalBigFishRuntimeRun({ work: item }));
|
||||
setSelectionStage('big-fish-runtime');
|
||||
pushAppHistoryPath(
|
||||
buildPublicWorkStagePath('big-fish-runtime', publicWorkCode),
|
||||
);
|
||||
void recordBigFishPlay(sessionId, { elapsedMs: 0 }).catch((error) => {
|
||||
setBigFishError(
|
||||
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
|
||||
setBigFishRun(null);
|
||||
try {
|
||||
const { run } = await startBigFishRuntimeRun(sessionId);
|
||||
setBigFishRuntimeStartedAt(Date.now());
|
||||
setBigFishRun(run);
|
||||
setSelectionStage('big-fish-runtime');
|
||||
pushAppHistoryPath(
|
||||
buildPublicWorkStagePath('big-fish-runtime', publicWorkCode),
|
||||
);
|
||||
});
|
||||
void recordBigFishPlay(sessionId, { elapsedMs: 0 }).catch((error) => {
|
||||
setBigFishError(
|
||||
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
setBigFishError(
|
||||
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'),
|
||||
);
|
||||
}
|
||||
},
|
||||
[bigFishFlow, resolveBigFishErrorMessage, setSelectionStage],
|
||||
);
|
||||
@@ -2018,10 +2053,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
(entry) => entry.sourceSessionId === sessionId,
|
||||
);
|
||||
if (matchedEntry) {
|
||||
startBigFishRunFromWork(matchedEntry);
|
||||
void startBigFishRunFromWork(matchedEntry);
|
||||
return;
|
||||
}
|
||||
startBigFishRunFromWork({
|
||||
void startBigFishRunFromWork({
|
||||
workId: `big-fish:${sessionId}`,
|
||||
sourceSessionId: sessionId,
|
||||
ownerUserId: work.ownerUserId ?? '',
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
getBigFishCreationSession,
|
||||
} from '../../services/big-fish-creation';
|
||||
import { listBigFishGallery } from '../../services/big-fish-gallery';
|
||||
import { startLocalBigFishRuntimeRun } from '../../services/big-fish-runtime';
|
||||
import { startBigFishRun } from '../../services/big-fish-runtime';
|
||||
import { listBigFishWorks } from '../../services/big-fish-works';
|
||||
import {
|
||||
createPuzzleAgentSession,
|
||||
@@ -155,8 +155,9 @@ vi.mock('../../services/big-fish-gallery', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../../services/big-fish-runtime', () => ({
|
||||
advanceLocalBigFishRuntimeRun: vi.fn((run) => run),
|
||||
startLocalBigFishRuntimeRun: vi.fn(),
|
||||
recordBigFishPlay: vi.fn().mockResolvedValue({ items: [] }),
|
||||
startBigFishRun: vi.fn(),
|
||||
submitBigFishInput: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/puzzle-agent', () => ({
|
||||
@@ -1091,28 +1092,30 @@ beforeEach(() => {
|
||||
vi.mocked(listBigFishGallery).mockResolvedValue({
|
||||
items: [],
|
||||
});
|
||||
vi.mocked(startLocalBigFishRuntimeRun).mockReturnValue({
|
||||
runId: 'big-fish-run-1',
|
||||
sessionId: 'big-fish-session-public-1',
|
||||
status: 'running',
|
||||
tick: 0,
|
||||
playerLevel: 1,
|
||||
winLevel: 8,
|
||||
leaderEntityId: 'owned-1',
|
||||
ownedEntities: [
|
||||
{
|
||||
entityId: 'owned-1',
|
||||
level: 1,
|
||||
position: { x: 0, y: 0 },
|
||||
radius: 12,
|
||||
offscreenSeconds: 0,
|
||||
},
|
||||
],
|
||||
wildEntities: [],
|
||||
cameraCenter: { x: 0, y: 0 },
|
||||
lastInput: { x: 0, y: 0 },
|
||||
eventLog: ['机械鱼群开始巡游。'],
|
||||
updatedAt: '2026-04-25T12:12:00.000Z',
|
||||
vi.mocked(startBigFishRun).mockResolvedValue({
|
||||
run: {
|
||||
runId: 'big-fish-run-1',
|
||||
sessionId: 'big-fish-session-public-1',
|
||||
status: 'running',
|
||||
tick: 0,
|
||||
playerLevel: 1,
|
||||
winLevel: 8,
|
||||
leaderEntityId: 'owned-1',
|
||||
ownedEntities: [
|
||||
{
|
||||
entityId: 'owned-1',
|
||||
level: 1,
|
||||
position: { x: 0, y: 0 },
|
||||
radius: 12,
|
||||
offscreenSeconds: 0,
|
||||
},
|
||||
],
|
||||
wildEntities: [],
|
||||
cameraCenter: { x: 0, y: 0 },
|
||||
lastInput: { x: 0, y: 0 },
|
||||
eventLog: ['机械鱼群开始巡游。'],
|
||||
updatedAt: '2026-04-25T12:12:00.000Z',
|
||||
},
|
||||
});
|
||||
vi.mocked(listPuzzleWorks).mockResolvedValue({
|
||||
items: [],
|
||||
@@ -2067,11 +2070,9 @@ test('public code search opens a published big fish work by BF code', async () =
|
||||
await user.click(screen.getByRole('button', { name: '搜索' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(startLocalBigFishRuntimeRun).toHaveBeenCalledWith({
|
||||
work: expect.objectContaining({
|
||||
sourceSessionId: 'big-fish-session-public-1',
|
||||
}),
|
||||
});
|
||||
expect(startBigFishRun).toHaveBeenCalledWith(
|
||||
'big-fish-session-public-1',
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText('Lv.1/8 · 进行中')).toBeTruthy();
|
||||
expect(getBigFishCreationSession).not.toHaveBeenCalledWith(
|
||||
|
||||
@@ -4,10 +4,18 @@ import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import { AnimationState, WorldType, type GameState } from '../../types';
|
||||
import {
|
||||
AnimationState,
|
||||
WorldType,
|
||||
type GameState,
|
||||
type StoryMoment,
|
||||
} from '../../types';
|
||||
import { RpgRuntimeShell } from './RpgRuntimeShell';
|
||||
import type { RpgRuntimeShellProps } from './types';
|
||||
|
||||
const noop = () => {};
|
||||
const asyncFalse = async () => false;
|
||||
|
||||
vi.mock('../auth/AuthUiContext', () => ({
|
||||
useAuthUi: () => null,
|
||||
}));
|
||||
@@ -34,7 +42,7 @@ vi.mock('./useRpgRuntimeShellViewModel', () => ({
|
||||
shouldMountNpcModals: false,
|
||||
visibleGameState: mockVisibleGameState,
|
||||
visibleCurrentStory: {
|
||||
storyText: '测试故事',
|
||||
text: '测试故事',
|
||||
options: [],
|
||||
},
|
||||
sceneTransitionPhase: 'idle',
|
||||
@@ -97,22 +105,23 @@ function createGameState(
|
||||
description: '测试角色',
|
||||
backstory: '测试背景',
|
||||
personality: '冷静',
|
||||
motivation: '完成测试',
|
||||
combatStyle: '均衡',
|
||||
role: '主角',
|
||||
avatar: '',
|
||||
portrait: '',
|
||||
imageSrc: '',
|
||||
initialAffinity: 0,
|
||||
relationshipHooks: [],
|
||||
tags: [],
|
||||
assetFolder: '',
|
||||
assetVariant: '',
|
||||
attributes: {
|
||||
strength: 5,
|
||||
agility: 5,
|
||||
intelligence: 5,
|
||||
spirit: 5,
|
||||
},
|
||||
backstoryReveal: {
|
||||
publicSummary: '测试',
|
||||
privateChatUnlockAffinity: 60,
|
||||
chapters: [],
|
||||
},
|
||||
skills: [],
|
||||
initialItems: [],
|
||||
adventureOpenings: {},
|
||||
},
|
||||
runtimeMode,
|
||||
runtimePersistenceDisabled: runtimePersistenceDisabled ?? false,
|
||||
@@ -176,14 +185,15 @@ function buildProps(
|
||||
runtimePersistenceDisabled?: boolean,
|
||||
): RpgRuntimeShellProps {
|
||||
const gameState = createGameState(runtimeMode, runtimePersistenceDisabled);
|
||||
const currentStory: StoryMoment = {
|
||||
text: '测试故事',
|
||||
options: [],
|
||||
};
|
||||
mockVisibleGameState = gameState;
|
||||
return {
|
||||
session: {
|
||||
gameState,
|
||||
currentStory: {
|
||||
storyText: '测试故事',
|
||||
options: [],
|
||||
},
|
||||
currentStory,
|
||||
isLoading: false,
|
||||
aiError: null,
|
||||
bottomTab: 'adventure',
|
||||
@@ -201,36 +211,66 @@ function buildProps(
|
||||
exitNpcChat: () => false,
|
||||
handleMapTravelToScene: () => false,
|
||||
npcUi: {
|
||||
isNpcModalOpen: false,
|
||||
currentNpcEncounter: null,
|
||||
selectedNpc: null,
|
||||
isGeneratingNpcResponse: false,
|
||||
npcResponseError: null,
|
||||
generatedNpcText: '',
|
||||
npcResponseOptions: [],
|
||||
selectedOptionId: null,
|
||||
tradeModal: null,
|
||||
giftModal: null,
|
||||
recruitModal: null,
|
||||
setTradeMode: noop,
|
||||
selectTradeNpcItem: noop,
|
||||
selectTradePlayerItem: noop,
|
||||
setTradeQuantity: noop,
|
||||
closeTradeModal: noop,
|
||||
confirmTrade: noop,
|
||||
selectGiftItem: noop,
|
||||
closeGiftModal: noop,
|
||||
confirmGift: noop,
|
||||
selectRecruitRelease: noop,
|
||||
closeRecruitModal: noop,
|
||||
confirmRecruit: noop,
|
||||
},
|
||||
characterChatUi: {
|
||||
isCharacterChatModalOpen: false,
|
||||
activeCharacter: null,
|
||||
modal: null,
|
||||
openChat: noop,
|
||||
closeChat: noop,
|
||||
setDraft: noop,
|
||||
useSuggestion: noop,
|
||||
refreshSuggestions: noop,
|
||||
sendDraft: noop,
|
||||
},
|
||||
inventoryUi: {
|
||||
isInventoryOpen: false,
|
||||
useInventoryItem: asyncFalse,
|
||||
equipInventoryItem: asyncFalse,
|
||||
unequipItem: asyncFalse,
|
||||
playerCurrency: 0,
|
||||
currencyText: '0',
|
||||
backpackItems: [],
|
||||
equipmentSlots: [],
|
||||
forgeRecipes: [],
|
||||
craftRecipe: asyncFalse,
|
||||
dismantleItem: asyncFalse,
|
||||
reforgeItem: asyncFalse,
|
||||
},
|
||||
battleRewardUi: {
|
||||
isRewardModalOpen: false,
|
||||
rewards: [],
|
||||
reward: null,
|
||||
dismiss: noop,
|
||||
},
|
||||
questUi: {
|
||||
isQuestPanelOpen: false,
|
||||
acknowledgeQuestCompletion: noop,
|
||||
claimQuestReward: () => null,
|
||||
},
|
||||
npcChatQuestOfferUi: {
|
||||
isOfferModalOpen: false,
|
||||
pendingQuest: null,
|
||||
replacePendingOffer: () => false,
|
||||
abandonPendingOffer: () => false,
|
||||
acceptPendingOffer: () => null,
|
||||
},
|
||||
goalUi: {
|
||||
isGoalPanelOpen: false,
|
||||
entries: [],
|
||||
goalStack: {
|
||||
northStarGoal: null,
|
||||
activeGoal: null,
|
||||
immediateStepGoal: null,
|
||||
supportGoals: [],
|
||||
},
|
||||
pulse: null,
|
||||
dismissPulse: noop,
|
||||
},
|
||||
},
|
||||
entry: {
|
||||
|
||||
Reference in New Issue
Block a user