Files
Genarrative/src/components/visual-novel-runtime/VisualNovelRuntimeShell.test.tsx
2026-05-08 11:44:42 +08:00

166 lines
5.1 KiB
TypeScript

/* @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(
<VisualNovelRuntimeShell
draft={mockVisualNovelDraft}
run={mockVisualNovelRun}
onBack={() => {}}
/>,
);
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(
<VisualNovelRuntimeShell
draft={mockVisualNovelDraft}
run={mockVisualNovelRun}
onBack={() => {}}
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(
<VisualNovelRuntimeShell
draft={mockVisualNovelDraft}
run={mockVisualNovelRun}
onBack={() => {}}
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(
<VisualNovelRuntimeShell
draft={mockVisualNovelDraft}
run={runWithoutChoices}
onBack={() => {}}
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(
<VisualNovelRuntimeShell
draft={mockVisualNovelDraft}
run={mockVisualNovelRun}
onBack={() => {}}
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(
<VisualNovelRuntimeShell
draft={mockVisualNovelDraft}
run={mockVisualNovelRun}
streamingText={transientText}
onBack={() => {}}
/>,
);
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);
});