/* @vitest-environment jsdom */ import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect, test, vi } from 'vitest'; import { buildVisualNovelForbiddenCopyPattern } from './visualNovelForbiddenCopy'; import { mockVisualNovelDraft, mockVisualNovelRun } from './visualNovelMockData'; import { VisualNovelRuntimeShell } from './VisualNovelRuntimeShell'; test('visual novel runtime renders mock play surface and opens panels as dialogs', async () => { const user = userEvent.setup(); render( {}} />, ); expect(screen.getByText('风雪站台')).toBeTruthy(); expect(screen.getAllByText('林遥').length).toBeGreaterThan(0); expect(screen.queryByText(buildVisualNovelForbiddenCopyPattern())).toBeNull(); await user.click(screen.getByRole('button', { name: '历史' })); const dialog = screen.getByRole('dialog', { name: '历史' }); expect(within(dialog).getByText('#1')).toBeTruthy(); }); test('visual novel runtime submits free text action with client event id', async () => { const user = userEvent.setup(); const onSubmitAction = vi.fn(); render( {}} onSubmitAction={onSubmitAction} />, ); await user.type(screen.getByLabelText('输入行动'), '检查广播柜'); await user.click(screen.getByRole('button', { name: '发送行动' })); expect(onSubmitAction).toHaveBeenCalledWith( expect.objectContaining({ actionKind: 'free_text', text: '检查广播柜', }), ); expect(onSubmitAction.mock.calls[0]?.[0].clientEventId).toMatch( /^vn-free-text-/u, ); }); test('visual novel runtime submits choice and continue actions', async () => { const user = userEvent.setup(); const onSubmitAction = vi.fn(); const onContinue = vi.fn(); const runWithoutChoices = { ...mockVisualNovelRun, availableChoices: [], }; const { rerender } = render( {}} onSubmitAction={onSubmitAction} onContinue={onContinue} />, ); await user.click(screen.getByRole('button', { name: '靠近广播柜,确认频段来源。' })); expect(onSubmitAction).toHaveBeenCalledWith( expect.objectContaining({ actionKind: 'choice', choiceId: 'vn-choice-radio', }), ); expect(onSubmitAction.mock.calls[0]?.[0].clientEventId).toMatch( /^vn-choice-/u, ); rerender( {}} onSubmitAction={onSubmitAction} onContinue={onContinue} />, ); await user.click(screen.getByRole('button', { name: '继续' })); expect(onContinue).toHaveBeenCalledTimes(1); }); test('visual novel runtime panels call regeneration and platform archive actions', async () => { const user = userEvent.setup(); const onRegenerateHistoryEntry = vi.fn(); const onSaveRun = vi.fn(); const onResumeSaveArchive = vi.fn(); render( {}} onRegenerateHistoryEntry={onRegenerateHistoryEntry} onSaveRun={onSaveRun} onResumeSaveArchive={onResumeSaveArchive} saveArchives={[ { worldKey: 'visual-novel:archive-1', ownerUserId: 'mock-user', profileId: 'vn-profile-mock-1', worldType: 'visual-novel', worldName: '雪线电台', subtitle: '风雪站台', summaryText: '第 2 回合', coverImageSrc: null, lastPlayedAt: '2026-05-05T12:00:00.000Z', }, ]} />, ); await user.click(screen.getByRole('button', { name: '历史' })); await user.click(screen.getByRole('button', { name: '重生成' })); expect(onRegenerateHistoryEntry).toHaveBeenCalledWith('vn-history-1'); await user.click(screen.getByRole('button', { name: '关闭' })); await user.click(screen.getByRole('button', { name: '存档' })); await user.click(screen.getByRole('button', { name: '保存' })); expect(onSaveRun).toHaveBeenCalledTimes(1); await user.click(screen.getByText('雪线电台')); expect(onResumeSaveArchive).toHaveBeenCalledWith('visual-novel:archive-1'); }); test('visual novel runtime shows raw text only as transient stream text', () => { const transientText = '这是临时流式文本'; render( {}} />, ); expect(screen.getByText(transientText)).toBeTruthy(); const textModeBlocks = screen.queryAllByText((content, element) => { return Boolean( element?.className.toString().includes('whitespace-pre-line') && content.includes(transientText), ); }); expect(textModeBlocks).toHaveLength(0); });