From 5d3e264cf3a8ff1c7d49dcce209ba457a51df433 Mon Sep 17 00:00:00 2001 From: kun1h Date: Mon, 6 Jul 2026 21:45:18 +0800 Subject: [PATCH] test: complete P4.1 LM Studio coverage and fix auto/wizard handoff Co-authored-by: Cursor --- e2e/auto-writing.spec.ts | 84 +++++++++++++++++ e2e/wizard-writing.spec.ts | 74 +++++++++++++++ src/renderer/components/ai/AutoPanel.tsx | 2 +- src/renderer/components/layout/RightPanel.tsx | 8 +- src/renderer/stores/useAutoStore.ts | 11 ++- src/renderer/stores/useWizardStore.ts | 9 +- tests/main/auto-generate.test.ts | 70 +++++++++++++++ tests/main/auto-plan.test.ts | 31 +++++++ tests/main/wizard-generate.test.ts | 89 +++++++++++++++++++ tests/main/wizard-preview.test.ts | 40 +++++++++ vitest.config.ts | 3 +- 11 files changed, 415 insertions(+), 6 deletions(-) create mode 100644 tests/main/auto-generate.test.ts create mode 100644 tests/main/auto-plan.test.ts create mode 100644 tests/main/wizard-generate.test.ts create mode 100644 tests/main/wizard-preview.test.ts diff --git a/e2e/auto-writing.spec.ts b/e2e/auto-writing.spec.ts index 75548da..3b9b241 100644 --- a/e2e/auto-writing.spec.ts +++ b/e2e/auto-writing.spec.ts @@ -20,6 +20,53 @@ async function skipOnboarding(page: Page): Promise { } } +async function ensureChapterEditor(page: Page): Promise { + await page.getByRole('button', { name: '+ 新章' }).click() + await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 }) +} + +async function openBookWithContent(page: Page): Promise { + await page.getByRole('button', { name: /新建书籍/ }).first().click() + await page.locator('.dialog-content input').first().fill('自动测试书') + await page.getByRole('button', { name: '创建' }).click() + await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 }) + await ensureChapterEditor(page) + const editor = page.locator('.ProseMirror') + await editor.click() + await editor.pressSequentially('林远站在演武场中央,众人目光汇聚。') + await page.keyboard.press('Control+s') + await expect(page.getByText('已保存')).toBeVisible({ timeout: 15_000 }) +} + +async function openAutoPanel(page: Page): Promise { + await page.getByTestId('panel-tab-ai').click() + await page.getByTestId('ai-session-new').click() + await expect(page.getByTestId('ai-mode-tab-auto')).toBeEnabled({ timeout: 10_000 }) + await page.getByTestId('ai-mode-tab-auto').click() + await expect(page.getByTestId('auto-panel')).toBeVisible({ timeout: 10_000 }) +} + +async function planAutoScenes(page: Page): Promise { + await page.getByTestId('auto-goal-input').fill('林远通过入门考核,展现天赋震惊众人') + await page.getByTestId('auto-plan-scenes').click() + await expect(page.getByTestId('auto-start-generate')).toBeVisible({ timeout: 240_000 }) +} + +async function generateOneAutoScene(page: Page): Promise { + await page.getByTestId('auto-start-generate').click() + const naming0 = page.getByTestId('naming-option-0') + const finishBtn = page.getByTestId('auto-finish-chapter') + await expect(naming0.or(finishBtn).or(page.getByTestId('auto-start-generate'))).toBeVisible({ + timeout: 240_000 + }) + if (await naming0.isVisible()) { + await naming0.click() + await expect(finishBtn.or(page.getByTestId('auto-start-generate'))).toBeVisible({ + timeout: 240_000 + }) + } +} + test.describe('Auto writing', () => { let userDataDir: string @@ -49,4 +96,41 @@ test.describe('Auto writing', () => { await expect(page.getByTestId('ai-mode-tab-auto')).toBeDisabled({ timeout: 10_000 }) await app.close() }) + + test('E2E-AUTO-01: auto flow creates chapter', async () => { + test.setTimeout(300_000) + const app = await launchApp(userDataDir) + const page = await app.firstWindow() + await skipOnboarding(page) + await openBookWithContent(page) + await openAutoPanel(page) + await planAutoScenes(page) + await generateOneAutoScene(page) + await expect(page.getByTestId('auto-finish-chapter')).toBeVisible({ timeout: 30_000 }) + await page.getByTestId('auto-finish-chapter').click() + await expect(page.locator('.ProseMirror')).not.toBeEmpty({ timeout: 30_000 }) + await app.close() + }) + + test('E2E-AUTO-02: pause and handoff to interactive', async () => { + test.setTimeout(300_000) + const app = await launchApp(userDataDir) + const page = await app.firstWindow() + await skipOnboarding(page) + await openBookWithContent(page) + await openAutoPanel(page) + await planAutoScenes(page) + await page.getByTestId('auto-start-generate').click() + await expect(page.getByTestId('auto-pause')).toBeEnabled({ timeout: 15_000 }) + await page.waitForTimeout(2_000) + await page.getByTestId('auto-pause').click() + await expect(page.getByTestId('auto-handoff-interactive')).toBeVisible({ timeout: 10_000 }) + await page.getByTestId('auto-handoff-interactive').click() + await expect(page.getByTestId('interactive-panel')).toBeVisible({ timeout: 15_000 }) + const acceptBtn = page.getByTestId('interactive-accept-scene') + const plotA = page.getByTestId('plot-option-A') + const startBtn = page.getByTestId('interactive-start') + await expect(acceptBtn.or(plotA).or(startBtn)).toBeVisible({ timeout: 120_000 }) + await app.close() + }) }) diff --git a/e2e/wizard-writing.spec.ts b/e2e/wizard-writing.spec.ts index 07de974..0a5b0bb 100644 --- a/e2e/wizard-writing.spec.ts +++ b/e2e/wizard-writing.spec.ts @@ -20,6 +20,42 @@ async function skipOnboarding(page: Page): Promise { } } +async function ensureChapterEditor(page: Page): Promise { + await page.getByRole('button', { name: '+ 新章' }).click() + await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 }) +} + +async function openBookWithContent(page: Page): Promise { + await page.getByRole('button', { name: /新建书籍/ }).first().click() + await page.locator('.dialog-content input').first().fill('向导测试书') + await page.getByRole('button', { name: '创建' }).click() + await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 }) + await ensureChapterEditor(page) + const editor = page.locator('.ProseMirror') + await editor.click() + await editor.pressSequentially('林远站在演武场中央,众人目光汇聚。') + await page.keyboard.press('Control+s') + await expect(page.getByText('已保存')).toBeVisible({ timeout: 15_000 }) +} + +async function openWizardPanel(page: Page): Promise { + await page.getByTestId('panel-tab-ai').click() + await page.getByTestId('ai-session-new').click() + await expect(page.getByTestId('ai-mode-tab-wizard')).toBeEnabled({ timeout: 10_000 }) + await page.getByTestId('ai-mode-tab-wizard').click() + await expect(page.getByTestId('wizard-panel')).toBeVisible({ timeout: 10_000 }) +} + +async function confirmWizardContext(page: Page): Promise { + await page.getByTestId('wizard-context-edit').click() + await expect(page.getByTestId('context-editor-modal')).toBeVisible({ timeout: 10_000 }) + await page.getByTestId('context-tab-chapter').click() + await page.locator('[data-testid^="context-item-"]').first().check() + await page.getByTestId('context-refresh').click() + await page.getByTestId('context-save').click() + await expect(page.getByTestId('wizard-preview')).toBeVisible({ timeout: 15_000 }) +} + test.describe('Wizard writing', () => { let userDataDir: string @@ -49,4 +85,42 @@ test.describe('Wizard writing', () => { await expect(page.getByTestId('ai-mode-tab-wizard')).toBeDisabled({ timeout: 10_000 }) await app.close() }) + + test('E2E-WIZ-01: wizard flow reaches interactive panel', async () => { + test.setTimeout(600_000) + const app = await launchApp(userDataDir) + const page = await app.firstWindow() + await skipOnboarding(page) + await openBookWithContent(page) + await openWizardPanel(page) + + await page.getByTestId('wizard-goal-input').fill('林远通过入门考核,展现天赋震惊众人') + await page.getByRole('button', { name: '下一步' }).click() + + await page.getByTestId('wizard-rhythm-mixed').click() + await page.getByRole('button', { name: '下一步' }).click() + + await page.getByRole('button', { name: '下一步' }).click() + + await confirmWizardContext(page) + + await page.getByRole('button', { name: '刷新预览' }).click() + await expect(page.getByTestId('wizard-preview')).not.toContainText('…', { timeout: 240_000 }) + + await page.getByTestId('wizard-start-generate').click() + await expect(page.getByTestId('ai-mode-tab-interactive')).toHaveClass(/active/, { timeout: 360_000 }) + + const acceptBtn = page.getByTestId('interactive-accept-scene') + const plotA = page.getByTestId('plot-option-A') + const naming0 = page.getByTestId('naming-option-0') + await expect(acceptBtn.or(plotA).or(naming0)).toBeVisible({ timeout: 120_000 }) + + if (await naming0.isVisible()) { + await naming0.click() + } + + await expect(page.getByTestId('interactive-panel')).toBeVisible({ timeout: 30_000 }) + await expect(acceptBtn.or(plotA)).toBeVisible({ timeout: 120_000 }) + await app.close() + }) }) diff --git a/src/renderer/components/ai/AutoPanel.tsx b/src/renderer/components/ai/AutoPanel.tsx index d8ac317..09f1bfe 100644 --- a/src/renderer/components/ai/AutoPanel.tsx +++ b/src/renderer/components/ai/AutoPanel.tsx @@ -176,7 +176,7 @@ export function AutoPanel({ bookId, sessionId, disabled }: AutoPanelProps): Reac type="button" className="btn" data-testid="auto-pause" - disabled={disabled || streaming} + disabled={disabled} onClick={() => void pause(bookId)} > {t('auto.pause')} diff --git a/src/renderer/components/layout/RightPanel.tsx b/src/renderer/components/layout/RightPanel.tsx index 7527626..9df92d9 100644 --- a/src/renderer/components/layout/RightPanel.tsx +++ b/src/renderer/components/layout/RightPanel.tsx @@ -2,6 +2,8 @@ import { useTranslation } from 'react-i18next' import { useAppStore } from '@renderer/stores/useAppStore' import { useBookStore } from '@renderer/stores/useBookStore' import { useInteractiveStore } from '@renderer/stores/useInteractiveStore' +import { useAutoStore } from '@renderer/stores/useAutoStore' +import { useWizardStore } from '@renderer/stores/useWizardStore' import { WordFreqPanel } from '@renderer/components/wordfreq/WordFreqPanel' import { AiPanel } from '@renderer/components/ai/AiPanel' import { KnowledgePanel } from '@renderer/components/knowledge/KnowledgePanel' @@ -29,7 +31,11 @@ export function RightPanel(): React.JSX.Element { onClick={() => { if (tab.id === 'ai') { const bookId = useBookStore.getState().currentBookId - if (bookId) void useInteractiveStore.getState().checkGate(bookId) + if (bookId) { + void useInteractiveStore.getState().checkGate(bookId) + void useAutoStore.getState().checkGate(bookId) + void useWizardStore.getState().checkGate(bookId) + } } setRightPanel(tab.id) }} diff --git a/src/renderer/stores/useAutoStore.ts b/src/renderer/stores/useAutoStore.ts index 373abcc..17e4cdc 100644 --- a/src/renderer/stores/useAutoStore.ts +++ b/src/renderer/stores/useAutoStore.ts @@ -5,6 +5,7 @@ import { useBookStore } from '@renderer/stores/useBookStore' import { useAppStore } from '@renderer/stores/useAppStore' import { useEditStore } from '@renderer/stores/useEditStore' import { useAiStore } from '@renderer/stores/useAiStore' +import { useInteractiveStore } from '@renderer/stores/useInteractiveStore' import i18n from '@renderer/i18n' interface AutoState { @@ -126,11 +127,17 @@ export const useAutoStore = create((set, get) => ({ handoffInteractive: async (bookId) => { const flowId = get().flow?.id - if (!flowId) return + const sessionId = useAiStore.getState().activeSessionId + if (!flowId || !sessionId) return const flow = await ipcCall(() => window.electronAPI.auto.handoffInteractive(bookId, flowId)) - set({ flow }) + set({ flow, streaming: false, streamBuffer: '' }) useAiStore.getState().setWritingMode('interactive') useAppStore.getState().showToast(i18n.t('wizard.handoffToast')) + await useInteractiveStore.getState().loadFlow(bookId, sessionId) + const interactiveFlow = useInteractiveStore.getState().flow + if (interactiveFlow?.step === 'plot_suggest') { + await useInteractiveStore.getState().suggestPlots(bookId) + } }, finishChapter: async (bookId, title) => { diff --git a/src/renderer/stores/useWizardStore.ts b/src/renderer/stores/useWizardStore.ts index b7853d5..60cf12e 100644 --- a/src/renderer/stores/useWizardStore.ts +++ b/src/renderer/stores/useWizardStore.ts @@ -8,6 +8,7 @@ import type { } from '@shared/types' import { ipcCall } from '@renderer/lib/ipc-client' import { useAiStore } from '@renderer/stores/useAiStore' +import { useInteractiveStore } from '@renderer/stores/useInteractiveStore' import { useAppStore } from '@renderer/stores/useAppStore' import i18n from '@renderer/i18n' @@ -108,13 +109,19 @@ export const useWizardStore = create((set, get) => ({ startGenerate: async (bookId) => { const flowId = get().flow?.id - if (!flowId) return + const sessionId = useAiStore.getState().activeSessionId + if (!flowId || !sessionId) return set({ streaming: true, streamBuffer: '' }) try { const flow = await ipcCall(() => window.electronAPI.wizard.startGenerate(bookId, flowId)) set({ flow }) useAiStore.getState().setWritingMode('interactive') useAppStore.getState().showToast(i18n.t('wizard.handoffToast')) + await useInteractiveStore.getState().loadFlow(bookId, sessionId) + const interactiveFlow = useInteractiveStore.getState().flow + if (interactiveFlow?.step === 'plot_suggest') { + await useInteractiveStore.getState().suggestPlots(bookId) + } } finally { set({ streaming: false, streamBuffer: '' }) } diff --git a/tests/main/auto-generate.test.ts b/tests/main/auto-generate.test.ts new file mode 100644 index 0000000..64bda46 --- /dev/null +++ b/tests/main/auto-generate.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from 'vitest' +import { DatabaseSync } from 'node:sqlite' +import { migrate } from '../../src/main/db/migrate' +import { AiSessionRepository } from '../../src/main/db/repositories/ai-session.repo' +import { InteractiveRepository } from '../../src/main/db/repositories/interactive.repo' +import { AutoWritingService } from '../../src/main/services/auto-writing.service' +import { AiClientService } from '../../src/main/services/ai-client.service' +import { DEFAULT_AI_CONFIG } from '../../src/shared/types' + +const AI_TEST_CONFIG = { ...DEFAULT_AI_CONFIG, timeoutMs: 240_000, maxTokens: 4096 } +const CONTEXT = '林远站在演武场中央,众人目光汇聚,等待入门考核开始。' + +describe('AutoWritingService generate', () => { + const service = (db: DatabaseSync) => + new AutoWritingService(db, new AiClientService(() => AI_TEST_CONFIG)) + + async function planFirstScene(db: DatabaseSync, svc: AutoWritingService, flowId: string): Promise { + await svc.planScenes(flowId, CONTEXT) + } + + it('IT-AUTO-02: generateNext writes non-empty scene', async () => { + const db = new DatabaseSync(':memory:') + migrate(db) + const session = new AiSessionRepository(db).create('t', DEFAULT_AI_CONFIG.model) + const svc = service(db) + const flow = svc.start(session.id) + svc.setGoal(flow.id, { + chapterGoal: '林远通过入门考核,展现天赋震惊众人', + wordMin: 800, + wordMax: 1500 + }) + await planFirstScene(db, svc, flow.id) + + let afterGen = await svc.generateNext(flow.id, CONTEXT, () => {}) + if (afterGen.step === 'naming_pause' && afterGen.namingPending) { + afterGen = await svc.resolveNaming( + flow.id, + afterGen.namingPending.options[0]!, + CONTEXT, + () => {} + ) + } + + const scenes = new InteractiveRepository(db).listScenes(flow.id) + expect(scenes.length).toBeGreaterThanOrEqual(1) + expect(scenes[0]!.contentHtml.replace(/<[^>]+>/g, '').trim().length).toBeGreaterThan(20) + db.close() + }, 240_000) + + it('IT-AUTO-03: pause and resume update step', async () => { + const db = new DatabaseSync(':memory:') + migrate(db) + const session = new AiSessionRepository(db).create('t', DEFAULT_AI_CONFIG.model) + const svc = service(db) + const flow = svc.start(session.id) + svc.setGoal(flow.id, { + chapterGoal: '林远通过入门考核', + wordMin: 800, + wordMax: 1500 + }) + await planFirstScene(db, svc, flow.id) + + const paused = svc.pause(flow.id) + expect(paused.step).toBe('paused') + + const resumed = svc.resume(flow.id) + expect(resumed.step).toBe('auto_generate') + db.close() + }, 240_000) +}) diff --git a/tests/main/auto-plan.test.ts b/tests/main/auto-plan.test.ts new file mode 100644 index 0000000..8698e69 --- /dev/null +++ b/tests/main/auto-plan.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from 'vitest' +import { DatabaseSync } from 'node:sqlite' +import { migrate } from '../../src/main/db/migrate' +import { AiSessionRepository } from '../../src/main/db/repositories/ai-session.repo' +import { AutoWritingService } from '../../src/main/services/auto-writing.service' +import { AiClientService } from '../../src/main/services/ai-client.service' +import { DEFAULT_AI_CONFIG } from '../../src/shared/types' + +const AI_TEST_CONFIG = { ...DEFAULT_AI_CONFIG, timeoutMs: 240_000, maxTokens: 4096 } +const CONTEXT = '林远站在演武场中央,众人目光汇聚,等待入门考核开始。' + +describe('AutoWritingService.planScenes', () => { + const service = (db: DatabaseSync) => + new AutoWritingService(db, new AiClientService(() => AI_TEST_CONFIG)) + + it('IT-AUTO-01: planScenes returns >=2 scenes', async () => { + const db = new DatabaseSync(':memory:') + migrate(db) + const session = new AiSessionRepository(db).create('t', DEFAULT_AI_CONFIG.model) + const svc = service(db) + const flow = svc.start(session.id) + svc.setGoal(flow.id, { + chapterGoal: '林远通过入门考核,展现天赋震惊众人', + wordMin: 800, + wordMax: 1500 + }) + const plan = await svc.planScenes(flow.id, CONTEXT) + expect(plan.scenes.length).toBeGreaterThanOrEqual(2) + db.close() + }, 360_000) +}) diff --git a/tests/main/wizard-generate.test.ts b/tests/main/wizard-generate.test.ts new file mode 100644 index 0000000..609ac1f --- /dev/null +++ b/tests/main/wizard-generate.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect } from 'vitest' +import { DatabaseSync } from 'node:sqlite' +import { migrate } from '../../src/main/db/migrate' +import { AiSessionRepository } from '../../src/main/db/repositories/ai-session.repo' +import { InteractiveRepository } from '../../src/main/db/repositories/interactive.repo' +import { WizardWritingService } from '../../src/main/services/wizard-writing.service' +import { AutoWritingService } from '../../src/main/services/auto-writing.service' +import { AiClientService } from '../../src/main/services/ai-client.service' +import { DEFAULT_AI_CONFIG, type AutoWritingConfig, type InteractiveFlow } from '../../src/shared/types' + +const AI_TEST_CONFIG = { ...DEFAULT_AI_CONFIG, timeoutMs: 300_000, maxTokens: 4096 } +const CONTEXT = '林远站在演武场中央,众人目光汇聚,等待入门考核开始。' + +describe('WizardWritingService.startGenerate', () => { + const wizard = (db: DatabaseSync) => + new WizardWritingService(db, new AiClientService(() => AI_TEST_CONFIG)) + const auto = (db: DatabaseSync) => + new AutoWritingService(db, new AiClientService(() => AI_TEST_CONFIG)) + + async function setupWizardFlow(svc: WizardWritingService, sessionId: string): Promise { + let flow = svc.start(sessionId) + flow = svc.setGoal(flow.id, { + chapterGoal: '林远通过入门考核,展现天赋', + wordMin: 600, + wordMax: 1200 + }) + flow = svc.setRhythm(flow.id, 'tense') + flow = svc.setPov(flow.id) + flow = svc.confirmContext( + flow.id, + { chapterIds: [], outlineIds: [], settingIds: [], inspirationIds: [] }, + CONTEXT + ) + return flow.id + } + + async function continueAfterPartialGenerate( + wizSvc: WizardWritingService, + autoSvc: AutoWritingService, + repo: InteractiveRepository, + flowId: string, + initial: InteractiveFlow + ): Promise { + let flow = initial + for (let guard = 0; guard < 8 && flow.flowMode !== 'interactive'; guard++) { + if (flow.step === 'naming_pause' && flow.namingPending) { + flow = await autoSvc.resolveNaming( + flowId, + flow.namingPending.options[0]!, + CONTEXT, + () => {} + ) + continue + } + const config = repo.getModeConfig(flowId) + const planLen = config.scenePlan?.length ?? 0 + const idx = config.currentPlanIndex ?? 0 + if (idx < planLen) { + flow = await autoSvc.generateNext(flowId, CONTEXT, () => {}) + continue + } + if (flow.scenes.length > 0) { + return wizSvc.handoffToInteractive(flowId) + } + break + } + return flow + } + + it('IT-WIZ-02: startGenerate handoffs to interactive with scenes', async () => { + const db = new DatabaseSync(':memory:') + migrate(db) + const repo = new InteractiveRepository(db) + const session = new AiSessionRepository(db).create('t', DEFAULT_AI_CONFIG.model) + const wizSvc = wizard(db) + const autoSvc = auto(db) + const flowId = await setupWizardFlow(wizSvc, session.id) + + const initial = await wizSvc.startGenerate(flowId, CONTEXT, () => {}) + const flow = + initial.flowMode === 'interactive' + ? initial + : await continueAfterPartialGenerate(wizSvc, autoSvc, repo, flowId, initial) + + expect(flow.flowMode).toBe('interactive') + expect(flow.scenes.length).toBeGreaterThanOrEqual(1) + db.close() + }, 600_000) +}) diff --git a/tests/main/wizard-preview.test.ts b/tests/main/wizard-preview.test.ts new file mode 100644 index 0000000..143d3b9 --- /dev/null +++ b/tests/main/wizard-preview.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest' +import { DatabaseSync } from 'node:sqlite' +import { migrate } from '../../src/main/db/migrate' +import { AiSessionRepository } from '../../src/main/db/repositories/ai-session.repo' +import { WizardWritingService } from '../../src/main/services/wizard-writing.service' +import { AiClientService } from '../../src/main/services/ai-client.service' +import { DEFAULT_AI_CONFIG } from '../../src/shared/types' + +const AI_TEST_CONFIG = { ...DEFAULT_AI_CONFIG, timeoutMs: 240_000, maxTokens: 4096 } +const CONTEXT = '林远站在演武场中央,众人目光汇聚,等待入门考核开始。' + +describe('WizardWritingService.buildPreview', () => { + const service = (db: DatabaseSync) => + new WizardWritingService(db, new AiClientService(() => AI_TEST_CONFIG)) + + it('IT-WIZ-01: buildPreview returns non-empty HTML', async () => { + const db = new DatabaseSync(':memory:') + migrate(db) + const session = new AiSessionRepository(db).create('t', DEFAULT_AI_CONFIG.model) + const svc = service(db) + let flow = svc.start(session.id) + flow = svc.setGoal(flow.id, { + chapterGoal: '林远通过入门考核,展现天赋震惊众人', + wordMin: 800, + wordMax: 1500 + }) + flow = svc.setRhythm(flow.id, 'mixed') + flow = svc.setPov(flow.id) + flow = svc.confirmContext( + flow.id, + { chapterIds: [], outlineIds: [], settingIds: [], inspirationIds: [] }, + CONTEXT + ) + + const preview = await svc.buildPreview(flow.id, CONTEXT, '林远') + expect(preview).toMatch(/

/) + expect(preview.replace(/<[^>]+>/g, '').trim().length).toBeGreaterThan(10) + db.close() + }, 240_000) +}) diff --git a/vitest.config.ts b/vitest.config.ts index 45dfd75..9586c28 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,7 +5,8 @@ export default defineConfig({ test: { environment: 'node', include: ['tests/**/*.test.ts'], - testTimeout: 240_000 + testTimeout: 240_000, + fileParallelism: false }, resolve: { alias: {