import { expect, Page, test } from '@playwright/test'; import path from 'node:path'; declare global { interface Window { __seedIpOperatorScenario?: (scenario: 'empty' | 'ready' | 'needs_input' | 'failed') => unknown; } } const scenarioSeedPath = path.resolve(__dirname, '../scripts/validation/ip-operator-scenario-seed.js'); async function seedScenario(page: Page, scenario: 'empty' | 'ready' | 'needs_input' | 'failed', startTab: 'home' | 'ip-operator' = 'ip-operator') { await page.addInitScript({ path: scenarioSeedPath }); await page.addInitScript(([nextScenario, nextTab]) => { window.__seedIpOperatorScenario?.(nextScenario); localStorage.setItem('tiktok.currentTab', nextTab); }, [scenario, startTab]); } test('sidebar entry and wizard form reach generation confirmation', async ({ page }) => { await seedScenario(page, 'empty', 'home'); await page.goto('/'); await page.getByRole('button', { name: /IP操盘/ }).click(); await expect(page.locator('.ip-operator-page')).toBeVisible(); await expect(page.locator('.ip-card h2')).toContainText('创建 IP 档案'); const profileCard = page.locator('.ip-card').first(); await profileCard.locator('input').nth(0).fill('E2E New Profile'); await profileCard.locator('input').nth(1).fill('Local growth consultant'); await profileCard.locator('input').nth(2).fill('Local service'); await profileCard.locator('input').nth(3).fill('Traditional business owners'); await profileCard.locator('input').nth(4).fill('IP diagnosis and content operation'); await profileCard.locator('textarea').nth(0).fill('Content looks like advertising'); await profileCard.locator('textarea').nth(1).fill('Served local stores with short-video conversion'); await profileCard.locator('textarea').nth(2).fill('Positioning, topics, scripts'); await profileCard.locator('textarea').nth(5).fill('Generate a usable 7-day test plan'); await profileCard.locator('.btn-primary').click(); const questionnaireCard = page.locator('.ip-card').first(); await expect(questionnaireCard.locator('h2')).toContainText('轻问卷'); await questionnaireCard.locator('textarea').nth(0).fill('They want trust but dislike hard selling.'); await questionnaireCard.locator('textarea').nth(1).fill('I can turn cases into practical content.'); await questionnaireCard.locator('textarea').nth(2).fill('I have real local-service experience.'); await questionnaireCard.locator('.btn-primary').click(); const benchmarkCard = page.locator('.ip-card').first(); await expect(benchmarkCard.locator('h2')).toContainText('对标账号'); await benchmarkCard.locator('input').nth(0).fill('Benchmark Owner Account'); await benchmarkCard.locator('input').nth(2).fill('Why owner content fails to convert'); await benchmarkCard.locator('textarea').nth(1).fill('It uses direct pain-point diagnosis.'); await benchmarkCard.locator('.btn-primary').click(); const generateCard = page.locator('.ip-card').first(); await expect(generateCard.locator('h2')).toContainText('确认生成范围'); await expect(generateCard.locator('.ip-generate-scope span')).toHaveCount(9); await expect(generateCard.locator('.btn-primary')).toBeEnabled(); }); test('needs_input plan blocks production entry points in the browser', async ({ page }) => { await seedScenario(page, 'needs_input'); await page.goto('/'); await expect(page.locator('.ip-result')).toBeVisible(); await expect(page.locator('.ip-readiness-panel.is-blocked')).toBeVisible(); await expect(page.locator('.ip-summary-card').filter({ hasText: '64/100' })).toBeVisible(); await expect(page.locator('.ip-result-hero .ip-actions button').nth(1)).toBeDisabled(); await expect(page.locator('.ip-result-hero .ip-actions .btn-primary')).toBeDisabled(); }); test('topic and script edits persist userEdited markers and audit trail', async ({ page }) => { await seedScenario(page, 'ready'); await page.goto('/'); await page.locator('.ip-result-tabs button').nth(1).click(); const firstTopicTitle = page.locator('.ip-topic-card input').first(); await firstTopicTitle.fill('Edited owner IP topic title'); await firstTopicTitle.press('Tab'); await expect.poll(async () => page.evaluate(() => { const planId = 'ip_plan_e2e_ready'; const raw = localStorage.getItem(`tiktok.ipOperator.plan.${planId}.local-guest`); const plan = raw ? JSON.parse(raw) : null; return plan?.topics?.some((topic: { title: string; userEdited?: boolean }) => ( topic.title === 'Edited owner IP topic title' && topic.userEdited === true )); })).toBe(true); await expect(page.locator('.ip-topic-card').first()).toContainText('已手动编辑'); await page.locator('.ip-result-tabs button').nth(2).click(); const firstScriptHook = page.locator('.ip-script-detail input').first(); await firstScriptHook.fill('Edited script hook for browser validation'); await firstScriptHook.press('Tab'); await expect.poll(async () => page.evaluate(() => { const planId = 'ip_plan_e2e_ready'; const raw = localStorage.getItem(`tiktok.ipOperator.plan.${planId}.local-guest`); const plan = raw ? JSON.parse(raw) : null; return plan?.scripts?.some((script: { hook: string; userEdited?: boolean }) => ( script.hook === 'Edited script hook for browser validation' && script.userEdited === true )); })).toBe(true); const auditCount = await page.evaluate(() => { const raw = localStorage.getItem('tiktok.ipOperator.auditTrail.local-guest') || '[]'; return JSON.parse(raw).length; }); expect(auditCount).toBeGreaterThanOrEqual(3); }); test('failed plan still renders diagnostics instead of a blank page', async ({ page }) => { await seedScenario(page, 'failed'); await page.goto('/'); await expect(page.locator('.ip-result')).toBeVisible(); await expect(page.locator('.ip-summary-card').filter({ hasText: '生成失败' })).toBeVisible(); await page.locator('.ip-debug-panel details summary').click(); await expect(page.locator('.ip-debug-panel')).toContainText('llm_malformed_json'); await expect(page.locator('.ip-debug-panel')).toContainText('Malformed JSON in diagnosis stage'); }); test('ready plan syncs high-priority topics and enters topic-to-video', async ({ page }) => { await seedScenario(page, 'ready'); await page.goto('/'); await expect(page.locator('.ip-operator-page h1')).toContainText('IP操盘工作台'); await expect(page.locator('.ip-readiness-panel')).toContainText('可进入生产链路'); await expect(page.locator('.ip-summary-card').filter({ hasText: '88/100' })).toBeVisible(); await page.locator('.ip-result-hero .ip-actions .btn-primary').click(); await expect.poll(async () => { return page.evaluate(() => { const items = JSON.parse(localStorage.getItem('videoWorkflow.topicPool.items') || '[]'); return items.filter((item: { sourceType?: string }) => item.sourceType === 'ip_operator').length; }); }).toBe(4); await page.locator('.ip-result-hero .ip-actions button').nth(1).click(); await expect(page.locator('.topic-list')).toBeVisible(); await expect(page.getByText('Why owner IP should not look like an ad account')).toBeVisible(); await page.locator('.topic-pool-toolbar select').first().selectOption('ip_operator'); await expect(page.getByText('Why owner IP should not look like an ad account')).toBeVisible(); await expect(page.locator('.topic-source-card')).toHaveCount(4); await page.locator('.topic-card-actions .btn-primary').first().click(); await expect(page.locator('.t2v-page')).toBeVisible(); await expect(page.locator('.t2v-brief-card')).toBeVisible(); const topicValue = await page.locator('textarea.dh-textarea').first().inputValue(); expect(topicValue).toMatch(/Why owner IP should not look like an ad account|Owner IP trust topic [2-4]/); });