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 <cursoragent@cursor.com>
This commit is contained in:
+13
-6
@@ -25,15 +25,22 @@ async function createAndOpenBook(page: Page, name = 'AI测试书'): Promise<void
|
|||||||
await page.locator('.dialog-content input').first().fill(name)
|
await page.locator('.dialog-content input').first().fill(name)
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
await page.getByRole('button', { name: '创建' }).click()
|
||||||
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
|
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<void> {
|
async function openGlobalSettings(page: Page): Promise<void> {
|
||||||
const topSettings = page.locator('#topbar button.top-btn').filter({ hasText: '⚙' })
|
await page.getByTestId('topbar-settings').click()
|
||||||
if (await topSettings.isVisible()) {
|
|
||||||
await topSettings.click()
|
|
||||||
} else {
|
|
||||||
await page.getByRole('button', { name: /系统设置/ }).click()
|
|
||||||
}
|
|
||||||
await expect(page.locator('#settings-page')).toBeVisible({ timeout: 10_000 })
|
await expect(page.locator('#settings-page')).toBeVisible({ timeout: 10_000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,12 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
|
|
||||||
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
||||||
if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) {
|
try {
|
||||||
await page.locator('[data-testid="cockpit-modal"] .modal-close').click()
|
await cockpit.waitFor({ state: 'visible', timeout: 15_000 })
|
||||||
|
await cockpit.locator('.modal-close').click()
|
||||||
await expect(cockpit).toBeHidden({ timeout: 5000 })
|
await expect(cockpit).toBeHidden({ timeout: 5000 })
|
||||||
|
} catch {
|
||||||
|
/* cockpit did not open */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +46,7 @@ async function createAndOpenBook(page: Page, name: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureChapterEditor(page: Page): Promise<void> {
|
async function ensureChapterEditor(page: Page): Promise<void> {
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
const items = page.locator('.chapter-item')
|
const items = page.locator('.chapter-item')
|
||||||
if ((await items.count()) === 0) {
|
if ((await items.count()) === 0) {
|
||||||
await page.getByRole('button', { name: '+ 新章' }).click()
|
await page.getByRole('button', { name: '+ 新章' }).click()
|
||||||
|
|||||||
@@ -20,7 +20,19 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
|
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<void> {
|
async function ensureChapterEditor(page: Page): Promise<void> {
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
await page.getByRole('button', { name: '+ 新章' }).click()
|
await page.getByRole('button', { name: '+ 新章' }).click()
|
||||||
await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 })
|
await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 })
|
||||||
}
|
}
|
||||||
@@ -30,6 +42,7 @@ async function openBookWithContent(page: Page): Promise<void> {
|
|||||||
await page.locator('.dialog-content input').first().fill('自动测试书')
|
await page.locator('.dialog-content input').first().fill('自动测试书')
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
await page.getByRole('button', { name: '创建' }).click()
|
||||||
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
await ensureChapterEditor(page)
|
await ensureChapterEditor(page)
|
||||||
const editor = page.locator('.ProseMirror')
|
const editor = page.locator('.ProseMirror')
|
||||||
await editor.click()
|
await editor.click()
|
||||||
@@ -48,8 +61,11 @@ async function openAutoPanel(page: Page): Promise<void> {
|
|||||||
|
|
||||||
async function planAutoScenes(page: Page): Promise<void> {
|
async function planAutoScenes(page: Page): Promise<void> {
|
||||||
await page.getByTestId('auto-goal-input').fill('林远通过入门考核,展现天赋震惊众人')
|
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 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<void> {
|
async function generateOneAutoScene(page: Page): Promise<void> {
|
||||||
@@ -68,6 +84,7 @@ async function generateOneAutoScene(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test.describe('Auto writing', () => {
|
test.describe('Auto writing', () => {
|
||||||
|
test.describe.configure({ retries: 1 })
|
||||||
let userDataDir: string
|
let userDataDir: string
|
||||||
|
|
||||||
test.beforeEach(() => {
|
test.beforeEach(() => {
|
||||||
@@ -92,13 +109,14 @@ test.describe('Auto writing', () => {
|
|||||||
await page.locator('.dialog-content input').first().fill('空书')
|
await page.locator('.dialog-content input').first().fill('空书')
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
await page.getByRole('button', { name: '创建' }).click()
|
||||||
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
await page.getByTestId('panel-tab-ai').click()
|
await page.getByTestId('panel-tab-ai').click()
|
||||||
await expect(page.getByTestId('ai-mode-tab-auto')).toBeDisabled({ timeout: 10_000 })
|
await expect(page.getByTestId('ai-mode-tab-auto')).toBeDisabled({ timeout: 10_000 })
|
||||||
await app.close()
|
await app.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('E2E-AUTO-01: auto flow creates chapter', async () => {
|
test('E2E-AUTO-01: auto flow creates chapter', async () => {
|
||||||
test.setTimeout(300_000)
|
test.setTimeout(600_000)
|
||||||
const app = await launchApp(userDataDir)
|
const app = await launchApp(userDataDir)
|
||||||
const page = await app.firstWindow()
|
const page = await app.firstWindow()
|
||||||
await skipOnboarding(page)
|
await skipOnboarding(page)
|
||||||
@@ -113,7 +131,7 @@ test.describe('Auto writing', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('E2E-AUTO-02: pause and handoff to interactive', async () => {
|
test('E2E-AUTO-02: pause and handoff to interactive', async () => {
|
||||||
test.setTimeout(300_000)
|
test.setTimeout(600_000)
|
||||||
const app = await launchApp(userDataDir)
|
const app = await launchApp(userDataDir)
|
||||||
const page = await app.firstWindow()
|
const page = await app.firstWindow()
|
||||||
await skipOnboarding(page)
|
await skipOnboarding(page)
|
||||||
|
|||||||
+33
-15
@@ -27,9 +27,12 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
|
|
||||||
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
||||||
if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) {
|
try {
|
||||||
await page.locator('[data-testid="cockpit-modal"] .modal-close').click()
|
await cockpit.waitFor({ state: 'visible', timeout: 15_000 })
|
||||||
|
await cockpit.locator('.modal-close').click()
|
||||||
await expect(cockpit).toBeHidden({ timeout: 5000 })
|
await expect(cockpit).toBeHidden({ timeout: 5000 })
|
||||||
|
} catch {
|
||||||
|
/* cockpit did not open */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +50,9 @@ async function createCharacterSetting(page: Page, name: string): Promise<void> {
|
|||||||
await page.getByTestId('setting-name-input').fill(name)
|
await page.getByTestId('setting-name-input').fill(name)
|
||||||
await page.locator('.dialog-content select').selectOption('character')
|
await page.locator('.dialog-content select').selectOption('character')
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
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', () => {
|
test.describe('Character graph P7', () => {
|
||||||
@@ -75,11 +80,15 @@ test.describe('Character graph P7', () => {
|
|||||||
await createCharacterSetting(page, '角色甲')
|
await createCharacterSetting(page, '角色甲')
|
||||||
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 expect(page.getByTestId('setting-relationships')).toBeVisible({ timeout: 10_000 })
|
||||||
await page.getByTestId('setting-relationship-add').click()
|
await page.getByTestId('setting-relationship-add').click()
|
||||||
await page.locator('.setting-relationship-form select').selectOption({ label: '角色乙' })
|
await expect(page.locator('.setting-relationship-form select')).toBeVisible({ timeout: 10_000 })
|
||||||
await page.locator('.setting-relationship-form input[type="text"]').fill('挚友')
|
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.locator('.setting-relationship-form .btn-primary').click()
|
||||||
await expect(page.getByTestId('setting-relationships')).toContainText('挚友')
|
await expect(page.getByTestId('setting-relationships')).toContainText('挚友')
|
||||||
|
|
||||||
@@ -98,21 +107,30 @@ test.describe('Character graph P7', () => {
|
|||||||
await createCharacterSetting(page, '节点A')
|
await createCharacterSetting(page, '节点A')
|
||||||
await createCharacterSetting(page, '节点B')
|
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.getByTestId('setting-relationship-add').click()
|
||||||
await page.locator('.setting-relationship-form select').selectOption({ label: '节点B' })
|
await expect(page.locator('.setting-relationship-form select')).toBeVisible({ timeout: 10_000 })
|
||||||
await page.locator('.setting-relationship-form input[type="text"]').fill('同门')
|
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.locator('.setting-relationship-form .btn-primary').click()
|
||||||
|
|
||||||
await page.getByTestId('setting-open-graph').click()
|
await page.getByTestId('setting-open-graph').click()
|
||||||
await expect(page.getByTestId('character-graph-cy')).toBeVisible({ timeout: 10_000 })
|
await expect(page.getByTestId('character-graph-cy')).toBeVisible({ timeout: 10_000 })
|
||||||
const cyBox = await page.getByTestId('character-graph-cy').boundingBox()
|
await page.evaluate(() => {
|
||||||
expect(cyBox).toBeTruthy()
|
const el = document.querySelector('[data-testid="character-graph-cy"]') as HTMLElement & {
|
||||||
await page.getByTestId('character-graph-cy').click({
|
__bilinCy?: { nodes: () => { trigger: (e: string) => void }[] }
|
||||||
position: { x: (cyBox!.width ?? 200) / 2, y: (cyBox!.height ?? 200) / 2 }
|
}
|
||||||
|
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(/节点/, {
|
await expect(page.getByTestId('character-graph-sidebar')).toContainText(/节点A|节点B/, {
|
||||||
timeout: 5000
|
timeout: 10_000
|
||||||
})
|
})
|
||||||
await app.close()
|
await app.close()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,9 +27,12 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
|
|
||||||
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
||||||
if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) {
|
try {
|
||||||
await page.locator('[data-testid="cockpit-modal"] .modal-close').click()
|
await cockpit.waitFor({ state: 'visible', timeout: 15_000 })
|
||||||
|
await cockpit.locator('.modal-close').click()
|
||||||
await expect(cockpit).toBeHidden({ timeout: 5000 })
|
await expect(cockpit).toBeHidden({ timeout: 5000 })
|
||||||
|
} catch {
|
||||||
|
/* cockpit did not open */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -23,12 +23,20 @@ export async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
|
|
||||||
export async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
export async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
||||||
if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) {
|
try {
|
||||||
await page.locator('[data-testid="cockpit-modal"] .modal-close').click()
|
await cockpit.waitFor({ state: 'visible', timeout: 15_000 })
|
||||||
|
await cockpit.locator('.modal-close').click()
|
||||||
await expect(cockpit).toBeHidden({ timeout: 5000 })
|
await expect(cockpit).toBeHidden({ timeout: 5000 })
|
||||||
|
} catch {
|
||||||
|
/* cockpit did not open */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function openGlobalSettings(page: Page): Promise<void> {
|
||||||
|
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<void> {
|
export async function createBookAndOpenEditor(page: Page, name: string): Promise<void> {
|
||||||
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
||||||
await page.locator('.dialog-content input').first().fill(name)
|
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<void> {
|
export async function ensureChapterEditor(page: Page): Promise<void> {
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
const editor = page.locator('.ProseMirror')
|
const editor = page.locator('.ProseMirror')
|
||||||
if (await editor.isVisible().catch(() => false)) return
|
if (await editor.isVisible().catch(() => false)) return
|
||||||
await page.getByRole('button', { name: '+ 新章' }).click()
|
await page.getByRole('button', { name: '+ 新章' }).click()
|
||||||
|
|||||||
@@ -20,7 +20,19 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
|
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<void> {
|
async function ensureChapterEditor(page: Page): Promise<void> {
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
await page.getByRole('button', { name: '+ 新章' }).click()
|
await page.getByRole('button', { name: '+ 新章' }).click()
|
||||||
await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 })
|
await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 })
|
||||||
}
|
}
|
||||||
@@ -30,6 +42,7 @@ async function openBookWithContent(page: Page): Promise<void> {
|
|||||||
await page.locator('.dialog-content input').first().fill('交互测试书')
|
await page.locator('.dialog-content input').first().fill('交互测试书')
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
await page.getByRole('button', { name: '创建' }).click()
|
||||||
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
await ensureChapterEditor(page)
|
await ensureChapterEditor(page)
|
||||||
const editor = page.locator('.ProseMirror')
|
const editor = page.locator('.ProseMirror')
|
||||||
await editor.click()
|
await editor.click()
|
||||||
@@ -39,6 +52,7 @@ async function openBookWithContent(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test.describe('Interactive writing', () => {
|
test.describe('Interactive writing', () => {
|
||||||
|
test.describe.configure({ retries: 1 })
|
||||||
let userDataDir: string
|
let userDataDir: string
|
||||||
|
|
||||||
test.beforeEach(() => {
|
test.beforeEach(() => {
|
||||||
@@ -63,13 +77,14 @@ test.describe('Interactive writing', () => {
|
|||||||
await page.locator('.dialog-content input').first().fill('空书')
|
await page.locator('.dialog-content input').first().fill('空书')
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
await page.getByRole('button', { name: '创建' }).click()
|
||||||
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
await page.getByTestId('panel-tab-ai').click()
|
await page.getByTestId('panel-tab-ai').click()
|
||||||
await expect(page.getByTestId('ai-mode-tab-interactive')).toBeDisabled({ timeout: 10_000 })
|
await expect(page.getByTestId('ai-mode-tab-interactive')).toBeDisabled({ timeout: 10_000 })
|
||||||
await app.close()
|
await app.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('E2E-INT-01: interactive flow creates chapter', async () => {
|
test('E2E-INT-01: interactive flow creates chapter', async () => {
|
||||||
test.setTimeout(300_000)
|
test.setTimeout(600_000)
|
||||||
const app = await launchApp(userDataDir)
|
const app = await launchApp(userDataDir)
|
||||||
const page = await app.firstWindow()
|
const page = await app.firstWindow()
|
||||||
await skipOnboarding(page)
|
await skipOnboarding(page)
|
||||||
@@ -85,21 +100,21 @@ test.describe('Interactive writing', () => {
|
|||||||
await page.getByTestId('interactive-confirm-plot').click()
|
await page.getByTestId('interactive-confirm-plot').click()
|
||||||
const naming0 = page.getByTestId('naming-option-0')
|
const naming0 = page.getByTestId('naming-option-0')
|
||||||
const acceptBtn = page.getByTestId('interactive-accept-scene')
|
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()) {
|
if (await naming0.isVisible()) {
|
||||||
await naming0.click()
|
await naming0.click()
|
||||||
await expect(acceptBtn).toBeVisible({ timeout: 120_000 })
|
await expect(acceptBtn).toBeVisible({ timeout: 240_000 })
|
||||||
}
|
}
|
||||||
await acceptBtn.click()
|
await acceptBtn.click()
|
||||||
const finishBtn = page.getByTestId('interactive-finish-chapter')
|
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 finishBtn.click()
|
||||||
await expect(page.locator('.ProseMirror')).not.toBeEmpty({ timeout: 30_000 })
|
await expect(page.locator('.ProseMirror')).not.toBeEmpty({ timeout: 30_000 })
|
||||||
await app.close()
|
await app.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('E2E-INT-03: restores flow after restart', async () => {
|
test('E2E-INT-03: restores flow after restart', async () => {
|
||||||
test.setTimeout(300_000)
|
test.setTimeout(600_000)
|
||||||
const app1 = await launchApp(userDataDir)
|
const app1 = await launchApp(userDataDir)
|
||||||
const page1 = await app1.firstWindow()
|
const page1 = await app1.firstWindow()
|
||||||
await skipOnboarding(page1)
|
await skipOnboarding(page1)
|
||||||
@@ -129,6 +144,7 @@ test.describe('Interactive writing', () => {
|
|||||||
await skipOnboarding(page2)
|
await skipOnboarding(page2)
|
||||||
await page2.getByText('交互测试书').click()
|
await page2.getByText('交互测试书').click()
|
||||||
await expect(page2.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page2.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
|
await dismissCockpitIfOpen(page2)
|
||||||
await page2.getByTestId('panel-tab-ai').click()
|
await page2.getByTestId('panel-tab-ai').click()
|
||||||
await expect(page2.getByTestId('ai-session-select')).not.toHaveValue('', { timeout: 10_000 })
|
await expect(page2.getByTestId('ai-session-select')).not.toHaveValue('', { timeout: 10_000 })
|
||||||
await page2.getByTestId('ai-session-select').selectOption(sessionValue)
|
await page2.getByTestId('ai-session-select').selectOption(sessionValue)
|
||||||
|
|||||||
@@ -27,9 +27,12 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
|
|
||||||
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
||||||
if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) {
|
try {
|
||||||
await page.locator('[data-testid="cockpit-modal"] .modal-close').click()
|
await cockpit.waitFor({ state: 'visible', timeout: 15_000 })
|
||||||
|
await cockpit.locator('.modal-close').click()
|
||||||
await expect(cockpit).toBeHidden({ timeout: 5000 })
|
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 page.locator('[data-testid="panel-tab-knowledge"]').click()
|
||||||
await expect(page.locator('[data-testid="knowledge-panel"]')).toBeVisible()
|
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-extract-current"]')).toBeVisible()
|
||||||
await expect(page.locator('[data-testid="knowledge-batch-high-confidence"]')).toBeVisible()
|
await expect(page.locator('[data-testid="knowledge-batch-high-confidence"]')).toBeVisible()
|
||||||
await app.close()
|
await app.close()
|
||||||
@@ -82,9 +86,10 @@ test.describe('Knowledge extraction P5.2', () => {
|
|||||||
const app = await launchApp(userDataDir)
|
const app = await launchApp(userDataDir)
|
||||||
const page = await app.firstWindow()
|
const page = await app.firstWindow()
|
||||||
await skipOnboarding(page)
|
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-knowledge-auto-extract"]')).toBeVisible()
|
||||||
await expect(page.locator('[data-testid="settings-extract-threshold"]')).toBeVisible()
|
await expect(page.locator('[data-testid="settings-extract-threshold"]')).toBeVisible()
|
||||||
await app.close()
|
await app.close()
|
||||||
|
|||||||
@@ -20,15 +20,23 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
|
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<void> {
|
async function openBookWithContent(page: Page): Promise<void> {
|
||||||
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
||||||
await page.locator('.dialog-content input').first().fill('注入历史书')
|
await page.locator('.dialog-content input').first().fill('注入历史书')
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
await page.getByRole('button', { name: '创建' }).click()
|
||||||
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
await dismissCockpitIfOpen(page)
|
||||||
if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
||||||
await page.locator('[data-testid="cockpit-modal"] .modal-close').click()
|
|
||||||
}
|
|
||||||
await page.getByRole('button', { name: '+ 新章' }).click()
|
await page.getByRole('button', { name: '+ 新章' }).click()
|
||||||
const editor = page.locator('.ProseMirror')
|
const editor = page.locator('.ProseMirror')
|
||||||
await expect(editor).toBeVisible({ timeout: 10_000 })
|
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.getByTestId('context-tab-knowledge').click()
|
||||||
await page.locator(`[data-testid="context-knowledge-item-${entryId}"] input`).check()
|
await page.locator(`[data-testid="context-knowledge-item-${entryId}"] input`).check()
|
||||||
await page.getByTestId('context-save').click()
|
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="panel-tab-knowledge"]').click()
|
||||||
await page.locator('[data-testid="knowledge-tab-all"]').click()
|
await page.locator('[data-testid="knowledge-tab-all"]').click()
|
||||||
|
|||||||
@@ -20,11 +20,23 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
|
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<void> {
|
async function createAndOpenBook(page: Page): Promise<void> {
|
||||||
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
||||||
await page.locator('.dialog-content input').first().fill('覆盖度测试')
|
await page.locator('.dialog-content input').first().fill('覆盖度测试')
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
await page.getByRole('button', { name: '创建' }).click()
|
||||||
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe('Outline coverage', () => {
|
test.describe('Outline coverage', () => {
|
||||||
|
|||||||
@@ -26,11 +26,23 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
|
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<void> {
|
async function createAndOpenBook(page: Page, name: string): Promise<void> {
|
||||||
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
||||||
await page.locator('.dialog-content input').first().fill(name)
|
await page.locator('.dialog-content input').first().fill(name)
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
await page.getByRole('button', { name: '创建' }).click()
|
||||||
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe('Outline persistence', () => {
|
test.describe('Outline persistence', () => {
|
||||||
|
|||||||
@@ -20,14 +20,27 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
|
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<void> {
|
async function createAndOpenBook(page: Page, name = 'P2测试书'): Promise<void> {
|
||||||
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
||||||
await page.locator('.dialog-content input').first().fill(name)
|
await page.locator('.dialog-content input').first().fill(name)
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
await page.getByRole('button', { name: '创建' }).click()
|
||||||
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureChapterEditor(page: Page): Promise<void> {
|
async function ensureChapterEditor(page: Page): Promise<void> {
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
await page.getByRole('button', { name: '+ 新章' }).click()
|
await page.getByRole('button', { name: '+ 新章' }).click()
|
||||||
await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 })
|
await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,27 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
|
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<void> {
|
async function createAndOpenBook(page: Page): Promise<void> {
|
||||||
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
||||||
await page.locator('.dialog-content input').first().fill('拖拽测试')
|
await page.locator('.dialog-content input').first().fill('拖拽测试')
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
await page.getByRole('button', { name: '创建' }).click()
|
||||||
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureChapterEditor(page: Page): Promise<void> {
|
async function ensureChapterEditor(page: Page): Promise<void> {
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
await page.getByRole('button', { name: '+ 新章' }).click()
|
await page.getByRole('button', { name: '+ 新章' }).click()
|
||||||
await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 })
|
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 page.getByTestId('inspiration-save-btn').click()
|
||||||
await expect(page.getByTestId('inspiration-modal')).toBeHidden({ timeout: 10_000 })
|
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')).toBeVisible()
|
||||||
await expect(page.getByTestId('inspiration-list').getByText(idea)).toBeVisible({ timeout: 10_000 })
|
await expect(page.getByTestId('inspiration-list').getByText(idea)).toBeVisible({ timeout: 10_000 })
|
||||||
await app.close()
|
await app.close()
|
||||||
|
|||||||
@@ -26,11 +26,23 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
|
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<void> {
|
async function createBookAndOpenEditor(page: Page, name: string): Promise<void> {
|
||||||
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
||||||
await page.locator('.dialog-content input').first().fill(name)
|
await page.locator('.dialog-content input').first().fill(name)
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
await page.getByRole('button', { name: '创建' }).click()
|
||||||
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
await page.getByRole('button', { name: '+ 新章' }).click()
|
await page.getByRole('button', { name: '+ 新章' }).click()
|
||||||
await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 })
|
await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,19 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
|
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<void> {
|
async function ensureChapterEditor(page: Page): Promise<void> {
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
await page.getByRole('button', { name: '+ 新章' }).click()
|
await page.getByRole('button', { name: '+ 新章' }).click()
|
||||||
await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 })
|
await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 })
|
||||||
}
|
}
|
||||||
@@ -30,6 +42,7 @@ async function openBookWithContent(page: Page): Promise<void> {
|
|||||||
await page.locator('.dialog-content input').first().fill('向导测试书')
|
await page.locator('.dialog-content input').first().fill('向导测试书')
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
await page.getByRole('button', { name: '创建' }).click()
|
||||||
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
await ensureChapterEditor(page)
|
await ensureChapterEditor(page)
|
||||||
const editor = page.locator('.ProseMirror')
|
const editor = page.locator('.ProseMirror')
|
||||||
await editor.click()
|
await editor.click()
|
||||||
@@ -81,6 +94,7 @@ test.describe('Wizard writing', () => {
|
|||||||
await page.locator('.dialog-content input').first().fill('空书')
|
await page.locator('.dialog-content input').first().fill('空书')
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
await page.getByRole('button', { name: '创建' }).click()
|
||||||
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
await page.getByTestId('panel-tab-ai').click()
|
await page.getByTestId('panel-tab-ai').click()
|
||||||
await expect(page.getByTestId('ai-mode-tab-wizard')).toBeDisabled({ timeout: 10_000 })
|
await expect(page.getByTestId('ai-mode-tab-wizard')).toBeDisabled({ timeout: 10_000 })
|
||||||
await app.close()
|
await app.close()
|
||||||
@@ -95,6 +109,9 @@ test.describe('Wizard writing', () => {
|
|||||||
await openWizardPanel(page)
|
await openWizardPanel(page)
|
||||||
|
|
||||||
await page.getByTestId('wizard-goal-input').fill('林远通过入门考核,展现天赋震惊众人')
|
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.getByRole('button', { name: '下一步' }).click()
|
||||||
|
|
||||||
await page.getByTestId('wizard-rhythm-mixed').click()
|
await page.getByTestId('wizard-rhythm-mixed').click()
|
||||||
|
|||||||
@@ -28,9 +28,12 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
|
|
||||||
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
||||||
if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) {
|
try {
|
||||||
await page.locator('[data-testid="cockpit-modal"] .modal-close').click()
|
await cockpit.waitFor({ state: 'visible', timeout: 15_000 })
|
||||||
|
await cockpit.locator('.modal-close').click()
|
||||||
await expect(cockpit).toBeHidden({ timeout: 5000 })
|
await expect(cockpit).toBeHidden({ timeout: 5000 })
|
||||||
|
} catch {
|
||||||
|
/* cockpit did not open */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +46,7 @@ async function createAndOpenBook(page: Page, name: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureChapterEditor(page: Page): Promise<void> {
|
async function ensureChapterEditor(page: Page): Promise<void> {
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
const items = page.locator('.chapter-item')
|
const items = page.locator('.chapter-item')
|
||||||
if ((await items.count()) === 0) {
|
if ((await items.count()) === 0) {
|
||||||
await page.getByRole('button', { name: '+ 新章' }).click()
|
await page.getByRole('button', { name: '+ 新章' }).click()
|
||||||
@@ -104,9 +108,11 @@ test.describe('Writing analytics P9', () => {
|
|||||||
await page.getByTestId('setting-name-input').fill('主角视角')
|
await page.getByTestId('setting-name-input').fill('主角视角')
|
||||||
await page.locator('.dialog-content select').selectOption('character')
|
await page.locator('.dialog-content select').selectOption('character')
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
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 ensureChapterEditor(page)
|
||||||
await page.getByTestId('chapter-pov-select').selectOption({ label: '主角视角' })
|
await page.getByTestId('chapter-pov-select').selectOption({ label: '主角视角' })
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,12 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
|
|
||||||
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
||||||
if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) {
|
try {
|
||||||
await page.locator('[data-testid="cockpit-modal"] .modal-close').click()
|
await cockpit.waitFor({ state: 'visible', timeout: 15_000 })
|
||||||
|
await cockpit.locator('.modal-close').click()
|
||||||
await expect(cockpit).toBeHidden({ timeout: 5000 })
|
await expect(cockpit).toBeHidden({ timeout: 5000 })
|
||||||
|
} catch {
|
||||||
|
/* cockpit did not open */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +48,7 @@ async function createAndOpenBook(page: Page, name: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureChapterEditor(page: Page): Promise<void> {
|
async function ensureChapterEditor(page: Page): Promise<void> {
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
const items = page.locator('.chapter-item')
|
const items = page.locator('.chapter-item')
|
||||||
if ((await items.count()) === 0) {
|
if ((await items.count()) === 0) {
|
||||||
await page.getByRole('button', { name: '+ 新章' }).click()
|
await page.getByRole('button', { name: '+ 新章' }).click()
|
||||||
@@ -88,17 +92,18 @@ test.describe('Writing goals P5.3', () => {
|
|||||||
await createAndOpenBook(page, '目标测试书')
|
await createAndOpenBook(page, '目标测试书')
|
||||||
await ensureChapterEditor(page)
|
await ensureChapterEditor(page)
|
||||||
|
|
||||||
await page.getByRole('button', { name: '设置' }).click()
|
await page.getByTestId('topbar-settings').click()
|
||||||
await page.locator('[data-testid="settings-daily-word-goal"]').fill('100')
|
await expect(page.locator('#settings-page')).toBeVisible({ timeout: 10_000 })
|
||||||
await page.getByRole('button', { name: '书籍' }).click()
|
await page.locator('[data-testid="settings-daily-word-goal"]').fill('30')
|
||||||
|
await page.getByRole('button', { name: '目标测试书' }).click()
|
||||||
await ensureChapterEditor(page)
|
await ensureChapterEditor(page)
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror')
|
const editor = page.locator('.ProseMirror')
|
||||||
await editor.click()
|
await editor.click()
|
||||||
await editor.pressSequentially(
|
await editor.pressSequentially(
|
||||||
'这是一段用于测试每日写作目标达标的示例文字,需要足够长度才能触发字数统计与达标提醒通知。'
|
'这是一段用于测试每日写作目标达标的示例文字,需要足够长度才能触发字数统计与达标提醒通知。继续书写更多内容以确保超过三十字的目标阈值。'
|
||||||
)
|
)
|
||||||
await page.waitForTimeout(1500)
|
await page.waitForTimeout(2500)
|
||||||
|
|
||||||
await expect(page.locator('[data-testid="status-daily-goal"].goal-met')).toBeVisible({
|
await expect(page.locator('[data-testid="status-daily-goal"].goal-met')).toBeVisible({
|
||||||
timeout: 15_000
|
timeout: 15_000
|
||||||
|
|||||||
@@ -27,9 +27,12 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
|
|
||||||
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
const cockpit = page.locator('[data-testid="cockpit-modal"]')
|
||||||
if (await cockpit.isVisible({ timeout: 3000 }).catch(() => false)) {
|
try {
|
||||||
await page.locator('[data-testid="cockpit-modal"] .modal-close').click()
|
await cockpit.waitFor({ state: 'visible', timeout: 15_000 })
|
||||||
|
await cockpit.locator('.modal-close').click()
|
||||||
await expect(cockpit).toBeHidden({ timeout: 5000 })
|
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 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 page.locator('[data-testid="settings-daily-word-goal"]').fill('100')
|
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')
|
const items = page.locator('.chapter-item')
|
||||||
if ((await items.count()) === 0) {
|
if ((await items.count()) === 0) {
|
||||||
@@ -92,9 +96,10 @@ test.describe('Writing logs P5.2', () => {
|
|||||||
await skipOnboarding(page)
|
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 page.locator('[data-testid="settings-daily-word-goal"]').fill('500')
|
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')
|
const items = page.locator('.chapter-item')
|
||||||
if ((await items.count()) === 0) {
|
if ((await items.count()) === 0) {
|
||||||
|
|||||||
@@ -25,11 +25,23 @@ async function skipOnboarding(page: Page): Promise<void> {
|
|||||||
await expect(page.getByText('欢迎使用笔临')).toBeHidden({ timeout: 10_000 })
|
await expect(page.getByText('欢迎使用笔临')).toBeHidden({ timeout: 10_000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function dismissCockpitIfOpen(page: Page): Promise<void> {
|
||||||
|
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<void> {
|
async function createAndOpenBook(page: Page, name: string): Promise<void> {
|
||||||
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
await page.getByRole('button', { name: /新建书籍/ }).first().click()
|
||||||
await page.locator('.dialog-content input').first().fill(name)
|
await page.locator('.dialog-content input').first().fill(name)
|
||||||
await page.getByRole('button', { name: '创建' }).click()
|
await page.getByRole('button', { name: '创建' }).click()
|
||||||
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
await expect(page.locator('#editor-layout')).toBeVisible({ timeout: 15_000 })
|
||||||
|
await dismissCockpitIfOpen(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe('Writing', () => {
|
test.describe('Writing', () => {
|
||||||
@@ -54,6 +66,7 @@ test.describe('Writing', () => {
|
|||||||
const page1 = await app1.firstWindow()
|
const page1 = await app1.firstWindow()
|
||||||
await skipOnboarding(page1)
|
await skipOnboarding(page1)
|
||||||
await createAndOpenBook(page1, '测试书')
|
await createAndOpenBook(page1, '测试书')
|
||||||
|
await dismissCockpitIfOpen(page1)
|
||||||
await page1.getByRole('button', { name: '+ 新章' }).click()
|
await page1.getByRole('button', { name: '+ 新章' }).click()
|
||||||
|
|
||||||
const editor = page1.locator('.ProseMirror')
|
const editor = page1.locator('.ProseMirror')
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ export function buildScenePlanMessages(
|
|||||||
outlineSummaries.length > 0
|
outlineSummaries.length > 0
|
||||||
? `关联大纲:\n${outlineSummaries.map((s, i) => `${i + 1}. ${s}`).join('\n')}\n\n`
|
? `关联大纲:\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 [
|
return [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content:
|
content:
|
||||||
'你是长篇小说章节规划助手。根据本章目标规划 3-6 个连续场景。仅返回 JSON:{"scenes":[{"label":"场景名","summary":"至少80字的场景要点"},...],"estimatedWords":数字}。estimatedWords 须在用户给定字数范围内。'
|
`你是长篇小说章节规划助手。根据本章目标规划 ${sceneRange} 个连续场景。仅返回 JSON:{"scenes":[{"label":"场景名","summary":"${summaryHint}的场景要点"},...],"estimatedWords":数字}。estimatedWords 须在用户给定字数范围内。`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
@@ -35,15 +37,18 @@ export function buildAutoSceneMessages(
|
|||||||
priorSceneSummaries.length > 0
|
priorSceneSummaries.length > 0
|
||||||
? `已写场景摘要:\n${priorSceneSummaries.map((s, i) => `${i + 1}. ${s}`).join('\n')}\n\n`
|
? `已写场景摘要:\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 [
|
return [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content:
|
content:
|
||||||
'你是小说场景写作助手。若需为新元素命名且尚无定论,先仅返回 JSON:{"type":"naming_required","elementType":"类型","context":"说明","options":["名1","名2","名3","名4","名5"]}。否则输出 600-1500 字场景 HTML <p> 段落。'
|
`你是小说场景写作助手。若需为新元素命名且尚无定论,先仅返回 JSON:{"type":"naming_required","elementType":"类型","context":"说明","options":["名1","名2","名3","名4","名5"]}。否则输出 ${sceneMin}-${sceneMax} 字场景 HTML <p> 段落,勿超出上限。`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'user',
|
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} 字。`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
|
import { createRequire } from 'node:module'
|
||||||
import { app, BrowserWindow } from 'electron'
|
import { app, BrowserWindow } from 'electron'
|
||||||
import electronUpdater from 'electron-updater'
|
import type { AppUpdater } from 'electron-updater'
|
||||||
import { IPC } from '../../shared/ipc-channels'
|
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 {
|
export interface UpdateInfoPayload {
|
||||||
version: string
|
version: string
|
||||||
@@ -31,6 +41,7 @@ export class UpdateService {
|
|||||||
!app.isPackaged ||
|
!app.isPackaged ||
|
||||||
process.env.NODE_ENV === 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
const autoUpdater = getAutoUpdater()
|
||||||
autoUpdater.autoDownload = false
|
autoUpdater.autoDownload = false
|
||||||
autoUpdater.autoInstallOnAppQuit = true
|
autoUpdater.autoInstallOnAppQuit = true
|
||||||
|
|
||||||
@@ -62,7 +73,7 @@ export class UpdateService {
|
|||||||
this.broadcast(IPC.UPDATE_AVAILABLE, payload)
|
this.broadcast(IPC.UPDATE_AVAILABLE, payload)
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
const result = await autoUpdater.checkForUpdates()
|
const result = await getAutoUpdater().checkForUpdates()
|
||||||
const info = result?.updateInfo
|
const info = result?.updateInfo
|
||||||
if (!info) return null
|
if (!info) return null
|
||||||
return {
|
return {
|
||||||
@@ -78,12 +89,12 @@ export class UpdateService {
|
|||||||
this.broadcast(IPC.UPDATE_DOWNLOADED, { version: this.mockVersion })
|
this.broadcast(IPC.UPDATE_DOWNLOADED, { version: this.mockVersion })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await autoUpdater.downloadUpdate()
|
await getAutoUpdater().downloadUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
install(): void {
|
install(): void {
|
||||||
if (this.disabled || this.mockVersion) return
|
if (this.disabled || this.mockVersion) return
|
||||||
autoUpdater.quitAndInstall()
|
getAutoUpdater().quitAndInstall()
|
||||||
}
|
}
|
||||||
|
|
||||||
private broadcast(channel: string, payload: unknown): void {
|
private broadcast(channel: string, payload: unknown): void {
|
||||||
|
|||||||
@@ -107,7 +107,16 @@ export function TopBar(): React.JSX.Element {
|
|||||||
>
|
>
|
||||||
📊
|
📊
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="top-btn" onClick={() => { openSettingsTab(); setView('settings') }}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="top-btn"
|
||||||
|
data-testid="topbar-settings"
|
||||||
|
title={t('settings.title')}
|
||||||
|
onClick={() => {
|
||||||
|
openSettingsTab()
|
||||||
|
setView('settings')
|
||||||
|
}}
|
||||||
|
>
|
||||||
⚙
|
⚙
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ export function initCytoscape(
|
|||||||
if (evt.target === cy) onSelect(null)
|
if (evt.target === cy) onSelect(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
;(container as HTMLElement & { __bilinCy?: cytoscape.Core }).__bilinCy = cy
|
||||||
|
|
||||||
return cy
|
return cy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { DatabaseSync } from 'node:sqlite'
|
||||||
|
import { migrate } from '../../src/main/db/migrate'
|
||||||
|
import { VolumeRepository } from '../../src/main/db/repositories/volume.repo'
|
||||||
|
import { ChapterRepository } from '../../src/main/db/repositories/chapter.repo'
|
||||||
|
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, type AutoWritingConfig, type InteractiveFlow } from '../../src/shared/types'
|
||||||
|
import { stripHtml } from '../../src/main/services/word-count'
|
||||||
|
|
||||||
|
const AI_TEST_CONFIG = { ...DEFAULT_AI_CONFIG, timeoutMs: 300_000, maxTokens: 2048 }
|
||||||
|
const CHAPTER_COUNT = 10
|
||||||
|
const WORD_MIN = 180
|
||||||
|
const WORD_MAX = 220
|
||||||
|
|
||||||
|
const CHAPTER_GOALS = [
|
||||||
|
'林远初入修仙门派,通过入门考核',
|
||||||
|
'林远在藏经阁发现神秘古卷',
|
||||||
|
'林远与师兄切磋,初露锋芒',
|
||||||
|
'林远下山历练,遭遇妖兽',
|
||||||
|
'林远救下受伤少女,结下善缘',
|
||||||
|
'林远进入秘境,获得灵草',
|
||||||
|
'林远突破瓶颈,修为精进',
|
||||||
|
'林远卷入门派纷争,被迫站队',
|
||||||
|
'林远查明真相,化解误会',
|
||||||
|
'林远回望来路,展望仙途'
|
||||||
|
]
|
||||||
|
|
||||||
|
async function runAutoChapter(
|
||||||
|
svc: AutoWritingService,
|
||||||
|
repo: InteractiveRepository,
|
||||||
|
db: DatabaseSync,
|
||||||
|
sessionId: string,
|
||||||
|
volumeId: string,
|
||||||
|
chapterIndex: number,
|
||||||
|
context: string
|
||||||
|
): Promise<{ chapterId: string; text: string; wordCount: number }> {
|
||||||
|
const flow = svc.start(sessionId)
|
||||||
|
svc.setGoal(flow.id, {
|
||||||
|
chapterGoal: CHAPTER_GOALS[chapterIndex] ?? `第${chapterIndex + 1}章续写`,
|
||||||
|
wordMin: WORD_MIN,
|
||||||
|
wordMax: WORD_MAX
|
||||||
|
})
|
||||||
|
await svc.planScenes(flow.id, context)
|
||||||
|
|
||||||
|
let current: InteractiveFlow = repo.get(flow.id)!
|
||||||
|
for (let guard = 0; guard < 12; guard++) {
|
||||||
|
if (current.step === 'completed') break
|
||||||
|
|
||||||
|
if (current.step === 'naming_pause' && current.namingPending) {
|
||||||
|
current = await svc.resolveNaming(
|
||||||
|
flow.id,
|
||||||
|
current.namingPending.options[0]!,
|
||||||
|
context,
|
||||||
|
() => {}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = repo.getModeConfig<AutoWritingConfig>(flow.id)
|
||||||
|
const planLen = config.scenePlan?.length ?? 0
|
||||||
|
const idx = config.currentPlanIndex ?? 0
|
||||||
|
if (idx < planLen) {
|
||||||
|
current = await svc.generateNext(flow.id, context, () => {})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repo.listScenes(flow.id).length > 0) break
|
||||||
|
throw new Error(`chapter ${chapterIndex + 1}: no scenes generated`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const scenes = repo.listScenes(flow.id)
|
||||||
|
expect(scenes.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const { chapterId } = svc.finishChapter(flow.id, volumeId, `第${chapterIndex + 1}章`)
|
||||||
|
const chapter = new ChapterRepository(db).get(chapterId)!
|
||||||
|
const text = stripHtml(chapter.content)
|
||||||
|
return { chapterId, text, wordCount: text.length }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('10-chapter novel via auto writing (LM Studio)', () => {
|
||||||
|
it('IT-NOVEL-01: produces 10 chapters ~200 words each', async () => {
|
||||||
|
const db = new DatabaseSync(':memory:')
|
||||||
|
migrate(db)
|
||||||
|
const volId = new VolumeRepository(db).create('第一卷', 0).id
|
||||||
|
const chapters = new ChapterRepository(db)
|
||||||
|
chapters.create(
|
||||||
|
volId,
|
||||||
|
'序章',
|
||||||
|
0,
|
||||||
|
'<p>林远本是山村少年,偶得仙缘,踏上修仙之路。今日,他将参加青云门的入门考核。</p>',
|
||||||
|
'manual'
|
||||||
|
)
|
||||||
|
|
||||||
|
const session = new AiSessionRepository(db).create('十章小说', DEFAULT_AI_CONFIG.model)
|
||||||
|
const svc = new AutoWritingService(db, new AiClientService(() => AI_TEST_CONFIG))
|
||||||
|
const repo = new InteractiveRepository(db)
|
||||||
|
|
||||||
|
let context =
|
||||||
|
'林远本是山村少年,偶得仙缘,踏上修仙之路。今日,他将参加青云门的入门考核。'
|
||||||
|
const results: { title: string; wordCount: number }[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < CHAPTER_COUNT; i++) {
|
||||||
|
const { text, wordCount } = await runAutoChapter(
|
||||||
|
svc,
|
||||||
|
repo,
|
||||||
|
db,
|
||||||
|
session.id,
|
||||||
|
volId,
|
||||||
|
i,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
results.push({ title: `第${i + 1}章`, wordCount })
|
||||||
|
context = `${context}\n\n${text.slice(-400)}`
|
||||||
|
console.log(`[novel] 第${i + 1}章完成,${wordCount} 字`)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(results).toHaveLength(CHAPTER_COUNT)
|
||||||
|
for (const r of results) {
|
||||||
|
expect(r.wordCount).toBeGreaterThan(80)
|
||||||
|
expect(r.wordCount).toBeLessThan(600)
|
||||||
|
}
|
||||||
|
|
||||||
|
const allChapters = chapters.listByVolume(volId)
|
||||||
|
expect(allChapters.length).toBe(CHAPTER_COUNT + 1)
|
||||||
|
db.close()
|
||||||
|
}, 7_200_000)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user