ip-operator-workbench.spec.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import { expect, Page, test } from '@playwright/test';
  2. import path from 'node:path';
  3. declare global {
  4. interface Window {
  5. __seedIpOperatorScenario?: (scenario: 'empty' | 'ready' | 'needs_input' | 'failed') => unknown;
  6. }
  7. }
  8. const scenarioSeedPath = path.resolve(__dirname, '../scripts/validation/ip-operator-scenario-seed.js');
  9. async function seedScenario(page: Page, scenario: 'empty' | 'ready' | 'needs_input' | 'failed', startTab: 'home' | 'ip-operator' = 'ip-operator') {
  10. await page.addInitScript({ path: scenarioSeedPath });
  11. await page.addInitScript(([nextScenario, nextTab]) => {
  12. window.__seedIpOperatorScenario?.(nextScenario);
  13. localStorage.setItem('tiktok.currentTab', nextTab);
  14. }, [scenario, startTab]);
  15. }
  16. test('sidebar entry and wizard form reach generation confirmation', async ({ page }) => {
  17. await seedScenario(page, 'empty', 'home');
  18. await page.goto('/');
  19. await page.getByRole('button', { name: /IP操盘/ }).click();
  20. await expect(page.locator('.ip-operator-page')).toBeVisible();
  21. await expect(page.locator('.ip-card h2')).toContainText('创建 IP 档案');
  22. const profileCard = page.locator('.ip-card').first();
  23. await profileCard.locator('input').nth(0).fill('E2E New Profile');
  24. await profileCard.locator('input').nth(1).fill('Local growth consultant');
  25. await profileCard.locator('input').nth(2).fill('Local service');
  26. await profileCard.locator('input').nth(3).fill('Traditional business owners');
  27. await profileCard.locator('input').nth(4).fill('IP diagnosis and content operation');
  28. await profileCard.locator('textarea').nth(0).fill('Content looks like advertising');
  29. await profileCard.locator('textarea').nth(1).fill('Served local stores with short-video conversion');
  30. await profileCard.locator('textarea').nth(2).fill('Positioning, topics, scripts');
  31. await profileCard.locator('textarea').nth(5).fill('Generate a usable 7-day test plan');
  32. await profileCard.locator('.btn-primary').click();
  33. const questionnaireCard = page.locator('.ip-card').first();
  34. await expect(questionnaireCard.locator('h2')).toContainText('轻问卷');
  35. await questionnaireCard.locator('textarea').nth(0).fill('They want trust but dislike hard selling.');
  36. await questionnaireCard.locator('textarea').nth(1).fill('I can turn cases into practical content.');
  37. await questionnaireCard.locator('textarea').nth(2).fill('I have real local-service experience.');
  38. await questionnaireCard.locator('.btn-primary').click();
  39. const benchmarkCard = page.locator('.ip-card').first();
  40. await expect(benchmarkCard.locator('h2')).toContainText('对标账号');
  41. await benchmarkCard.locator('input').nth(0).fill('Benchmark Owner Account');
  42. await benchmarkCard.locator('input').nth(2).fill('Why owner content fails to convert');
  43. await benchmarkCard.locator('textarea').nth(1).fill('It uses direct pain-point diagnosis.');
  44. await benchmarkCard.locator('.btn-primary').click();
  45. const generateCard = page.locator('.ip-card').first();
  46. await expect(generateCard.locator('h2')).toContainText('确认生成范围');
  47. await expect(generateCard.locator('.ip-generate-scope span')).toHaveCount(9);
  48. await expect(generateCard.locator('.btn-primary')).toBeEnabled();
  49. });
  50. test('needs_input plan blocks production entry points in the browser', async ({ page }) => {
  51. await seedScenario(page, 'needs_input');
  52. await page.goto('/');
  53. await expect(page.locator('.ip-result')).toBeVisible();
  54. await expect(page.locator('.ip-readiness-panel.is-blocked')).toBeVisible();
  55. await expect(page.locator('.ip-summary-card').filter({ hasText: '64/100' })).toBeVisible();
  56. await expect(page.locator('.ip-result-hero .ip-actions button').nth(1)).toBeDisabled();
  57. await expect(page.locator('.ip-result-hero .ip-actions .btn-primary')).toBeDisabled();
  58. });
  59. test('topic and script edits persist userEdited markers and audit trail', async ({ page }) => {
  60. await seedScenario(page, 'ready');
  61. await page.goto('/');
  62. await page.locator('.ip-result-tabs button').nth(1).click();
  63. const firstTopicTitle = page.locator('.ip-topic-card input').first();
  64. await firstTopicTitle.fill('Edited owner IP topic title');
  65. await firstTopicTitle.press('Tab');
  66. await expect.poll(async () => page.evaluate(() => {
  67. const planId = 'ip_plan_e2e_ready';
  68. const raw = localStorage.getItem(`tiktok.ipOperator.plan.${planId}.local-guest`);
  69. const plan = raw ? JSON.parse(raw) : null;
  70. return plan?.topics?.some((topic: { title: string; userEdited?: boolean }) => (
  71. topic.title === 'Edited owner IP topic title' && topic.userEdited === true
  72. ));
  73. })).toBe(true);
  74. await expect(page.locator('.ip-topic-card').first()).toContainText('已手动编辑');
  75. await page.locator('.ip-result-tabs button').nth(2).click();
  76. const firstScriptHook = page.locator('.ip-script-detail input').first();
  77. await firstScriptHook.fill('Edited script hook for browser validation');
  78. await firstScriptHook.press('Tab');
  79. await expect.poll(async () => page.evaluate(() => {
  80. const planId = 'ip_plan_e2e_ready';
  81. const raw = localStorage.getItem(`tiktok.ipOperator.plan.${planId}.local-guest`);
  82. const plan = raw ? JSON.parse(raw) : null;
  83. return plan?.scripts?.some((script: { hook: string; userEdited?: boolean }) => (
  84. script.hook === 'Edited script hook for browser validation' && script.userEdited === true
  85. ));
  86. })).toBe(true);
  87. const auditCount = await page.evaluate(() => {
  88. const raw = localStorage.getItem('tiktok.ipOperator.auditTrail.local-guest') || '[]';
  89. return JSON.parse(raw).length;
  90. });
  91. expect(auditCount).toBeGreaterThanOrEqual(3);
  92. });
  93. test('failed plan still renders diagnostics instead of a blank page', async ({ page }) => {
  94. await seedScenario(page, 'failed');
  95. await page.goto('/');
  96. await expect(page.locator('.ip-result')).toBeVisible();
  97. await expect(page.locator('.ip-summary-card').filter({ hasText: '生成失败' })).toBeVisible();
  98. await page.locator('.ip-debug-panel details summary').click();
  99. await expect(page.locator('.ip-debug-panel')).toContainText('llm_malformed_json');
  100. await expect(page.locator('.ip-debug-panel')).toContainText('Malformed JSON in diagnosis stage');
  101. });
  102. test('ready plan syncs high-priority topics and enters topic-to-video', async ({ page }) => {
  103. await seedScenario(page, 'ready');
  104. await page.goto('/');
  105. await expect(page.locator('.ip-operator-page h1')).toContainText('IP操盘工作台');
  106. await expect(page.locator('.ip-readiness-panel')).toContainText('可进入生产链路');
  107. await expect(page.locator('.ip-summary-card').filter({ hasText: '88/100' })).toBeVisible();
  108. await page.locator('.ip-result-hero .ip-actions .btn-primary').click();
  109. await expect.poll(async () => {
  110. return page.evaluate(() => {
  111. const items = JSON.parse(localStorage.getItem('videoWorkflow.topicPool.items') || '[]');
  112. return items.filter((item: { sourceType?: string }) => item.sourceType === 'ip_operator').length;
  113. });
  114. }).toBe(4);
  115. await page.locator('.ip-result-hero .ip-actions button').nth(1).click();
  116. await expect(page.locator('.topic-list')).toBeVisible();
  117. await expect(page.getByText('Why owner IP should not look like an ad account')).toBeVisible();
  118. await page.locator('.topic-pool-toolbar select').first().selectOption('ip_operator');
  119. await expect(page.getByText('Why owner IP should not look like an ad account')).toBeVisible();
  120. await expect(page.locator('.topic-source-card')).toHaveCount(4);
  121. await page.locator('.topic-card-actions .btn-primary').first().click();
  122. await expect(page.locator('.t2v-page')).toBeVisible();
  123. await expect(page.locator('.t2v-brief-card')).toBeVisible();
  124. const topicValue = await page.locator('textarea.dh-textarea').first().inputValue();
  125. expect(topicValue).toMatch(/Why owner IP should not look like an ad account|Owner IP trust topic [2-4]/);
  126. });