import { expect, Page, test } from '@playwright/test'; import path from 'node:path'; declare global { interface Window { __seedIpOperatorScenario?: (scenario: 'ready') => unknown; } } const scenarioSeedPath = path.resolve(__dirname, '../scripts/validation/ip-operator-scenario-seed.js'); async function seedEvidenceLoop(page: Page) { await page.addInitScript({ path: scenarioSeedPath }); await page.addInitScript(() => { window.__seedIpOperatorScenario?.('ready'); const scope = 'local-guest'; const planId = 'ip_plan_e2e_ready'; const accountId = 'ip_account_e2e_ready'; const now = new Date().toISOString(); const ownedWorkId = 'evidence_owned_work'; const commentId = 'evidence_owned_comment'; const benchmarkId = 'evidence_benchmark_work'; const painId = 'pain_evidence_loop'; const planKey = `tiktok.ipOperator.plan.${planId}.${scope}`; const plan = JSON.parse(localStorage.getItem(planKey) || '{}'); plan.evidenceItems = [ { id: ownedWorkId, platform: 'douyin', kind: 'owned_post', bucket: 'accounts', accountId, sourceRole: 'owned', title: '自有账号作品:老板 IP 如何避免成为广告号', rawText: '先讲客户判断,再讲产品。', summary: '播放 50000,评论 120,收藏 380。', tags: ['自有账号', '账号作品'], relatedTopicIds: [], relatedScriptIds: [], sourceId: 'work-owned-1', evidenceRefs: ['https://www.douyin.com/video/7304943423281171749'], selectedForGeneration: true, confidence: 'high', createdAt: now, updatedAt: now, }, { id: commentId, platform: 'douyin', kind: 'comment', bucket: 'topics', accountId, sourceRole: 'owned', title: '自有账号评论:老板 IP 如何避免成为广告号', rawText: '传统行业老板怎么讲案例才不像广告?', summary: '传统行业老板怎么讲案例才不像广告?', tags: ['自有账号', '评论痛点'], relatedTopicIds: [], relatedScriptIds: [], sourceId: 'comment-owned-1', evidenceRefs: [], selectedForGeneration: true, confidence: 'high', createdAt: now, updatedAt: now, }, { id: benchmarkId, platform: 'douyin', kind: 'competitor_post', bucket: 'accounts', sourceRole: 'benchmark', title: '对标作品:先讲判断标准再讲方法', rawText: '反常识开头,案例证明。', summary: '可借鉴问题钩子和案例结构。', tags: ['对标账号'], relatedTopicIds: [], relatedScriptIds: [], sourceId: 'benchmark-work-1', evidenceRefs: [], selectedForGeneration: true, confidence: 'medium', createdAt: now, updatedAt: now, }, ]; plan.commentPainInsights = [{ id: painId, title: '用户追问:怎么讲案例', category: 'question', severity: 'high', summary: '评论集中追问老板如何讲案例而不显得像广告。', commentCount: 8, keywordFrequency: [{ keyword: '怎么', count: 8 }], representativeComments: ['传统行业老板怎么讲案例才不像广告?'], evidenceItemIds: [commentId], suggestedTopics: ['案例表达三步法'], scriptAngles: ['用评论原话开头'], riskBoundaries: ['不承诺结果'], createdAt: now, updatedAt: now, }]; plan.accountStrategyReports = [{ id: 'strategy_evidence_loop', accountId, snapshotId: 'ip_snapshot_e2e_ready', positioningProposalId: 'ip_positioning_proposal_e2e_ready', sourceMode: 'llm', sourceLabel: 'LLM 证据诊断', diagnosisSummary: {}, evidenceRefs: [], directions: [], taskSuggestions: [], createdAt: now, updatedAt: now, }]; plan.topics = (plan.topics || []).map((topic: Record, index: number) => ({ ...topic, evidenceRefs: index === 0 ? [ownedWorkId, commentId] : [ownedWorkId], })); localStorage.setItem(planKey, JSON.stringify(plan)); const directionIndexKey = `tiktok.ipOperator.contentDirections.${scope}`; for (const directionId of JSON.parse(localStorage.getItem(directionIndexKey) || '[]')) { const key = `tiktok.ipOperator.contentDirection.${directionId}.${scope}`; const direction = JSON.parse(localStorage.getItem(key) || '{}'); direction.sourceEvidenceIds = [ownedWorkId, benchmarkId]; direction.commentPainRefs = [painId]; localStorage.setItem(key, JSON.stringify(direction)); } const taskIndexKey = `tiktok.ipOperator.operationTasks.${scope}`; for (const taskId of JSON.parse(localStorage.getItem(taskIndexKey) || '[]')) { const key = `tiktok.ipOperator.operationTask.${taskId}.${scope}`; const task = JSON.parse(localStorage.getItem(key) || '{}'); task.relatedEvidenceIds = [ownedWorkId, commentId]; localStorage.setItem(key, JSON.stringify(task)); } const proposalKey = `tiktok.ipOperator.positioningProposal.ip_positioning_proposal_e2e_ready.${scope}`; const proposal = JSON.parse(localStorage.getItem(proposalKey) || '{}'); proposal.evidenceItemIds = [ownedWorkId, commentId]; localStorage.setItem(proposalKey, JSON.stringify(proposal)); localStorage.setItem('tiktok.currentTab', 'ip-operator'); }); } test('IP operator shows traceable evidence drawer for direction conclusions', async ({ page }) => { await seedEvidenceLoop(page); await page.goto('/'); await expect(page.getByRole('heading', { name: 'IP操盘工作台' })).toBeVisible(); await page.getByRole('button', { name: /方向选题/ }).click(); await page.getByRole('button', { name: '查看方向证据' }).first().click(); const drawer = page.getByRole('complementary', { name: '证据来源' }); await expect(drawer).toBeVisible(); await expect(drawer.getByText('自有账号作品').first()).toBeVisible(); await expect(drawer.getByText('评论痛点').first()).toBeVisible(); await expect(drawer.getByText('对标作品').first()).toBeVisible(); });