From 8e5045b882605963b7087c63f0a042f1edb428bc Mon Sep 17 00:00:00 2001 From: kun1h Date: Thu, 9 Jul 2026 14:03:19 +0800 Subject: [PATCH] fix(test): stabilize E2E cockpit flow and add LM Studio novel integration test Harden cockpit dismiss timing and global settings navigation in E2E helpers, tune short-chapter AI prompts for local models, fix electron-updater CJS loading, and add a 10-chapter auto-writing integration test against LM Studio. Co-authored-by: Cursor --- e2e/ai-chat.spec.ts | 19 ++- e2e/ai-context.spec.ts | 8 +- e2e/auto-writing.spec.ts | 24 +++- e2e/character-graph.spec.ts | 48 +++++-- e2e/cockpit-knowledge.spec.ts | 7 +- e2e/helpers.ts | 13 +- e2e/interactive-writing.spec.ts | 26 +++- e2e/knowledge-extraction.spec.ts | 13 +- e2e/knowledge-injection-history.spec.ts | 19 ++- e2e/outline-coverage.spec.ts | 12 ++ e2e/outline.spec.ts | 12 ++ e2e/p2-features.spec.ts | 13 ++ e2e/p21-features.spec.ts | 14 ++ e2e/shortcuts.spec.ts | 12 ++ e2e/wizard-writing.spec.ts | 17 +++ e2e/writing-analytics.spec.ts | 14 +- e2e/writing-goals.spec.ts | 19 ++- e2e/writing-logs.spec.ts | 17 ++- e2e/writing.spec.ts | 13 ++ .../services/auto-prompt-builder.service.ts | 11 +- src/main/services/update.service.ts | 21 ++- src/renderer/components/layout/TopBar.tsx | 11 +- src/renderer/lib/graph-cytoscape.ts | 2 + .../main/novel-10-chapter.integration.test.ts | 130 ++++++++++++++++++ 24 files changed, 425 insertions(+), 70 deletions(-) create mode 100644 tests/main/novel-10-chapter.integration.test.ts diff --git a/e2e/ai-chat.spec.ts b/e2e/ai-chat.spec.ts index 845649f..dfc7fc6 100644 --- a/e2e/ai-chat.spec.ts +++ b/e2e/ai-chat.spec.ts @@ -25,15 +25,22 @@ async function createAndOpenBook(page: Page, name = 'AI测试书'): Promise { + const cockpit = page.locator('[data-testid="cockpit-modal"]') + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() + await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ + } } async function openGlobalSettings(page: Page): Promise { - const topSettings = page.locator('#topbar button.top-btn').filter({ hasText: '⚙' }) - if (await topSettings.isVisible()) { - await topSettings.click() - } else { - await page.getByRole('button', { name: /系统设置/ }).click() - } + await page.getByTestId('topbar-settings').click() await expect(page.locator('#settings-page')).toBeVisible({ timeout: 10_000 }) } diff --git a/e2e/ai-context.spec.ts b/e2e/ai-context.spec.ts index c12f1fa..ab2270e 100644 --- a/e2e/ai-context.spec.ts +++ b/e2e/ai-context.spec.ts @@ -28,9 +28,12 @@ async function skipOnboarding(page: Page): Promise { async function dismissCockpitIfOpen(page: Page): Promise { const cockpit = page.locator('[data-testid="cockpit-modal"]') - if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) { - await page.locator('[data-testid="cockpit-modal"] .modal-close').click() + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ } } @@ -43,6 +46,7 @@ async function createAndOpenBook(page: Page, name: string): Promise { } async function ensureChapterEditor(page: Page): Promise { + await dismissCockpitIfOpen(page) const items = page.locator('.chapter-item') if ((await items.count()) === 0) { await page.getByRole('button', { name: '+ 新章' }).click() diff --git a/e2e/auto-writing.spec.ts b/e2e/auto-writing.spec.ts index 3b9b241..1126ed4 100644 --- a/e2e/auto-writing.spec.ts +++ b/e2e/auto-writing.spec.ts @@ -20,7 +20,19 @@ async function skipOnboarding(page: Page): Promise { } } +async function dismissCockpitIfOpen(page: Page): Promise { + const cockpit = page.locator('[data-testid="cockpit-modal"]') + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() + await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ + } +} + async function ensureChapterEditor(page: Page): Promise { + await dismissCockpitIfOpen(page) await page.getByRole('button', { name: '+ 新章' }).click() await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 }) } @@ -30,6 +42,7 @@ async function openBookWithContent(page: Page): Promise { 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 dismissCockpitIfOpen(page) await ensureChapterEditor(page) const editor = page.locator('.ProseMirror') await editor.click() @@ -48,8 +61,11 @@ async function openAutoPanel(page: Page): Promise { async function planAutoScenes(page: Page): Promise { await page.getByTestId('auto-goal-input').fill('林远通过入门考核,展现天赋震惊众人') + const wordInputs = page.locator('.auto-word-range input') + await wordInputs.nth(0).fill('180') + await wordInputs.nth(1).fill('220') await page.getByTestId('auto-plan-scenes').click() - await expect(page.getByTestId('auto-start-generate')).toBeVisible({ timeout: 240_000 }) + await expect(page.getByTestId('auto-start-generate')).toBeVisible({ timeout: 360_000 }) } async function generateOneAutoScene(page: Page): Promise { @@ -68,6 +84,7 @@ async function generateOneAutoScene(page: Page): Promise { } test.describe('Auto writing', () => { + test.describe.configure({ retries: 1 }) let userDataDir: string test.beforeEach(() => { @@ -92,13 +109,14 @@ test.describe('Auto writing', () => { 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 dismissCockpitIfOpen(page) await page.getByTestId('panel-tab-ai').click() 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) + test.setTimeout(600_000) const app = await launchApp(userDataDir) const page = await app.firstWindow() await skipOnboarding(page) @@ -113,7 +131,7 @@ test.describe('Auto writing', () => { }) test('E2E-AUTO-02: pause and handoff to interactive', async () => { - test.setTimeout(300_000) + test.setTimeout(600_000) const app = await launchApp(userDataDir) const page = await app.firstWindow() await skipOnboarding(page) diff --git a/e2e/character-graph.spec.ts b/e2e/character-graph.spec.ts index d95cc5a..c7d0c3c 100644 --- a/e2e/character-graph.spec.ts +++ b/e2e/character-graph.spec.ts @@ -27,9 +27,12 @@ async function skipOnboarding(page: Page): Promise { async function dismissCockpitIfOpen(page: Page): Promise { const cockpit = page.locator('[data-testid="cockpit-modal"]') - if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) { - await page.locator('[data-testid="cockpit-modal"] .modal-close').click() + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ } } @@ -47,7 +50,9 @@ async function createCharacterSetting(page: Page, name: string): Promise { await page.getByTestId('setting-name-input').fill(name) await page.locator('.dialog-content select').selectOption('character') await page.getByRole('button', { name: '创建' }).click() - await expect(page.getByTestId(/setting-item-/)).toContainText(name, { timeout: 10_000 }) + await expect(page.getByTestId(/setting-item-/).filter({ hasText: name })).toBeVisible({ + timeout: 10_000 + }) } test.describe('Character graph P7', () => { @@ -75,11 +80,15 @@ test.describe('Character graph P7', () => { await createCharacterSetting(page, '角色甲') await createCharacterSetting(page, '角色乙') - await page.getByText('角色甲').click() + await page.getByTestId(/setting-item-/).filter({ hasText: '角色甲' }).click() await expect(page.getByTestId('setting-relationships')).toBeVisible({ timeout: 10_000 }) await page.getByTestId('setting-relationship-add').click() - await page.locator('.setting-relationship-form select').selectOption({ label: '角色乙' }) - await page.locator('.setting-relationship-form input[type="text"]').fill('挚友') + await expect(page.locator('.setting-relationship-form select')).toBeVisible({ timeout: 10_000 }) + await page.locator('.setting-relationship-form select').selectOption({ index: 1 }) + await expect(page.locator('.setting-relationship-form input.form-control')).toBeVisible({ + timeout: 10_000 + }) + await page.locator('.setting-relationship-form input.form-control').fill('挚友') await page.locator('.setting-relationship-form .btn-primary').click() await expect(page.getByTestId('setting-relationships')).toContainText('挚友') @@ -98,21 +107,30 @@ test.describe('Character graph P7', () => { await createCharacterSetting(page, '节点A') await createCharacterSetting(page, '节点B') - await page.getByText('节点A').click() + await page.getByTestId(/setting-item-/).filter({ hasText: '节点A' }).click() await page.getByTestId('setting-relationship-add').click() - await page.locator('.setting-relationship-form select').selectOption({ label: '节点B' }) - await page.locator('.setting-relationship-form input[type="text"]').fill('同门') + await expect(page.locator('.setting-relationship-form select')).toBeVisible({ timeout: 10_000 }) + await page.locator('.setting-relationship-form select').selectOption({ index: 1 }) + await expect(page.locator('.setting-relationship-form input.form-control')).toBeVisible({ + timeout: 10_000 + }) + await page.locator('.setting-relationship-form input.form-control').fill('同门') await page.locator('.setting-relationship-form .btn-primary').click() await page.getByTestId('setting-open-graph').click() await expect(page.getByTestId('character-graph-cy')).toBeVisible({ timeout: 10_000 }) - const cyBox = await page.getByTestId('character-graph-cy').boundingBox() - expect(cyBox).toBeTruthy() - await page.getByTestId('character-graph-cy').click({ - position: { x: (cyBox!.width ?? 200) / 2, y: (cyBox!.height ?? 200) / 2 } + await page.evaluate(() => { + const el = document.querySelector('[data-testid="character-graph-cy"]') as HTMLElement & { + __bilinCy?: { nodes: () => { trigger: (e: string) => void }[] } + } + const cy = el?.__bilinCy + if (!cy) throw new Error('cytoscape not ready') + const nodes = cy.nodes() + if (!nodes.length) throw new Error('no graph nodes') + nodes[0]!.trigger('tap') }) - await expect(page.getByTestId('character-graph-sidebar')).toContainText(/节点/, { - timeout: 5000 + await expect(page.getByTestId('character-graph-sidebar')).toContainText(/节点A|节点B/, { + timeout: 10_000 }) await app.close() }) diff --git a/e2e/cockpit-knowledge.spec.ts b/e2e/cockpit-knowledge.spec.ts index fb73c5c..6a43f38 100644 --- a/e2e/cockpit-knowledge.spec.ts +++ b/e2e/cockpit-knowledge.spec.ts @@ -27,9 +27,12 @@ async function skipOnboarding(page: Page): Promise { async function dismissCockpitIfOpen(page: Page): Promise { const cockpit = page.locator('[data-testid="cockpit-modal"]') - if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) { - await page.locator('[data-testid="cockpit-modal"] .modal-close').click() + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ } } diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 5f1419c..4ad871d 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -23,12 +23,20 @@ export async function skipOnboarding(page: Page): Promise { export async function dismissCockpitIfOpen(page: Page): Promise { const cockpit = page.locator('[data-testid="cockpit-modal"]') - if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) { - await page.locator('[data-testid="cockpit-modal"] .modal-close').click() + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ } } +export async function openGlobalSettings(page: Page): Promise { + await page.getByTestId('topbar-settings').click() + await expect(page.locator('#settings-page')).toBeVisible({ timeout: 10_000 }) +} + export async function createBookAndOpenEditor(page: Page, name: string): Promise { await page.getByRole('button', { name: /新建书籍/ }).first().click() await page.locator('.dialog-content input').first().fill(name) @@ -38,6 +46,7 @@ export async function createBookAndOpenEditor(page: Page, name: string): Promise } export async function ensureChapterEditor(page: Page): Promise { + await dismissCockpitIfOpen(page) const editor = page.locator('.ProseMirror') if (await editor.isVisible().catch(() => false)) return await page.getByRole('button', { name: '+ 新章' }).click() diff --git a/e2e/interactive-writing.spec.ts b/e2e/interactive-writing.spec.ts index b7995a1..9889c5d 100644 --- a/e2e/interactive-writing.spec.ts +++ b/e2e/interactive-writing.spec.ts @@ -20,7 +20,19 @@ async function skipOnboarding(page: Page): Promise { } } +async function dismissCockpitIfOpen(page: Page): Promise { + const cockpit = page.locator('[data-testid="cockpit-modal"]') + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() + await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ + } +} + async function ensureChapterEditor(page: Page): Promise { + await dismissCockpitIfOpen(page) await page.getByRole('button', { name: '+ 新章' }).click() await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 }) } @@ -30,6 +42,7 @@ async function openBookWithContent(page: Page): Promise { 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 dismissCockpitIfOpen(page) await ensureChapterEditor(page) const editor = page.locator('.ProseMirror') await editor.click() @@ -39,6 +52,7 @@ async function openBookWithContent(page: Page): Promise { } test.describe('Interactive writing', () => { + test.describe.configure({ retries: 1 }) let userDataDir: string test.beforeEach(() => { @@ -63,13 +77,14 @@ test.describe('Interactive writing', () => { 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 dismissCockpitIfOpen(page) await page.getByTestId('panel-tab-ai').click() await expect(page.getByTestId('ai-mode-tab-interactive')).toBeDisabled({ timeout: 10_000 }) await app.close() }) test('E2E-INT-01: interactive flow creates chapter', async () => { - test.setTimeout(300_000) + test.setTimeout(600_000) const app = await launchApp(userDataDir) const page = await app.firstWindow() await skipOnboarding(page) @@ -85,21 +100,21 @@ test.describe('Interactive writing', () => { await page.getByTestId('interactive-confirm-plot').click() const naming0 = page.getByTestId('naming-option-0') const acceptBtn = page.getByTestId('interactive-accept-scene') - await expect(naming0.or(acceptBtn)).toBeVisible({ timeout: 120_000 }) + await expect(naming0.or(acceptBtn)).toBeVisible({ timeout: 240_000 }) if (await naming0.isVisible()) { await naming0.click() - await expect(acceptBtn).toBeVisible({ timeout: 120_000 }) + await expect(acceptBtn).toBeVisible({ timeout: 240_000 }) } await acceptBtn.click() const finishBtn = page.getByTestId('interactive-finish-chapter') - await expect(finishBtn).toBeVisible({ timeout: 120_000 }) + await expect(finishBtn).toBeEnabled({ timeout: 240_000 }) await finishBtn.click() await expect(page.locator('.ProseMirror')).not.toBeEmpty({ timeout: 30_000 }) await app.close() }) test('E2E-INT-03: restores flow after restart', async () => { - test.setTimeout(300_000) + test.setTimeout(600_000) const app1 = await launchApp(userDataDir) const page1 = await app1.firstWindow() await skipOnboarding(page1) @@ -129,6 +144,7 @@ test.describe('Interactive writing', () => { await skipOnboarding(page2) await page2.getByText('交互测试书').click() await expect(page2.locator('#editor-layout')).toBeVisible({ timeout: 15_000 }) + await dismissCockpitIfOpen(page2) await page2.getByTestId('panel-tab-ai').click() await expect(page2.getByTestId('ai-session-select')).not.toHaveValue('', { timeout: 10_000 }) await page2.getByTestId('ai-session-select').selectOption(sessionValue) diff --git a/e2e/knowledge-extraction.spec.ts b/e2e/knowledge-extraction.spec.ts index 0cff302..cce68c3 100644 --- a/e2e/knowledge-extraction.spec.ts +++ b/e2e/knowledge-extraction.spec.ts @@ -27,9 +27,12 @@ async function skipOnboarding(page: Page): Promise { async function dismissCockpitIfOpen(page: Page): Promise { const cockpit = page.locator('[data-testid="cockpit-modal"]') - if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) { - await page.locator('[data-testid="cockpit-modal"] .modal-close').click() + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ } } @@ -73,6 +76,7 @@ test.describe('Knowledge extraction P5.2', () => { await page.locator('[data-testid="panel-tab-knowledge"]').click() await expect(page.locator('[data-testid="knowledge-panel"]')).toBeVisible() + await page.locator('[data-testid="knowledge-tab-pending"]').click() await expect(page.locator('[data-testid="knowledge-extract-current"]')).toBeVisible() await expect(page.locator('[data-testid="knowledge-batch-high-confidence"]')).toBeVisible() await app.close() @@ -82,9 +86,10 @@ test.describe('Knowledge extraction P5.2', () => { const app = await launchApp(userDataDir) const page = await app.firstWindow() await skipOnboarding(page) - await createAndOpenBook(page, '设置测试') + await createAndOpenBook(page, '全局设置测试') - await page.getByRole('button', { name: '设置' }).click() + await page.getByTestId('topbar-settings').click() + await expect(page.locator('#settings-page')).toBeVisible({ timeout: 10_000 }) await expect(page.locator('[data-testid="settings-knowledge-auto-extract"]')).toBeVisible() await expect(page.locator('[data-testid="settings-extract-threshold"]')).toBeVisible() await app.close() diff --git a/e2e/knowledge-injection-history.spec.ts b/e2e/knowledge-injection-history.spec.ts index 4117a2d..352d045 100644 --- a/e2e/knowledge-injection-history.spec.ts +++ b/e2e/knowledge-injection-history.spec.ts @@ -20,15 +20,23 @@ async function skipOnboarding(page: Page): Promise { } } +async function dismissCockpitIfOpen(page: Page): Promise { + const cockpit = page.locator('[data-testid="cockpit-modal"]') + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() + await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ + } +} + 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 }) - const cockpit = page.locator('[data-testid="cockpit-modal"]') - if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) { - await page.locator('[data-testid="cockpit-modal"] .modal-close').click() - } + await dismissCockpitIfOpen(page) await page.getByRole('button', { name: '+ 新章' }).click() const editor = page.locator('.ProseMirror') await expect(editor).toBeVisible({ timeout: 10_000 }) @@ -96,7 +104,8 @@ test.describe('Knowledge injection history P6.1', () => { await page.getByTestId('context-tab-knowledge').click() await page.locator(`[data-testid="context-knowledge-item-${entryId}"] input`).check() await page.getByTestId('context-save').click() - await page.getByRole('button', { name: '下一步' }).click() + await expect(page.getByTestId('context-editor-modal')).toBeHidden({ timeout: 10_000 }) + await page.locator('[data-testid="wizard-panel"] button.btn.primary').click() await page.locator('[data-testid="panel-tab-knowledge"]').click() await page.locator('[data-testid="knowledge-tab-all"]').click() diff --git a/e2e/outline-coverage.spec.ts b/e2e/outline-coverage.spec.ts index c9e27f6..5ac4a72 100644 --- a/e2e/outline-coverage.spec.ts +++ b/e2e/outline-coverage.spec.ts @@ -20,11 +20,23 @@ async function skipOnboarding(page: Page): Promise { } } +async function dismissCockpitIfOpen(page: Page): Promise { + const cockpit = page.locator('[data-testid="cockpit-modal"]') + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() + await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ + } +} + async function createAndOpenBook(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 dismissCockpitIfOpen(page) } test.describe('Outline coverage', () => { diff --git a/e2e/outline.spec.ts b/e2e/outline.spec.ts index dce9192..b064db8 100644 --- a/e2e/outline.spec.ts +++ b/e2e/outline.spec.ts @@ -26,11 +26,23 @@ async function skipOnboarding(page: Page): Promise { } } +async function dismissCockpitIfOpen(page: Page): Promise { + const cockpit = page.locator('[data-testid="cockpit-modal"]') + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() + await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ + } +} + async function createAndOpenBook(page: Page, name: string): Promise { await page.getByRole('button', { name: /新建书籍/ }).first().click() await page.locator('.dialog-content input').first().fill(name) await page.getByRole('button', { name: '创建' }).click() await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 }) + await dismissCockpitIfOpen(page) } test.describe('Outline persistence', () => { diff --git a/e2e/p2-features.spec.ts b/e2e/p2-features.spec.ts index 7aa01d7..18b3d76 100644 --- a/e2e/p2-features.spec.ts +++ b/e2e/p2-features.spec.ts @@ -20,14 +20,27 @@ async function skipOnboarding(page: Page): Promise { } } +async function dismissCockpitIfOpen(page: Page): Promise { + const cockpit = page.locator('[data-testid="cockpit-modal"]') + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() + await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ + } +} + async function createAndOpenBook(page: Page, name = 'P2测试书'): Promise { await page.getByRole('button', { name: /新建书籍/ }).first().click() await page.locator('.dialog-content input').first().fill(name) await page.getByRole('button', { name: '创建' }).click() await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 }) + await dismissCockpitIfOpen(page) } async function ensureChapterEditor(page: Page): Promise { + await dismissCockpitIfOpen(page) await page.getByRole('button', { name: '+ 新章' }).click() await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 }) } diff --git a/e2e/p21-features.spec.ts b/e2e/p21-features.spec.ts index 29ecb88..1e4d532 100644 --- a/e2e/p21-features.spec.ts +++ b/e2e/p21-features.spec.ts @@ -20,14 +20,27 @@ async function skipOnboarding(page: Page): Promise { } } +async function dismissCockpitIfOpen(page: Page): Promise { + const cockpit = page.locator('[data-testid="cockpit-modal"]') + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() + await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ + } +} + async function createAndOpenBook(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 dismissCockpitIfOpen(page) } async function ensureChapterEditor(page: Page): Promise { + await dismissCockpitIfOpen(page) await page.getByRole('button', { name: '+ 新章' }).click() await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 }) } @@ -150,6 +163,7 @@ test.describe('P2.1 features', () => { await page.getByTestId('inspiration-save-btn').click() await expect(page.getByTestId('inspiration-modal')).toBeHidden({ timeout: 10_000 }) + await page.getByTestId('sidebar-tab-inspiration').click() await expect(page.getByTestId('inspiration-list')).toBeVisible() await expect(page.getByTestId('inspiration-list').getByText(idea)).toBeVisible({ timeout: 10_000 }) await app.close() diff --git a/e2e/shortcuts.spec.ts b/e2e/shortcuts.spec.ts index c6ede4d..2ec6e72 100644 --- a/e2e/shortcuts.spec.ts +++ b/e2e/shortcuts.spec.ts @@ -26,11 +26,23 @@ async function skipOnboarding(page: Page): Promise { } } +async function dismissCockpitIfOpen(page: Page): Promise { + const cockpit = page.locator('[data-testid="cockpit-modal"]') + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() + await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ + } +} + async function createBookAndOpenEditor(page: Page, name: string): Promise { await page.getByRole('button', { name: /新建书籍/ }).first().click() await page.locator('.dialog-content input').first().fill(name) await page.getByRole('button', { name: '创建' }).click() await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 }) + await dismissCockpitIfOpen(page) await page.getByRole('button', { name: '+ 新章' }).click() await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 }) } diff --git a/e2e/wizard-writing.spec.ts b/e2e/wizard-writing.spec.ts index 0a5b0bb..3174519 100644 --- a/e2e/wizard-writing.spec.ts +++ b/e2e/wizard-writing.spec.ts @@ -20,7 +20,19 @@ async function skipOnboarding(page: Page): Promise { } } +async function dismissCockpitIfOpen(page: Page): Promise { + const cockpit = page.locator('[data-testid="cockpit-modal"]') + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() + await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ + } +} + async function ensureChapterEditor(page: Page): Promise { + await dismissCockpitIfOpen(page) await page.getByRole('button', { name: '+ 新章' }).click() await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 }) } @@ -30,6 +42,7 @@ async function openBookWithContent(page: Page): Promise { 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 dismissCockpitIfOpen(page) await ensureChapterEditor(page) const editor = page.locator('.ProseMirror') await editor.click() @@ -81,6 +94,7 @@ test.describe('Wizard writing', () => { 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 dismissCockpitIfOpen(page) await page.getByTestId('panel-tab-ai').click() await expect(page.getByTestId('ai-mode-tab-wizard')).toBeDisabled({ timeout: 10_000 }) await app.close() @@ -95,6 +109,9 @@ test.describe('Wizard writing', () => { await openWizardPanel(page) await page.getByTestId('wizard-goal-input').fill('林远通过入门考核,展现天赋震惊众人') + const wordInputs = page.locator('.auto-word-range input') + await wordInputs.nth(0).fill('180') + await wordInputs.nth(1).fill('220') await page.getByRole('button', { name: '下一步' }).click() await page.getByTestId('wizard-rhythm-mixed').click() diff --git a/e2e/writing-analytics.spec.ts b/e2e/writing-analytics.spec.ts index d7aed91..457eac6 100644 --- a/e2e/writing-analytics.spec.ts +++ b/e2e/writing-analytics.spec.ts @@ -28,9 +28,12 @@ async function skipOnboarding(page: Page): Promise { async function dismissCockpitIfOpen(page: Page): Promise { const cockpit = page.locator('[data-testid="cockpit-modal"]') - if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) { - await page.locator('[data-testid="cockpit-modal"] .modal-close').click() + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ } } @@ -43,6 +46,7 @@ async function createAndOpenBook(page: Page, name: string): Promise { } async function ensureChapterEditor(page: Page): Promise { + await dismissCockpitIfOpen(page) const items = page.locator('.chapter-item') if ((await items.count()) === 0) { await page.getByRole('button', { name: '+ 新章' }).click() @@ -104,9 +108,11 @@ test.describe('Writing analytics P9', () => { await page.getByTestId('setting-name-input').fill('主角视角') await page.locator('.dialog-content select').selectOption('character') await page.getByRole('button', { name: '创建' }).click() - await expect(page.getByText('主角视角')).toBeVisible({ timeout: 10_000 }) + await expect(page.getByTestId(/setting-item-/).filter({ hasText: '主角视角' })).toBeVisible({ + timeout: 10_000 + }) - await page.getByTestId('sidebar-tab-chapter').click() + await page.getByTestId('sidebar-tab-chapters').click() await ensureChapterEditor(page) await page.getByTestId('chapter-pov-select').selectOption({ label: '主角视角' }) diff --git a/e2e/writing-goals.spec.ts b/e2e/writing-goals.spec.ts index 8827f05..aba26d2 100644 --- a/e2e/writing-goals.spec.ts +++ b/e2e/writing-goals.spec.ts @@ -30,9 +30,12 @@ async function skipOnboarding(page: Page): Promise { async function dismissCockpitIfOpen(page: Page): Promise { const cockpit = page.locator('[data-testid="cockpit-modal"]') - if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) { - await page.locator('[data-testid="cockpit-modal"] .modal-close').click() + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ } } @@ -45,6 +48,7 @@ async function createAndOpenBook(page: Page, name: string): Promise { } async function ensureChapterEditor(page: Page): Promise { + await dismissCockpitIfOpen(page) const items = page.locator('.chapter-item') if ((await items.count()) === 0) { await page.getByRole('button', { name: '+ 新章' }).click() @@ -88,17 +92,18 @@ test.describe('Writing goals P5.3', () => { await createAndOpenBook(page, '目标测试书') await ensureChapterEditor(page) - await page.getByRole('button', { name: '设置' }).click() - await page.locator('[data-testid="settings-daily-word-goal"]').fill('100') - await page.getByRole('button', { name: '书籍' }).click() + await page.getByTestId('topbar-settings').click() + await expect(page.locator('#settings-page')).toBeVisible({ timeout: 10_000 }) + await page.locator('[data-testid="settings-daily-word-goal"]').fill('30') + await page.getByRole('button', { name: '目标测试书' }).click() await ensureChapterEditor(page) const editor = page.locator('.ProseMirror') await editor.click() await editor.pressSequentially( - '这是一段用于测试每日写作目标达标的示例文字,需要足够长度才能触发字数统计与达标提醒通知。' + '这是一段用于测试每日写作目标达标的示例文字,需要足够长度才能触发字数统计与达标提醒通知。继续书写更多内容以确保超过三十字的目标阈值。' ) - await page.waitForTimeout(1500) + await page.waitForTimeout(2500) await expect(page.locator('[data-testid="status-daily-goal"].goal-met')).toBeVisible({ timeout: 15_000 diff --git a/e2e/writing-logs.spec.ts b/e2e/writing-logs.spec.ts index cf9cd4a..5f4c0c4 100644 --- a/e2e/writing-logs.spec.ts +++ b/e2e/writing-logs.spec.ts @@ -27,9 +27,12 @@ async function skipOnboarding(page: Page): Promise { async function dismissCockpitIfOpen(page: Page): Promise { const cockpit = page.locator('[data-testid="cockpit-modal"]') - if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) { - await page.locator('[data-testid="cockpit-modal"] .modal-close').click() + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ } } @@ -64,9 +67,10 @@ test.describe('Writing logs P5.2', () => { await skipOnboarding(page) await createAndOpenBook(page, '日志测试书') - await page.getByRole('button', { name: '设置' }).click() + await page.getByTestId('topbar-settings').click() + await expect(page.locator('#settings-page')).toBeVisible({ timeout: 10_000 }) await page.locator('[data-testid="settings-daily-word-goal"]').fill('100') - await page.getByRole('button', { name: '书籍' }).click() + await page.getByRole('button', { name: '日志测试书' }).click() const items = page.locator('.chapter-item') if ((await items.count()) === 0) { @@ -92,9 +96,10 @@ test.describe('Writing logs P5.2', () => { await skipOnboarding(page) await createAndOpenBook(page, '状态栏测试') - await page.getByRole('button', { name: '设置' }).click() + await page.getByTestId('topbar-settings').click() + await expect(page.locator('#settings-page')).toBeVisible({ timeout: 10_000 }) await page.locator('[data-testid="settings-daily-word-goal"]').fill('500') - await page.getByRole('button', { name: '书籍' }).click() + await page.getByRole('button', { name: '状态栏测试' }).click() const items = page.locator('.chapter-item') if ((await items.count()) === 0) { diff --git a/e2e/writing.spec.ts b/e2e/writing.spec.ts index ea1eb6a..319662c 100644 --- a/e2e/writing.spec.ts +++ b/e2e/writing.spec.ts @@ -25,11 +25,23 @@ async function skipOnboarding(page: Page): Promise { await expect(page.getByText('欢迎使用笔临')).toBeHidden({ timeout: 10_000 }) } +async function dismissCockpitIfOpen(page: Page): Promise { + const cockpit = page.locator('[data-testid="cockpit-modal"]') + try { + await cockpit.waitFor({ state: 'visible', timeout: 15_000 }) + await cockpit.locator('.modal-close').click() + await expect(cockpit).toBeHidden({ timeout: 5000 }) + } catch { + /* cockpit did not open */ + } +} + async function createAndOpenBook(page: Page, name: string): Promise { await page.getByRole('button', { name: /新建书籍/ }).first().click() await page.locator('.dialog-content input').first().fill(name) await page.getByRole('button', { name: '创建' }).click() await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 }) + await dismissCockpitIfOpen(page) } test.describe('Writing', () => { @@ -54,6 +66,7 @@ test.describe('Writing', () => { const page1 = await app1.firstWindow() await skipOnboarding(page1) await createAndOpenBook(page1, '测试书') + await dismissCockpitIfOpen(page1) await page1.getByRole('button', { name: '+ 新章' }).click() const editor = page1.locator('.ProseMirror') diff --git a/src/main/services/auto-prompt-builder.service.ts b/src/main/services/auto-prompt-builder.service.ts index a2bd4bc..7807730 100644 --- a/src/main/services/auto-prompt-builder.service.ts +++ b/src/main/services/auto-prompt-builder.service.ts @@ -12,11 +12,13 @@ export function buildScenePlanMessages( outlineSummaries.length > 0 ? `关联大纲:\n${outlineSummaries.map((s, i) => `${i + 1}. ${s}`).join('\n')}\n\n` : '' + const sceneRange = wordMax <= 300 ? '1-2' : '3-6' + const summaryHint = wordMax <= 300 ? '至少40字' : '至少80字' return [ { role: 'system', content: - '你是长篇小说章节规划助手。根据本章目标规划 3-6 个连续场景。仅返回 JSON:{"scenes":[{"label":"场景名","summary":"至少80字的场景要点"},...],"estimatedWords":数字}。estimatedWords 须在用户给定字数范围内。' + `你是长篇小说章节规划助手。根据本章目标规划 ${sceneRange} 个连续场景。仅返回 JSON:{"scenes":[{"label":"场景名","summary":"${summaryHint}的场景要点"},...],"estimatedWords":数字}。estimatedWords 须在用户给定字数范围内。` }, { role: 'user', @@ -35,15 +37,18 @@ export function buildAutoSceneMessages( priorSceneSummaries.length > 0 ? `已写场景摘要:\n${priorSceneSummaries.map((s, i) => `${i + 1}. ${s}`).join('\n')}\n\n` : '' + const planLen = Math.max(1, config.scenePlan?.length ?? 1) + const sceneMin = Math.max(60, Math.floor((config.wordMin ?? 2000) / planLen)) + const sceneMax = Math.max(sceneMin + 20, Math.ceil((config.wordMax ?? 4000) / planLen)) return [ { role: 'system', content: - '你是小说场景写作助手。若需为新元素命名且尚无定论,先仅返回 JSON:{"type":"naming_required","elementType":"类型","context":"说明","options":["名1","名2","名3","名4","名5"]}。否则输出 600-1500 字场景 HTML

段落。' + `你是小说场景写作助手。若需为新元素命名且尚无定论,先仅返回 JSON:{"type":"naming_required","elementType":"类型","context":"说明","options":["名1","名2","名3","名4","名5"]}。否则输出 ${sceneMin}-${sceneMax} 字场景 HTML

段落,勿超出上限。` }, { role: 'user', - content: `${prior}上下文:\n${contextText}\n\n本章目标:${config.chapterGoal}\n\n当前场景:${planItem.label}\n场景要点:${planItem.summary}\n\n请生成本场景。` + content: `${prior}上下文:\n${contextText}\n\n本章目标:${config.chapterGoal}\n\n当前场景:${planItem.label}\n场景要点:${planItem.summary}\n\n请生成本场景,控制在 ${sceneMin}-${sceneMax} 字。` } ] } diff --git a/src/main/services/update.service.ts b/src/main/services/update.service.ts index 27b0e70..84bf9d7 100644 --- a/src/main/services/update.service.ts +++ b/src/main/services/update.service.ts @@ -1,8 +1,18 @@ +import { createRequire } from 'node:module' import { app, BrowserWindow } from 'electron' -import electronUpdater from 'electron-updater' +import type { AppUpdater } from 'electron-updater' import { IPC } from '../../shared/ipc-channels' -const { autoUpdater } = electronUpdater +const require = createRequire(import.meta.url) +let autoUpdaterRef: AppUpdater | null = null + +function getAutoUpdater(): AppUpdater { + if (!autoUpdaterRef) { + const electronUpdater = require('electron-updater') as { autoUpdater: AppUpdater } + autoUpdaterRef = electronUpdater.autoUpdater + } + return autoUpdaterRef +} export interface UpdateInfoPayload { version: string @@ -31,6 +41,7 @@ export class UpdateService { !app.isPackaged || process.env.NODE_ENV === 'development' + const autoUpdater = getAutoUpdater() autoUpdater.autoDownload = false autoUpdater.autoInstallOnAppQuit = true @@ -62,7 +73,7 @@ export class UpdateService { this.broadcast(IPC.UPDATE_AVAILABLE, payload) return payload } - const result = await autoUpdater.checkForUpdates() + const result = await getAutoUpdater().checkForUpdates() const info = result?.updateInfo if (!info) return null return { @@ -78,12 +89,12 @@ export class UpdateService { this.broadcast(IPC.UPDATE_DOWNLOADED, { version: this.mockVersion }) return } - await autoUpdater.downloadUpdate() + await getAutoUpdater().downloadUpdate() } install(): void { if (this.disabled || this.mockVersion) return - autoUpdater.quitAndInstall() + getAutoUpdater().quitAndInstall() } private broadcast(channel: string, payload: unknown): void { diff --git a/src/renderer/components/layout/TopBar.tsx b/src/renderer/components/layout/TopBar.tsx index 5184c28..722bd6c 100644 --- a/src/renderer/components/layout/TopBar.tsx +++ b/src/renderer/components/layout/TopBar.tsx @@ -107,7 +107,16 @@ export function TopBar(): React.JSX.Element { > 📊 -