166 lines
5.1 KiB
TypeScript
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);
|
|
});
|