/* @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( {}} 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( {}} 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( {}} 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( {}} 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( {}} 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( {}} 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( {}} 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( {}} 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( {}} 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( {}} 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(); });