499 lines
15 KiB
TypeScript
499 lines
15 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
|
|
import type { BarkBattleDraftConfig } from '../../../packages/shared/src/contracts/barkBattle';
|
|
import type { BigFishSessionSnapshotResponse } from '../../../packages/shared/src/contracts/bigFish';
|
|
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
|
import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent';
|
|
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
|
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
|
import type { SquareHoleSessionSnapshot } from '../../../packages/shared/src/contracts/squareHoleAgent';
|
|
import type { VisualNovelAgentSessionSnapshot } from '../../../packages/shared/src/contracts/visualNovel';
|
|
import type {
|
|
JumpHopSessionSnapshotResponse,
|
|
JumpHopWorkProfileResponse,
|
|
} from '../../services/jump-hop/jumpHopClient';
|
|
import type {
|
|
WoodenFishSessionSnapshotResponse,
|
|
WoodenFishWorkProfileResponse,
|
|
} from '../../services/wooden-fish/woodenFishClient';
|
|
import {
|
|
buildBabyObjectMatchCreationUrlState,
|
|
buildBarkBattleCreationUrlState,
|
|
buildBigFishCreationUrlState,
|
|
buildJumpHopCreationUrlState,
|
|
buildMatch3DCreationUrlState,
|
|
buildPuzzleCreationUrlState,
|
|
buildPuzzleDraftRuntimeUrlState,
|
|
buildPuzzlePublishedRuntimeUrlState,
|
|
buildPuzzleRuntimeUrlStateKey,
|
|
buildSquareHoleCreationUrlState,
|
|
buildVisualNovelCreationUrlState,
|
|
buildWoodenFishCreationUrlState,
|
|
hasCreationUrlStateValue,
|
|
hasPuzzleRuntimeUrlStateValue,
|
|
matchesBabyObjectMatchCreationUrlRestoreTarget,
|
|
matchesBarkBattleCreationUrlRestoreTarget,
|
|
matchesBigFishCreationUrlRestoreTarget,
|
|
matchesSessionProfileWorkCreationUrlRestoreTarget,
|
|
matchesVisualNovelCreationUrlRestoreTarget,
|
|
normalizeCreationUrlValue,
|
|
resolveCreationUrlRestoreTarget,
|
|
resolveInitialCreationUrlRestoreDecision,
|
|
resolveJumpHopCreationUrlRestoreStage,
|
|
resolveWoodenFishCreationUrlRestoreStage,
|
|
} from './platformCreationUrlStateModel';
|
|
|
|
describe('platformCreationUrlStateModel', () => {
|
|
test('normalizes private creation url state values', () => {
|
|
expect(normalizeCreationUrlValue(' session-1 ')).toBe('session-1');
|
|
expect(normalizeCreationUrlValue(' ')).toBeNull();
|
|
expect(
|
|
hasCreationUrlStateValue({
|
|
sessionId: ' ',
|
|
profileId: null,
|
|
draftId: undefined,
|
|
workId: 'work-1',
|
|
}),
|
|
).toBe(true);
|
|
expect(hasCreationUrlStateValue({})).toBe(false);
|
|
});
|
|
|
|
test('resolves initial creation url restore readiness', () => {
|
|
const readyParams = {
|
|
handled: false,
|
|
pathname: '/creation/puzzle/result',
|
|
state: { sessionId: 'puzzle-session-1' },
|
|
isLoadingPlatform: false,
|
|
canReadProtectedData: true,
|
|
};
|
|
|
|
expect(
|
|
resolveInitialCreationUrlRestoreDecision({
|
|
...readyParams,
|
|
handled: true,
|
|
}),
|
|
).toEqual({ type: 'skip' });
|
|
expect(
|
|
resolveInitialCreationUrlRestoreDecision({
|
|
...readyParams,
|
|
pathname: '/works/detail',
|
|
}),
|
|
).toEqual({ type: 'mark-handled' });
|
|
expect(
|
|
resolveInitialCreationUrlRestoreDecision({
|
|
...readyParams,
|
|
state: {},
|
|
}),
|
|
).toEqual({ type: 'mark-handled' });
|
|
expect(
|
|
resolveInitialCreationUrlRestoreDecision({
|
|
...readyParams,
|
|
isLoadingPlatform: true,
|
|
}),
|
|
).toEqual({ type: 'wait' });
|
|
expect(
|
|
resolveInitialCreationUrlRestoreDecision({
|
|
...readyParams,
|
|
canReadProtectedData: false,
|
|
}),
|
|
).toEqual({ type: 'wait' });
|
|
expect(resolveInitialCreationUrlRestoreDecision(readyParams)).toEqual({
|
|
type: 'restore',
|
|
});
|
|
});
|
|
|
|
test('resolves supported creation url restore targets from paths', () => {
|
|
const state = {
|
|
sessionId: ' session-1 ',
|
|
profileId: ' profile-1 ',
|
|
draftId: ' draft-1 ',
|
|
workId: ' work-1 ',
|
|
};
|
|
const cases = [
|
|
['/creation/big-fish/result', 'big-fish'],
|
|
['/creation/match3d/result', 'match3d'],
|
|
['/creation/square-hole/result', 'square-hole'],
|
|
['/creation/puzzle/result', 'puzzle'],
|
|
['/creation/visual-novel/result', 'visual-novel'],
|
|
['/creation/bark-battle/result', 'bark-battle'],
|
|
['/creation/baby-object-match/result', 'baby-object-match'],
|
|
['/creation/jump-hop/result', 'jump-hop'],
|
|
['/creation/wooden-fish/result', 'wooden-fish'],
|
|
] as const;
|
|
|
|
cases.forEach(([pathname, kind]) => {
|
|
expect(resolveCreationUrlRestoreTarget(pathname, state)).toMatchObject({
|
|
kind,
|
|
sessionId: 'session-1',
|
|
profileId: 'profile-1',
|
|
draftId: 'draft-1',
|
|
workId: 'work-1',
|
|
isGeneratingPath: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
test('normalizes creation url restore target values and generating paths', () => {
|
|
expect(
|
|
resolveCreationUrlRestoreTarget('/creation/jump-hop/generating', {
|
|
sessionId: ' ',
|
|
profileId: ' jump-profile-1 ',
|
|
draftId: undefined,
|
|
workId: null,
|
|
}),
|
|
).toEqual({
|
|
kind: 'jump-hop',
|
|
sessionId: null,
|
|
profileId: 'jump-profile-1',
|
|
draftId: null,
|
|
workId: null,
|
|
isGeneratingPath: true,
|
|
});
|
|
});
|
|
|
|
test('derives big fish restore session from work id when needed', () => {
|
|
expect(
|
|
resolveCreationUrlRestoreTarget('/creation/big-fish/result', {
|
|
workId: 'big-fish-work-river',
|
|
}),
|
|
).toEqual({
|
|
kind: 'big-fish',
|
|
sessionId: null,
|
|
profileId: null,
|
|
draftId: null,
|
|
workId: 'big-fish-work-river',
|
|
isGeneratingPath: false,
|
|
bigFishSessionId: 'river',
|
|
});
|
|
|
|
expect(
|
|
resolveCreationUrlRestoreTarget('/creation/big-fish/result', {
|
|
sessionId: 'big-fish-session-carp',
|
|
workId: 'big-fish-work-river',
|
|
}),
|
|
).toMatchObject({
|
|
kind: 'big-fish',
|
|
bigFishSessionId: 'big-fish-session-carp',
|
|
});
|
|
});
|
|
|
|
test('keeps unsupported creation paths without a concrete restore target', () => {
|
|
expect(
|
|
resolveCreationUrlRestoreTarget('/creation/rpg/result', {
|
|
sessionId: 'rpg-session-1',
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
resolveCreationUrlRestoreTarget('/creation/unknown/result', {
|
|
sessionId: 'unknown-session-1',
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
resolveCreationUrlRestoreTarget('/creation/big-fishery/result', {
|
|
sessionId: 'big-fish-session-1',
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
resolveCreationUrlRestoreTarget('/works/detail', {
|
|
workId: 'work-1',
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
test('matches restore targets against work and draft identities', () => {
|
|
const bigFishTarget = resolveCreationUrlRestoreTarget(
|
|
'/creation/big-fish/result',
|
|
{
|
|
workId: 'big-fish-work-river',
|
|
},
|
|
);
|
|
expect(bigFishTarget?.kind).toBe('big-fish');
|
|
if (bigFishTarget?.kind !== 'big-fish') {
|
|
throw new Error('big fish target expected');
|
|
}
|
|
expect(
|
|
matchesBigFishCreationUrlRestoreTarget(
|
|
{ sourceSessionId: 'river' },
|
|
bigFishTarget,
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
matchesBigFishCreationUrlRestoreTarget(
|
|
{ workId: 'big-fish-work-river' },
|
|
bigFishTarget,
|
|
),
|
|
).toBe(true);
|
|
|
|
const target = {
|
|
sessionId: 'session-1',
|
|
profileId: 'profile-1',
|
|
draftId: 'draft-1',
|
|
workId: 'work-1',
|
|
};
|
|
expect(
|
|
matchesSessionProfileWorkCreationUrlRestoreTarget(
|
|
{ sourceSessionId: 'session-1' },
|
|
target,
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
matchesSessionProfileWorkCreationUrlRestoreTarget(
|
|
{ profileId: 'profile-1' },
|
|
target,
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
matchesSessionProfileWorkCreationUrlRestoreTarget(
|
|
{ workId: 'work-1' },
|
|
target,
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
matchesVisualNovelCreationUrlRestoreTarget(
|
|
{ profileId: 'profile-1' },
|
|
target,
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
matchesBarkBattleCreationUrlRestoreTarget(
|
|
{ draftId: 'draft-1' },
|
|
target,
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
matchesBabyObjectMatchCreationUrlRestoreTarget(
|
|
{ profileId: 'work-1' },
|
|
target,
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
matchesSessionProfileWorkCreationUrlRestoreTarget(
|
|
{ sourceSessionId: null, profileId: null, workId: null },
|
|
{ sessionId: null, profileId: null, workId: null },
|
|
),
|
|
).toBe(false);
|
|
expect(
|
|
matchesBarkBattleCreationUrlRestoreTarget(
|
|
{ workId: null, draftId: null },
|
|
{ workId: null, draftId: null },
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
test('resolves work backed restore stages', () => {
|
|
expect(
|
|
resolveJumpHopCreationUrlRestoreStage({
|
|
isGeneratingPath: true,
|
|
hasRestoredDraft: false,
|
|
hasRestoredWork: true,
|
|
}),
|
|
).toBe('jump-hop-generating');
|
|
expect(
|
|
resolveJumpHopCreationUrlRestoreStage({
|
|
isGeneratingPath: false,
|
|
hasRestoredDraft: false,
|
|
hasRestoredWork: true,
|
|
}),
|
|
).toBe('jump-hop-result');
|
|
expect(
|
|
resolveJumpHopCreationUrlRestoreStage({
|
|
isGeneratingPath: false,
|
|
hasRestoredDraft: false,
|
|
hasRestoredWork: false,
|
|
}),
|
|
).toBe('jump-hop-workspace');
|
|
|
|
expect(
|
|
resolveWoodenFishCreationUrlRestoreStage({
|
|
isGeneratingPath: true,
|
|
hasRestoredDraft: true,
|
|
}),
|
|
).toBe('wooden-fish-generating');
|
|
expect(
|
|
resolveWoodenFishCreationUrlRestoreStage({
|
|
isGeneratingPath: false,
|
|
hasRestoredDraft: true,
|
|
}),
|
|
).toBe('wooden-fish-result');
|
|
expect(
|
|
resolveWoodenFishCreationUrlRestoreStage({
|
|
isGeneratingPath: false,
|
|
hasRestoredDraft: false,
|
|
}),
|
|
).toBe('wooden-fish-workspace');
|
|
});
|
|
|
|
test('builds creation restore state for core session based plays', () => {
|
|
expect(
|
|
buildBigFishCreationUrlState({
|
|
sessionId: ' big-fish-session-1 ',
|
|
} as BigFishSessionSnapshotResponse),
|
|
).toEqual({
|
|
sessionId: 'big-fish-session-1',
|
|
workId: 'big-fish-work-big-fish-session-1',
|
|
});
|
|
|
|
expect(
|
|
buildMatch3DCreationUrlState({
|
|
sessionId: 'match3d-session-1',
|
|
draft: { profileId: 'match3d-profile-draft' },
|
|
} as Match3DAgentSessionSnapshot),
|
|
).toEqual({
|
|
sessionId: 'match3d-session-1',
|
|
profileId: 'match3d-profile-draft',
|
|
workId: 'match3d-profile-draft',
|
|
});
|
|
|
|
expect(
|
|
buildSquareHoleCreationUrlState({
|
|
sessionId: 'square-session-1',
|
|
publishedProfileId: 'square-profile-published',
|
|
} as SquareHoleSessionSnapshot),
|
|
).toEqual({
|
|
sessionId: 'square-session-1',
|
|
profileId: 'square-profile-published',
|
|
workId: 'square-profile-published',
|
|
});
|
|
|
|
expect(
|
|
buildVisualNovelCreationUrlState({
|
|
sessionId: 'visual-session-1',
|
|
draft: { profileId: 'visual-profile-1' },
|
|
} as VisualNovelAgentSessionSnapshot),
|
|
).toEqual({
|
|
sessionId: 'visual-session-1',
|
|
profileId: 'visual-profile-1',
|
|
workId: 'visual-profile-1',
|
|
});
|
|
});
|
|
|
|
test('builds puzzle creation and runtime query state', () => {
|
|
expect(
|
|
buildPuzzleCreationUrlState({
|
|
sessionId: 'puzzle-session-ocean',
|
|
} as PuzzleAgentSessionSnapshot),
|
|
).toEqual({
|
|
sessionId: 'puzzle-session-ocean',
|
|
profileId: 'puzzle-profile-ocean',
|
|
workId: 'puzzle-work-ocean',
|
|
});
|
|
|
|
const draftRuntime = buildPuzzleDraftRuntimeUrlState(
|
|
buildPuzzleWork({
|
|
profileId: 'puzzle-profile-ocean',
|
|
sourceSessionId: null,
|
|
}),
|
|
'level-2',
|
|
);
|
|
expect(draftRuntime).toEqual({
|
|
mode: 'draft',
|
|
runtimeSessionId: 'puzzle-session-ocean',
|
|
runtimeProfileId: 'puzzle-profile-ocean',
|
|
runtimeLevelId: 'level-2',
|
|
});
|
|
expect(hasPuzzleRuntimeUrlStateValue(draftRuntime)).toBe(true);
|
|
expect(buildPuzzleRuntimeUrlStateKey(draftRuntime)).toBe(
|
|
'draft|puzzle-session-ocean|puzzle-profile-ocean|level-2|',
|
|
);
|
|
|
|
const publishedRuntime = buildPuzzlePublishedRuntimeUrlState(
|
|
buildPuzzleWork({ profileId: 'puzzle-profile-ocean' }),
|
|
);
|
|
expect(publishedRuntime.mode).toBe('published');
|
|
expect(publishedRuntime.runtimeProfileId).toBe('puzzle-profile-ocean');
|
|
expect(publishedRuntime.publicWorkCode).toMatch(/^PZ-/u);
|
|
});
|
|
|
|
test('builds creation state for work backed plays with work id priority', () => {
|
|
expect(
|
|
buildJumpHopCreationUrlState({
|
|
session: {
|
|
sessionId: 'jump-session-1',
|
|
draft: { profileId: 'jump-profile-draft' },
|
|
} as JumpHopSessionSnapshotResponse,
|
|
work: {
|
|
summary: {
|
|
profileId: 'jump-profile-work',
|
|
workId: 'jump-work-1',
|
|
},
|
|
} as JumpHopWorkProfileResponse,
|
|
}),
|
|
).toEqual({
|
|
sessionId: 'jump-session-1',
|
|
profileId: 'jump-profile-work',
|
|
workId: 'jump-work-1',
|
|
});
|
|
|
|
expect(
|
|
buildWoodenFishCreationUrlState({
|
|
session: {
|
|
sessionId: 'wood-session-1',
|
|
draft: { profileId: 'wood-profile-draft' },
|
|
} as WoodenFishSessionSnapshotResponse,
|
|
work: {
|
|
summary: {
|
|
profileId: 'wood-profile-work',
|
|
workId: 'wood-work-1',
|
|
},
|
|
} as WoodenFishWorkProfileResponse,
|
|
}),
|
|
).toEqual({
|
|
sessionId: 'wood-session-1',
|
|
profileId: 'wood-profile-work',
|
|
draftId: 'wood-profile-work',
|
|
workId: 'wood-work-1',
|
|
});
|
|
});
|
|
|
|
test('builds creation state for draft backed local plays', () => {
|
|
expect(
|
|
buildBarkBattleCreationUrlState({
|
|
draftId: 'bark-draft-1',
|
|
workId: 'bark-work-1',
|
|
} as BarkBattleDraftConfig),
|
|
).toEqual({
|
|
draftId: 'bark-draft-1',
|
|
workId: 'bark-work-1',
|
|
});
|
|
|
|
expect(
|
|
buildBabyObjectMatchCreationUrlState({
|
|
draftId: 'baby-draft-1',
|
|
profileId: 'baby-profile-1',
|
|
} as BabyObjectMatchDraft),
|
|
).toEqual({
|
|
profileId: 'baby-profile-1',
|
|
draftId: 'baby-draft-1',
|
|
workId: 'baby-profile-1',
|
|
});
|
|
});
|
|
});
|
|
|
|
function buildPuzzleWork(
|
|
overrides: Partial<PuzzleWorkSummary> = {},
|
|
): PuzzleWorkSummary {
|
|
return {
|
|
workId: 'puzzle-work-base',
|
|
profileId: 'puzzle-profile-base',
|
|
ownerUserId: 'user-1',
|
|
sourceSessionId: 'puzzle-session-base',
|
|
authorDisplayName: '测试作者',
|
|
workTitle: '潮雾拼图',
|
|
workDescription: '潮雾港口拼图。',
|
|
levelName: '潮雾拼图',
|
|
summary: '潮雾港口拼图。',
|
|
themeTags: [],
|
|
coverImageSrc: null,
|
|
coverAssetId: null,
|
|
publicationStatus: 'draft',
|
|
updatedAt: '2026-06-03T08:00:00.000Z',
|
|
publishedAt: null,
|
|
playCount: 0,
|
|
remixCount: 0,
|
|
likeCount: 0,
|
|
publishReady: false,
|
|
levels: [],
|
|
...overrides,
|
|
};
|
|
}
|