461 lines
15 KiB
TypeScript
461 lines
15 KiB
TypeScript
/* @vitest-environment jsdom */
|
||
|
||
import { render, screen, within } from '@testing-library/react';
|
||
import userEvent from '@testing-library/user-event';
|
||
import { afterEach, expect, test, vi } from 'vitest';
|
||
|
||
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
|
||
import { CustomWorldCreationHub } from './CustomWorldCreationHub';
|
||
|
||
const noopCreateType = () => {};
|
||
const originalClipboard = navigator.clipboard;
|
||
|
||
afterEach(() => {
|
||
window.sessionStorage.clear();
|
||
Object.defineProperty(navigator, 'clipboard', {
|
||
configurable: true,
|
||
value: originalClipboard,
|
||
});
|
||
});
|
||
|
||
test('creation hub shows published metric growth from cached page snapshot', async () => {
|
||
window.sessionStorage.setItem(
|
||
'genarrative.creationHub.publishedMetrics.v1',
|
||
JSON.stringify({
|
||
'puzzle:puzzle:work-growth': {
|
||
'play-count': 7,
|
||
'remix-count': 1,
|
||
'like-count': 2,
|
||
},
|
||
}),
|
||
);
|
||
|
||
render(
|
||
<CustomWorldCreationHub
|
||
items={[]}
|
||
puzzleItems={[
|
||
{
|
||
workId: 'puzzle:work-growth',
|
||
profileId: 'puzzle-profile-growth',
|
||
ownerUserId: 'user-1',
|
||
authorDisplayName: '拼图作者',
|
||
levelName: '涨潮拼图',
|
||
summary: '公开指标会从缓存快照涨到最新值。',
|
||
themeTags: ['涨潮'],
|
||
coverImageSrc: null,
|
||
publicationStatus: 'published',
|
||
updatedAt: new Date('2026-04-22T12:00:00.000Z').toISOString(),
|
||
publishedAt: new Date('2026-04-22T12:10:00.000Z').toISOString(),
|
||
playCount: 10,
|
||
remixCount: 4,
|
||
likeCount: 2,
|
||
publishReady: true,
|
||
},
|
||
]}
|
||
loading={false}
|
||
error={null}
|
||
onRetry={() => {}}
|
||
onCreateType={noopCreateType}
|
||
onOpenDraft={() => {}}
|
||
onEnterPublished={() => {}}
|
||
onOpenPuzzleDetail={() => {}}
|
||
/>,
|
||
);
|
||
|
||
expect(screen.getByLabelText('游玩 10次')).toBeTruthy();
|
||
expect(screen.getByLabelText('改造 4次')).toBeTruthy();
|
||
expect(await screen.findAllByText('↑')).toHaveLength(2);
|
||
});
|
||
|
||
const baseDraftItem: CustomWorldWorkSummary = {
|
||
workId: 'draft:session-1',
|
||
sourceType: 'agent_session',
|
||
status: 'draft',
|
||
title: '潮雾列岛',
|
||
subtitle: '补齐关键锚点',
|
||
summary: '玩家是失职返乡的守灯人。',
|
||
coverImageSrc: null,
|
||
updatedAt: new Date('2026-04-14T10:00:00.000Z').toISOString(),
|
||
publishedAt: null,
|
||
stage: 'object_refining',
|
||
stageLabel: '待完善草稿',
|
||
playableNpcCount: 3,
|
||
landmarkCount: 4,
|
||
sessionId: 'session-1',
|
||
profileId: null,
|
||
canResume: true,
|
||
canEnterWorld: false,
|
||
};
|
||
|
||
test('creation hub reflects updated draft title summary and counts after rerender', () => {
|
||
const { rerender } = render(
|
||
<CustomWorldCreationHub
|
||
items={[baseDraftItem]}
|
||
loading={false}
|
||
error={null}
|
||
onRetry={() => {}}
|
||
onCreateType={noopCreateType}
|
||
onOpenDraft={() => {}}
|
||
onEnterPublished={() => {}}
|
||
/>,
|
||
);
|
||
|
||
expect(screen.getByText('潮雾列岛')).toBeTruthy();
|
||
expect(screen.getByText('玩家是失职返乡的守灯人。')).toBeTruthy();
|
||
expect(screen.queryByText('角色 3')).toBeNull();
|
||
expect(screen.queryByText('地点 4')).toBeNull();
|
||
const rpgButton = screen.getByRole('button', { name: /角色扮演/u });
|
||
const puzzleButton = screen.getByRole('button', { name: /拼图.*创意礼物/u });
|
||
const match3dButton = screen.getByRole('button', { name: /抓大鹅/u });
|
||
expect(
|
||
rpgButton.compareDocumentPosition(puzzleButton) &
|
||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||
).toBeTruthy();
|
||
expect((rpgButton as HTMLButtonElement).disabled).toBe(false);
|
||
expect((match3dButton as HTMLButtonElement).disabled).toBe(false);
|
||
expect(
|
||
within(match3dButton).getAllByText('经典消除玩法').length,
|
||
).toBeGreaterThan(0);
|
||
expect(puzzleButton).toBeTruthy();
|
||
expect(screen.queryByRole('button', { name: /大鱼吃小鱼/u })).toBeNull();
|
||
|
||
rerender(
|
||
<CustomWorldCreationHub
|
||
items={[
|
||
{
|
||
...baseDraftItem,
|
||
title: '潮雾列岛·回潮版',
|
||
summary: '世界总卡和角色网已经继续长出了新的支线。',
|
||
playableNpcCount: 5,
|
||
landmarkCount: 6,
|
||
updatedAt: new Date('2026-04-14T10:10:00.000Z').toISOString(),
|
||
},
|
||
]}
|
||
loading={false}
|
||
error={null}
|
||
onRetry={() => {}}
|
||
onCreateType={noopCreateType}
|
||
onOpenDraft={() => {}}
|
||
onEnterPublished={() => {}}
|
||
/>,
|
||
);
|
||
|
||
expect(screen.getByText('潮雾列岛·回潮版')).toBeTruthy();
|
||
expect(
|
||
screen.getByText('世界总卡和角色网已经继续长出了新的支线。'),
|
||
).toBeTruthy();
|
||
expect(screen.queryByText('角色 5')).toBeNull();
|
||
expect(screen.queryByText('地点 6')).toBeNull();
|
||
});
|
||
|
||
test('creation hub mixes puzzle works into the same grid and uses puzzle tag to distinguish', () => {
|
||
render(
|
||
<CustomWorldCreationHub
|
||
items={[baseDraftItem]}
|
||
puzzleItems={[
|
||
{
|
||
workId: 'puzzle:work-1',
|
||
profileId: 'puzzle-profile-1',
|
||
ownerUserId: 'user-1',
|
||
authorDisplayName: '拼图作者',
|
||
levelName: '沉钟拼图',
|
||
summary: '拼图作品会与其他创作作品一起展示。',
|
||
themeTags: ['潮雾', '沉钟'],
|
||
coverImageSrc: null,
|
||
publicationStatus: 'published',
|
||
updatedAt: new Date('2026-04-22T12:00:00.000Z').toISOString(),
|
||
publishedAt: new Date('2026-04-22T12:10:00.000Z').toISOString(),
|
||
playCount: 8,
|
||
remixCount: 2,
|
||
likeCount: 3,
|
||
publishReady: true,
|
||
},
|
||
]}
|
||
loading={false}
|
||
error={null}
|
||
onRetry={() => {}}
|
||
onCreateType={noopCreateType}
|
||
onOpenDraft={() => {}}
|
||
onEnterPublished={() => {}}
|
||
onOpenPuzzleDetail={() => {}}
|
||
/>,
|
||
);
|
||
|
||
expect(screen.getByText('潮雾列岛')).toBeTruthy();
|
||
expect(screen.getByText('沉钟拼图')).toBeTruthy();
|
||
expect(screen.getAllByText('拼图').length).toBeGreaterThan(0);
|
||
expect(screen.getByLabelText('游玩 8次')).toBeTruthy();
|
||
expect(screen.getByLabelText('改造 2次')).toBeTruthy();
|
||
expect(screen.getByLabelText('点赞 3赞')).toBeTruthy();
|
||
expect(screen.queryByText('Remix')).toBeNull();
|
||
expect(screen.queryByText('PZ-PROFILE1')).toBeNull();
|
||
expect(screen.queryByText('潮雾')).toBeNull();
|
||
expect(screen.queryByText('沉钟')).toBeNull();
|
||
expect(screen.queryByText('我的拼图作品')).toBeNull();
|
||
});
|
||
|
||
test('creation hub shows puzzle point incentive and claims without opening card', async () => {
|
||
const user = userEvent.setup();
|
||
const onClaimPuzzlePointIncentive = vi.fn();
|
||
const onOpenPuzzleDetail = vi.fn();
|
||
|
||
render(
|
||
<CustomWorldCreationHub
|
||
items={[]}
|
||
puzzleItems={[
|
||
{
|
||
workId: 'puzzle:work-incentive',
|
||
profileId: 'puzzle-profile-incentive',
|
||
ownerUserId: 'user-1',
|
||
authorDisplayName: '拼图作者',
|
||
levelName: '百梦灯塔',
|
||
summary: '拼图作品会展示积分激励。',
|
||
themeTags: ['灯塔', '百梦'],
|
||
coverImageSrc: null,
|
||
publicationStatus: 'published',
|
||
updatedAt: new Date('2026-05-01T12:00:00.000Z').toISOString(),
|
||
publishedAt: new Date('2026-05-01T12:10:00.000Z').toISOString(),
|
||
playCount: 8,
|
||
remixCount: 2,
|
||
likeCount: 3,
|
||
pointIncentiveTotalHalfPoints: 5,
|
||
pointIncentiveClaimedPoints: 1,
|
||
pointIncentiveTotalPoints: 2.5,
|
||
pointIncentiveClaimablePoints: 1,
|
||
publishReady: true,
|
||
},
|
||
]}
|
||
loading={false}
|
||
error={null}
|
||
onRetry={() => {}}
|
||
onCreateType={noopCreateType}
|
||
onOpenDraft={() => {}}
|
||
onEnterPublished={() => {}}
|
||
onOpenPuzzleDetail={onOpenPuzzleDetail}
|
||
onClaimPuzzlePointIncentive={onClaimPuzzlePointIncentive}
|
||
/>,
|
||
);
|
||
|
||
expect(screen.getByLabelText('积分激励总数 2.5 光点')).toBeTruthy();
|
||
expect(screen.getByLabelText('待领取积分 1 光点')).toBeTruthy();
|
||
|
||
await user.click(screen.getByRole('button', { name: '领取积分' }));
|
||
|
||
expect(onClaimPuzzlePointIncentive).toHaveBeenCalledWith(
|
||
expect.objectContaining({ profileId: 'puzzle-profile-incentive' }),
|
||
);
|
||
expect(onOpenPuzzleDetail).not.toHaveBeenCalled();
|
||
});
|
||
|
||
test('creation hub shows RPG public work code from published library entry', () => {
|
||
render(
|
||
<CustomWorldCreationHub
|
||
items={[
|
||
{
|
||
...baseDraftItem,
|
||
workId: 'published:world-public-1',
|
||
sourceType: 'published_profile',
|
||
status: 'published',
|
||
title: '潮雾列岛已发布版',
|
||
profileId: 'world-public-1',
|
||
canResume: false,
|
||
canEnterWorld: true,
|
||
},
|
||
]}
|
||
rpgLibraryEntries={[
|
||
{
|
||
ownerUserId: 'user-1',
|
||
profileId: 'world-public-1',
|
||
publicWorkCode: 'CW-00000001',
|
||
authorPublicUserCode: 'SY-00000001',
|
||
profile: {} as never,
|
||
visibility: 'published',
|
||
publishedAt: '2026-04-20T10:00:00.000Z',
|
||
updatedAt: '2026-04-20T10:00:00.000Z',
|
||
authorDisplayName: '测试玩家',
|
||
worldName: '潮雾列岛已发布版',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summaryText: '已经发布的群岛世界作品。',
|
||
coverImageSrc: null,
|
||
themeMode: 'tide',
|
||
playableNpcCount: 3,
|
||
landmarkCount: 4,
|
||
playCount: 12,
|
||
remixCount: 4,
|
||
likeCount: 5,
|
||
},
|
||
]}
|
||
loading={false}
|
||
error={null}
|
||
onRetry={() => {}}
|
||
onCreateType={noopCreateType}
|
||
onOpenDraft={() => {}}
|
||
onEnterPublished={() => {}}
|
||
/>,
|
||
);
|
||
|
||
expect(screen.getByText('潮雾列岛已发布版')).toBeTruthy();
|
||
expect(screen.getByLabelText('游玩 12次')).toBeTruthy();
|
||
expect(screen.getByLabelText('改造 4次')).toBeTruthy();
|
||
expect(screen.getByLabelText('点赞 5赞')).toBeTruthy();
|
||
expect(screen.queryByText('Remix')).toBeNull();
|
||
expect(screen.queryByText('CW-00000001')).toBeNull();
|
||
});
|
||
|
||
test('creation hub shows delete action for persisted rpg drafts', () => {
|
||
render(
|
||
<CustomWorldCreationHub
|
||
items={[{ ...baseDraftItem, profileId: 'profile-1' }]}
|
||
loading={false}
|
||
error={null}
|
||
onRetry={() => {}}
|
||
onCreateType={noopCreateType}
|
||
onOpenDraft={() => {}}
|
||
onEnterPublished={() => {}}
|
||
onDeletePublished={() => {}}
|
||
/>,
|
||
);
|
||
|
||
expect(screen.getByRole('button', { name: '删除' })).toBeTruthy();
|
||
});
|
||
|
||
test('creation hub published work delete action is available beside share without opening card', async () => {
|
||
const user = userEvent.setup();
|
||
const onDeletePuzzle = vi.fn();
|
||
const onOpenPuzzleDetail = vi.fn();
|
||
|
||
render(
|
||
<CustomWorldCreationHub
|
||
items={[]}
|
||
puzzleItems={[
|
||
{
|
||
workId: 'puzzle:work-delete',
|
||
profileId: 'puzzle-profile-delete',
|
||
ownerUserId: 'user-1',
|
||
authorDisplayName: '拼图作者',
|
||
levelName: '待删拼图',
|
||
summary: '已发布作品也可以从创作页删除。',
|
||
themeTags: ['灯塔'],
|
||
coverImageSrc: null,
|
||
publicationStatus: 'published',
|
||
updatedAt: new Date('2026-05-02T12:00:00.000Z').toISOString(),
|
||
publishedAt: new Date('2026-05-02T12:10:00.000Z').toISOString(),
|
||
playCount: 8,
|
||
remixCount: 2,
|
||
likeCount: 1,
|
||
publishReady: true,
|
||
},
|
||
]}
|
||
loading={false}
|
||
error={null}
|
||
onRetry={() => {}}
|
||
onCreateType={noopCreateType}
|
||
onOpenDraft={() => {}}
|
||
onEnterPublished={() => {}}
|
||
onOpenPuzzleDetail={onOpenPuzzleDetail}
|
||
onDeletePuzzle={onDeletePuzzle}
|
||
/>,
|
||
);
|
||
|
||
expect(screen.getByRole('button', { name: '删除' })).toBeTruthy();
|
||
expect(screen.getByRole('button', { name: '分享' })).toBeTruthy();
|
||
|
||
await user.click(screen.getByRole('button', { name: '删除' }));
|
||
|
||
expect(onDeletePuzzle).toHaveBeenCalledWith(
|
||
expect.objectContaining({ profileId: 'puzzle-profile-delete' }),
|
||
);
|
||
expect(onOpenPuzzleDetail).not.toHaveBeenCalled();
|
||
});
|
||
|
||
test('creation hub opens persisted rpg drafts by card click', async () => {
|
||
const user = userEvent.setup();
|
||
const openedItems: CustomWorldWorkSummary[] = [];
|
||
const persistedDraft = {
|
||
...baseDraftItem,
|
||
workId: 'draft:profile-1',
|
||
sourceType: 'published_profile' as const,
|
||
sessionId: null,
|
||
profileId: 'profile-1',
|
||
title: '可继续整理的草稿',
|
||
};
|
||
|
||
render(
|
||
<CustomWorldCreationHub
|
||
items={[persistedDraft]}
|
||
loading={false}
|
||
error={null}
|
||
onRetry={() => {}}
|
||
onCreateType={noopCreateType}
|
||
onOpenDraft={(item) => {
|
||
openedItems.push(item);
|
||
}}
|
||
onEnterPublished={() => {}}
|
||
/>,
|
||
);
|
||
|
||
await user.click(
|
||
screen.getByRole('button', { name: /继续完善《可继续整理的草稿》/u }),
|
||
);
|
||
|
||
expect(openedItems).toEqual([persistedDraft]);
|
||
});
|
||
|
||
test('creation hub published share button copies share text without opening the card', async () => {
|
||
const user = userEvent.setup();
|
||
const writeText = vi.fn(async () => undefined);
|
||
const onOpenPuzzleDetail = vi.fn();
|
||
Object.defineProperty(navigator, 'clipboard', {
|
||
configurable: true,
|
||
value: { writeText },
|
||
});
|
||
|
||
render(
|
||
<CustomWorldCreationHub
|
||
items={[]}
|
||
puzzleItems={[
|
||
{
|
||
workId: 'puzzle:work-1',
|
||
profileId: 'puzzle-profile-1',
|
||
ownerUserId: 'user-1',
|
||
authorDisplayName: '拼图作者',
|
||
levelName: '沉钟拼图',
|
||
summary: '拼图作品会与其他创作作品一起展示。',
|
||
themeTags: ['潮雾', '沉钟'],
|
||
coverImageSrc: null,
|
||
publicationStatus: 'published',
|
||
updatedAt: new Date('2026-04-22T12:00:00.000Z').toISOString(),
|
||
publishedAt: new Date('2026-04-22T12:10:00.000Z').toISOString(),
|
||
playCount: 8,
|
||
remixCount: 2,
|
||
likeCount: 0,
|
||
publishReady: true,
|
||
},
|
||
]}
|
||
loading={false}
|
||
error={null}
|
||
onRetry={() => {}}
|
||
onCreateType={noopCreateType}
|
||
onOpenDraft={() => {}}
|
||
onEnterPublished={() => {}}
|
||
onOpenPuzzleDetail={onOpenPuzzleDetail}
|
||
/>,
|
||
);
|
||
|
||
await user.click(screen.getByRole('button', { name: '分享' }));
|
||
|
||
expect(writeText).toHaveBeenCalledWith(
|
||
expect.stringContaining('邀请你来玩《沉钟拼图》'),
|
||
);
|
||
expect(writeText).toHaveBeenCalledWith(
|
||
expect.stringContaining('作品号:PZ-PROFILE1'),
|
||
);
|
||
expect(writeText).toHaveBeenCalledWith(
|
||
expect.stringContaining('/gallery/puzzle/detail?work=PZ-PROFILE1'),
|
||
);
|
||
expect(onOpenPuzzleDetail).not.toHaveBeenCalled();
|
||
expect(
|
||
await screen.findByRole('button', { name: '分享内容已复制' }),
|
||
).toBeTruthy();
|
||
});
|