ip-operator-p2-core.spec.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import { expect, Page, test } from '@playwright/test';
  2. import path from 'node:path';
  3. declare global {
  4. interface Window {
  5. __seedIpOperatorScenario?: (scenario: 'ready') => unknown;
  6. }
  7. }
  8. const scenarioSeedPath = path.resolve(__dirname, '../scripts/validation/ip-operator-scenario-seed.js');
  9. const planKey = 'tiktok.ipOperator.plan.ip_plan_e2e_ready.local-guest';
  10. const viralAnalysisKey = 'videoWorkflow.viralAnalyses.items';
  11. async function seedP2Scenario(page: Page) {
  12. await page.addInitScript({ path: scenarioSeedPath });
  13. await page.addInitScript(([nextPlanKey, nextViralAnalysisKey]) => {
  14. const now = new Date().toISOString();
  15. window.__seedIpOperatorScenario?.('ready');
  16. localStorage.setItem('tiktok.currentTab', 'ip-operator');
  17. const raw = localStorage.getItem(nextPlanKey);
  18. if (!raw) return;
  19. const plan = JSON.parse(raw);
  20. plan.benchmarkEvidence = [{
  21. id: 'ip_evidence_e2e_existing',
  22. planId: plan.id,
  23. profileId: plan.profileId,
  24. benchmarkId: 'benchmark_e2e_1',
  25. source: 'viral_analysis',
  26. sourceId: 'analysis_e2e_existing',
  27. awemeId: '7123456789012345678',
  28. inputUrl: 'https://www.douyin.com/video/7123456789012345678',
  29. title: 'E2E benchmark comment evidence',
  30. authorName: 'Benchmark Owner Account',
  31. summary: 'Users reacted to the account because it named the trust problem before selling.',
  32. hookPattern: 'Question hook + hard contrast',
  33. reusableFrame: 'Name the hidden trust problem, then replace the case with this IP assets.',
  34. commentPainPoints: ['Looks like an ad account', 'No real case proof', 'Unsure whether the owner understands my problem'],
  35. migrationSuggestion: 'Use the comment pain to build a trust-diagnosis opportunity, then rewrite topics and scripts around one real owner scenario.',
  36. nonCopyableRisk: 'Do not copy the benchmark title, exact wording, claimed result, or industry-specific case.',
  37. evidenceRefs: ['comment: Looks like an ad account', 'comment: No real case proof'],
  38. confidence: 'high',
  39. createdAt: now,
  40. }];
  41. plan.evidenceFetchJobs = [];
  42. plan.evidenceSuggestions = [];
  43. localStorage.setItem(nextPlanKey, JSON.stringify(plan));
  44. localStorage.setItem(nextViralAnalysisKey, JSON.stringify([{
  45. id: 'analysis_e2e_saved',
  46. userId: 'local-guest',
  47. awemeId: '7987654321098765432',
  48. source: 'manual',
  49. videoSnapshot: {
  50. awemeId: '7987654321098765432',
  51. desc: 'E2E saved viral analysis',
  52. authorName: 'Saved Benchmark',
  53. diggCount: 12000,
  54. commentCount: 800,
  55. shareCount: 450,
  56. },
  57. commentsSnapshot: [
  58. { id: 'c1', text: 'This feels useful because it does not sell first.', likeCount: 99 },
  59. { id: 'c2', text: 'I want to see a real before-after case.', likeCount: 80 },
  60. ],
  61. confidence: 'medium',
  62. analysis: {
  63. summary: 'Saved analysis highlights trust-first owner content.',
  64. hookType: 'pain question',
  65. openingPattern: 'Ask why users distrust owner accounts',
  66. contentRhythm: 'hook-proof-action',
  67. conflict: 'selling first vs earning trust first',
  68. proofPoint: 'real case detail',
  69. audienceEmotion: 'skeptical but interested',
  70. commentTrigger: 'asks for before-after examples',
  71. reusableFrame: 'replace the case with this IP own client story',
  72. reusableAngles: ['trust diagnosis', 'case proof'],
  73. riskNotes: ['avoid copying exact claim'],
  74. evidenceRefs: ['c1', 'c2'],
  75. },
  76. savedTopicIds: [],
  77. createdAt: now,
  78. updatedAt: now,
  79. }]));
  80. }, [planKey, viralAnalysisKey]);
  81. }
  82. test('P2 core links benchmark evidence back to opportunities, topics, scripts, and production', async ({ page }) => {
  83. await seedP2Scenario(page);
  84. await page.goto('/');
  85. await expect(page.locator('.ip-operator-page')).toBeVisible();
  86. await expect(page.locator('.ip-quality-audit')).toBeVisible();
  87. await expect(page.locator('.ip-evidence-panel')).toBeVisible();
  88. await expect(page.getByText('E2E benchmark comment evidence')).toBeVisible();
  89. await page.locator('.ip-evidence-grid section').first().locator('article button').first().click();
  90. await expect(page.locator('.ip-evidence-suggestions article')).toHaveCount(3);
  91. await page.locator('.ip-evidence-suggestions article .btn-primary').first().click();
  92. await expect.poll(async () => page.evaluate((nextPlanKey) => {
  93. const plan = JSON.parse(localStorage.getItem(nextPlanKey) || '{}');
  94. return Boolean(
  95. plan.evidenceSuggestions?.some((item: { status?: string }) => item.status === 'applied')
  96. && plan.opportunityMap?.some((item: { type?: string; reason?: string }) => (
  97. item.type === '评论痛点迁移' && String(item.reason || '').includes('Looks like an ad account')
  98. ))
  99. );
  100. }, planKey)).toBe(true);
  101. await page.locator('.ip-evidence-form').first().locator('input').fill('https://v.douyin.com/short-only/');
  102. await page.locator('.ip-evidence-form').first().locator('button').click();
  103. await expect.poll(async () => page.evaluate((nextPlanKey) => {
  104. const plan = JSON.parse(localStorage.getItem(nextPlanKey) || '{}');
  105. return plan.evidenceFetchJobs?.some((job: { status?: string; failureCode?: string }) => (
  106. job.status === 'failed' && job.failureCode === 'invalid_url'
  107. ));
  108. }, planKey)).toBe(true);
  109. const savedForm = page.locator('.ip-evidence-form--saved');
  110. await savedForm.locator('button').first().click();
  111. await savedForm.locator('select').selectOption('analysis_e2e_saved');
  112. await savedForm.locator('button').nth(1).click();
  113. await expect.poll(async () => page.evaluate((nextPlanKey) => {
  114. const plan = JSON.parse(localStorage.getItem(nextPlanKey) || '{}');
  115. return Boolean(
  116. plan.benchmarkEvidence?.some((item: { sourceId?: string }) => item.sourceId === 'analysis_e2e_saved')
  117. && plan.evidenceFetchJobs?.some((job: { analysisId?: string; status?: string }) => (
  118. job.analysisId === 'analysis_e2e_saved' && job.status === 'completed'
  119. ))
  120. );
  121. }, planKey)).toBe(true);
  122. await page.locator('.ip-result-tabs button').nth(1).click();
  123. await expect(page.locator('.ip-topic-card').first()).toBeVisible();
  124. await page.locator('.ip-result-tabs button').nth(2).click();
  125. await expect(page.locator('.ip-script-detail').first()).toBeVisible();
  126. await page.locator('.ip-script-detail').first().locator('.btn-primary').click();
  127. await expect(page.locator('.dh-page')).toBeVisible();
  128. await expect(page.locator('textarea.dh-textarea').first()).not.toHaveValue('');
  129. });