Files
Genarrative/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx
2026-05-22 05:00:07 +08:00

899 lines
28 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 { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, expect, test, vi } from 'vitest';
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
import type { CreationEntryConfig } from '../../services/creationEntryConfigService';
import { derivePlatformCreationTypes } from '../platform-entry/platformEntryCreationTypes';
import { CustomWorldCreationHub } from './CustomWorldCreationHub';
const noopCreateType = () => {};
const testEntryConfig = {
startCard: {
title: '新建作品',
description: '选择模板后进入对应的创作表单。',
idleBadge: '模板 Tab',
busyBadge: '正在开启',
},
typeModal: {
title: '选择创作类型',
description: '先选玩法类型,再进入对应创作工作台。',
},
creationTypes: [
{
id: 'puzzle',
title: '拼图',
subtitle: '拼图关卡创作',
badge: '可创建',
imageSrc: '/creation-type-references/puzzle.webp',
visible: true,
open: true,
sortOrder: 30,
updatedAtMicros: 1,
},
{
id: 'match3d',
title: '抓大鹅',
subtitle: '3D 消除关卡',
badge: '可创建',
imageSrc: '/creation-type-references/match3d.webp',
visible: true,
open: true,
sortOrder: 40,
updatedAtMicros: 1,
},
{
id: 'square-hole',
title: '方洞',
subtitle: '形状投放挑战',
badge: '可创建',
imageSrc: '/creation-type-references/square-hole.webp',
visible: false,
open: true,
sortOrder: 50,
updatedAtMicros: 1,
},
{
id: 'visual-novel',
title: '视觉小说',
subtitle: '分支叙事体验',
badge: '敬请期待',
imageSrc: '/creation-type-references/visual-novel.webp',
visible: false,
open: false,
sortOrder: 60,
updatedAtMicros: 1,
},
{
id: 'airp',
title: 'AI RPG',
subtitle: '原生角色扮演',
badge: '即将开放',
imageSrc: '/creation-type-references/airp.webp',
visible: true,
open: false,
sortOrder: 70,
updatedAtMicros: 1,
},
],
} satisfies CreationEntryConfig;
const testCreationTypes = derivePlatformCreationTypes(
testEntryConfig.creationTypes,
);
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={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
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,
};
const hiddenSquareHoleItem: SquareHoleWorkSummary = {
workId: 'square-hole:work-hidden',
profileId: 'square-hole-profile-hidden',
ownerUserId: 'user-1',
gameName: '隐藏方洞挑战',
themeText: '方洞',
twistRule: '隐藏入口',
summary: '入口隐藏后,这条作品不应出现在创作页作品架。',
tags: ['方洞'],
coverImageSrc: null,
backgroundPrompt: '',
backgroundImageSrc: null,
shapeOptions: [],
holeOptions: [],
shapeCount: 0,
difficulty: 1,
publicationStatus: 'draft',
playCount: 0,
updatedAt: new Date('2026-05-10T10:00:00.000Z').toISOString(),
publishedAt: null,
publishReady: false,
sourceSessionId: 'square-hole-session-hidden',
};
const babyObjectMatchDraftItem: BabyObjectMatchDraft = {
draftId: 'baby-object-draft-delete',
profileId: 'baby-object-profile-delete',
templateId: 'baby-object-match',
templateName: '宝贝识物',
workTitle: '宝贝识物删除测试',
workDescription: '苹果和香蕉识物分类',
itemNames: ['苹果', '香蕉'],
itemAssets: [
{
itemId: 'baby-object-item-1',
itemName: '苹果',
imageSrc: '/apple.png',
assetObjectId: null,
generationProvider: 'placeholder',
prompt: '苹果',
},
{
itemId: 'baby-object-item-2',
itemName: '香蕉',
imageSrc: '/banana.png',
assetObjectId: null,
generationProvider: 'placeholder',
prompt: '香蕉',
},
],
visualPackage: null,
themeTags: ['寓教于乐'],
publicationStatus: 'draft',
createdAt: new Date('2026-05-11T10:00:00.000Z').toISOString(),
updatedAt: new Date('2026-05-11T10:00:00.000Z').toISOString(),
publishedAt: null,
};
const barkBattleDraftItem: BarkBattleWorkSummary = {
workId: 'bark-battle-work-draft-visible',
draftId: 'bark-battle-draft-visible',
ownerUserId: 'user-1',
authorDisplayName: '声浪作者',
title: '竖屏声浪草稿',
summary: '生成完成后也必须留在我的草稿里。',
themeDescription: '霓虹竖屏擂台',
playerImageDescription: '红围巾选手',
opponentImageDescription: '蓝头带对手',
onomatopoeia: ['炸场', '破阵'],
playerCharacterImageSrc: '/bark/player.png',
opponentCharacterImageSrc: '/bark/opponent.png',
uiBackgroundImageSrc: '/bark/background.png',
difficultyPreset: 'normal',
status: 'draft',
generationStatus: 'ready',
publishReady: true,
playCount: 0,
updatedAt: '2026-05-21T10:00:00.000Z',
publishedAt: null,
};
const barkBattlePublishedItem: BarkBattleWorkSummary = {
...barkBattleDraftItem,
workId: 'bark-battle-work-published-visible',
draftId: 'bark-battle-draft-published-visible',
title: '竖屏声浪已发布',
summary: '发布完成后必须留在已发布作品里。',
authorDisplayName: '发布作者',
status: 'published',
playCount: 9,
updatedAt: '2026-05-21T10:10:00.000Z',
publishedAt: '2026-05-21T10:10:00.000Z',
};
test('creation hub reflects updated draft title summary and counts after rerender', async () => {
const user = userEvent.setup();
const onCreateType = vi.fn();
const { rerender } = render(
<CustomWorldCreationHub
items={[baseDraftItem]}
loading={false}
error={null}
onRetry={() => {}}
onCreateType={onCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
expect(screen.getByText('潮雾列岛')).toBeTruthy();
expect(screen.getByText('玩家是失职返乡的守灯人。')).toBeTruthy();
expect(screen.queryByText('角色 3')).toBeNull();
expect(screen.queryByText('地点 4')).toBeNull();
const puzzleButton = screen.getByRole('button', {
name: /.*/u,
});
const match3dButton = screen.getByRole('button', {
name: /.*3D /u,
});
expect(puzzleButton).toBeTruthy();
expect(match3dButton).toBeTruthy();
expect((puzzleButton as HTMLButtonElement).disabled).toBe(false);
expect((match3dButton as HTMLButtonElement).disabled).toBe(false);
expect(screen.queryByRole('button', { name: //u })).toBeNull();
expect(screen.queryByText('反直觉形状分拣')).toBeNull();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
await user.click(match3dButton);
expect(onCreateType).toHaveBeenCalledWith('match3d');
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={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
expect(screen.getByText('潮雾列岛·回潮版')).toBeTruthy();
expect(
screen.getByText('世界总卡和角色网已经继续长出了新的支线。'),
).toBeTruthy();
expect(screen.queryByText('角色 5')).toBeNull();
expect(screen.queryByText('地点 6')).toBeNull();
});
test('creation hub hides square hole works when the creation type is hidden', () => {
const onOpenSquareHoleDetail = vi.fn();
render(
<CustomWorldCreationHub
items={[]}
squareHoleItems={[hiddenSquareHoleItem]}
loading={false}
error={null}
onRetry={() => {}}
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
onOpenSquareHoleDetail={onOpenSquareHoleDetail}
/>,
);
expect(screen.queryByText('隐藏方洞挑战')).toBeNull();
expect(
screen.queryByText('入口隐藏后,这条作品不应出现在创作页作品架。'),
).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={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
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}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
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={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
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 hides persisted draft delete action behind swipe underlay', () => {
const { container } = render(
<CustomWorldCreationHub
items={[{ ...baseDraftItem, profileId: 'profile-1' }]}
loading={false}
error={null}
onRetry={() => {}}
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
onDeletePublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
expect(
container.querySelector('.creation-work-card__swipe-underlay'),
).toBeTruthy();
expect(screen.queryByRole('button', { name: '删除' })).toBeNull();
});
test('creation hub reveals persisted draft delete action from left swipe', () => {
const { container } = render(
<CustomWorldCreationHub
items={[{ ...baseDraftItem, profileId: 'profile-1' }]}
loading={false}
error={null}
onRetry={() => {}}
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
onDeletePublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
const card = screen.getByRole('button', { name: //u });
fireEvent.touchStart(card, {
touches: [{ clientX: 180, clientY: 20 }],
});
fireEvent.touchMove(card, {
touches: [{ clientX: 92, clientY: 22 }],
});
fireEvent.touchEnd(card);
expect(screen.getByRole('button', { name: '删除' })).toBeTruthy();
expect(
container.querySelector('.creation-work-card-shell--actions-visible'),
).toBeTruthy();
});
test('creation hub reveals persisted draft delete action from keyboard', async () => {
const user = userEvent.setup();
render(
<CustomWorldCreationHub
items={[{ ...baseDraftItem, profileId: 'profile-1' }]}
loading={false}
error={null}
onRetry={() => {}}
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
onDeletePublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
screen.getByRole('button', { name: //u }).focus();
await user.keyboard('{ArrowLeft}');
expect(screen.getByRole('button', { name: '删除' })).toBeTruthy();
expect(screen.queryByRole('button', { name: '分享' })).toBeNull();
});
test('creation hub shows delete action for baby object match drafts', async () => {
const user = userEvent.setup();
const onDeleteBabyObjectMatch = vi.fn();
const onOpenBabyObjectMatchDetail = vi.fn();
render(
<CustomWorldCreationHub
items={[]}
babyObjectMatchItems={[babyObjectMatchDraftItem]}
loading={false}
error={null}
onRetry={() => {}}
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
onOpenBabyObjectMatchDetail={onOpenBabyObjectMatchDetail}
onDeleteBabyObjectMatch={onDeleteBabyObjectMatch}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
screen.getByRole('button', { name: //u }).focus();
await user.keyboard('{ArrowLeft}');
await user.click(screen.getByRole('button', { name: '删除' }));
expect(onDeleteBabyObjectMatch).toHaveBeenCalledWith(
babyObjectMatchDraftItem,
);
expect(onOpenBabyObjectMatchDetail).not.toHaveBeenCalled();
});
test('creation hub works-only tab filters bark battle draft and published works', async () => {
const user = userEvent.setup();
const onOpenBarkBattleDetail = vi.fn();
render(
<CustomWorldCreationHub
mode="works-only"
items={[]}
barkBattleItems={[barkBattleDraftItem, barkBattlePublishedItem]}
loading={false}
error={null}
onRetry={() => {}}
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
onOpenBarkBattleDetail={onOpenBarkBattleDetail}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
expect(screen.getByRole('button', { name: '全部 2' })).toBeTruthy();
expect(screen.getByRole('button', { name: '草稿 1' })).toBeTruthy();
expect(screen.getByRole('button', { name: '已发布 1' })).toBeTruthy();
expect(screen.getByText('竖屏声浪草稿')).toBeTruthy();
expect(screen.getByText('竖屏声浪已发布')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '草稿 1' }));
expect(screen.getByText('竖屏声浪草稿')).toBeTruthy();
expect(screen.queryByText('竖屏声浪已发布')).toBeNull();
await user.click(screen.getByRole('button', { name: '已发布 1' }));
expect(screen.queryByText('竖屏声浪草稿')).toBeNull();
expect(screen.getByText('竖屏声浪已发布')).toBeTruthy();
await user.click(
screen.getByRole('button', { name: //u }),
);
expect(onOpenBarkBattleDetail).toHaveBeenCalledWith(barkBattlePublishedItem);
});
test('creation hub published work delete action is revealed 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}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
expect(screen.queryByRole('button', { name: '删除' })).toBeNull();
expect(screen.getByRole('button', { name: '分享' })).toBeTruthy();
screen.getByRole('button', { name: //u }).focus();
await user.keyboard('{ArrowLeft}');
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={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
await user.click(
screen.getByRole('button', { name: /稿/u }),
);
expect(openedItems).toEqual([persistedDraft]);
});
test('creation hub published share icon 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}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
const shareButton = screen.getByRole('button', { name: '分享' });
expect(shareButton).toBeTruthy();
expect(screen.queryByText('删除')).toBeNull();
await user.click(shareButton);
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();
});
test('creation hub published share icon is shown directly on the card header', () => {
render(
<CustomWorldCreationHub
items={[]}
puzzleItems={[
{
workId: 'puzzle:work-share-icon',
profileId: 'puzzle-profile-share-icon',
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={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
expect(screen.getByRole('button', { name: '分享' })).toBeTruthy();
expect(screen.queryByRole('button', { name: '删除' })).toBeNull();
});
test('creation hub left swipe draft reveals delete without opening card', () => {
const onDeletePublished = vi.fn();
const onOpenDraft = vi.fn();
render(
<CustomWorldCreationHub
items={[{ ...baseDraftItem, profileId: 'profile-1' }]}
loading={false}
error={null}
onRetry={() => {}}
onCreateType={noopCreateType}
onOpenDraft={onOpenDraft}
onEnterPublished={() => {}}
onDeletePublished={onDeletePublished}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
const card = screen.getByRole('button', { name: //u });
fireEvent.touchStart(card, {
touches: [{ clientX: 180, clientY: 20 }],
});
fireEvent.touchMove(card, {
touches: [{ clientX: 88, clientY: 22 }],
});
fireEvent.touchEnd(card);
expect(screen.getByRole('button', { name: '删除' })).toBeTruthy();
expect(onOpenDraft).not.toHaveBeenCalled();
});