Files
Genarrative/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx
2026-05-02 17:56:42 +08:00

461 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* @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();
});