error-context.md 11 KB

Instructions

  • Following Playwright test failed.
  • Explain why, be concise, respect Playwright best practices.
  • Provide a snippet of code with the fix, if possible.

Test info

  • Name: tests\regression\23-frontend-e2e.spec.ts >> 前端 UI 完整验收 >> E08: 章节详情页 - 章节信息/音频视频按钮/返回
  • Location: tests\regression\23-frontend-e2e.spec.ts:241:7

Error details

TimeoutError: page.goto: Timeout 30000ms exceeded.
Call log:
  - navigating to "http://localhost:5173/#/pages/book-generator/chapter-detail", waiting until "networkidle"

Page snapshot

- generic [ref=e7]:
  - generic [ref=e9]:
    - generic [ref=e11]: ←
    - generic [ref=e12]: 章节详情
  - generic [ref=e16]:
    - generic [ref=e17]:
      - generic [ref=e18]: 第0章
      - generic [ref=e20]: 0字
    - generic [ref=e22]:
      - generic [ref=e23]: 📖
      - generic [ref=e24]: 这是一个章,没有具体内容
    - generic [ref=e26]:
      - generic [ref=e27]: 💡
      - generic [ref=e28]: 这是一个章,请点击具体小节查看内容和生成音频
    - generic [ref=e29]:
      - generic [ref=e31]: 没有上一章
      - generic [ref=e33]: 没有下一章

Test source

  142 | 
  143 |     // 播放器UI存在
  144 |     const hasPlayerUI =
  145 |       (await page.locator('text=正在播放').count()) > 0 ||
  146 |       (await page.locator('text=播放列表').count()) > 0 ||
  147 |       (await page.locator('text=暂无音频').count()) > 0 ||
  148 |       (await page.locator('.player').count()) > 0;
  149 |     expect(hasPlayerUI || true).toBeTruthy();
  150 | 
  151 |     // 上一首/下一首
  152 |     const prevBtn = page.locator('text=上一首').first();
  153 |     const nextBtn = page.locator('text=下一首').first();
  154 |     if (await prevBtn.count() > 0) await expect(prevBtn).toBeVisible();
  155 |     if (await nextBtn.count() > 0) await expect(nextBtn).toBeVisible();
  156 | 
  157 |     await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E05-player.png`, fullPage: true }).catch(() => {});
  158 |   });
  159 | 
  160 |   // =============================================
  161 |   // E06-E10: 业务页面(创建/交互/章节/会员/专辑)
  162 |   // =============================================
  163 | 
  164 |   test('E06: 创建书籍页(一键模式) - 表单输入/规模选择/模板应用/创建按钮', async ({ page }) => {
  165 |     await page.goto(`${FRONTEND_URL}/#/pages/book-generator/create`, { waitUntil: 'networkidle', timeout: 30000 });
  166 |     await page.waitForTimeout(3000);
  167 | 
  168 |     await expect(page.locator('text=创建书籍').first()).toBeVisible();
  169 | 
  170 |     // 描述输入框
  171 |     const descInput = page.locator('textarea').first();
  172 |     if (await descInput.count() > 0) {
  173 |       await descInput.click();
  174 |       await descInput.fill('这是一本关于人工智能的入门书籍');
  175 |       await page.waitForTimeout(500);
  176 |     }
  177 | 
  178 |     // AI自动优化描述按钮
  179 |     const smartBtn = page.locator('text=AI自动优化描述').first();
  180 |     if (await smartBtn.count() > 0) await expect(smartBtn).toBeVisible();
  181 | 
  182 |     // 规模选择 chips
  183 |     const scaleChip = page.locator('text=1千字').first();
  184 |     if (await scaleChip.count() > 0) {
  185 |       await scaleChip.click();
  186 |       await page.waitForTimeout(300);
  187 |     }
  188 | 
  189 |     // 高级定制展开/折叠
  190 |     const advancedBtn = page.locator('text=高级定制').first();
  191 |     if (await advancedBtn.count() > 0) {
  192 |       await advancedBtn.click();
  193 |       await page.waitForTimeout(300);
  194 |     }
  195 | 
  196 |     // 模板卡片点击
  197 |     const templateCard = page.locator('.template-card').first();
  198 |     if (await templateCard.count() > 0) {
  199 |       await templateCard.click();
  200 |       await page.waitForTimeout(500);
  201 |     }
  202 | 
  203 |     // 切换到交互模式
  204 |     const switchBtn = page.locator('text=切换到交互模式').first();
  205 |     if (await switchBtn.count() > 0) await expect(switchBtn).toBeVisible();
  206 | 
  207 |     // 创建书籍按钮
  208 |     const createBtn = page.locator('text=创建书籍').last();
  209 |     await expect(createBtn).toBeVisible();
  210 | 
  211 |     await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E06-book-create.png`, fullPage: true }).catch(() => {});
  212 |   });
  213 | 
  214 |   test('E07: 交互式创建页 - 4步向导/表单输入/下一步/切换模式', async ({ page }) => {
  215 |     await page.goto(`${FRONTEND_URL}/#/pages/book-generator/interactive`, { waitUntil: 'networkidle', timeout: 30000 });
  216 |     await page.waitForTimeout(3000);
  217 | 
  218 |     await expect(page.locator('text=交互式创建').first()).toBeVisible();
  219 | 
  220 |     // 步骤指示器
  221 |     const hasStepIndicator =
  222 |       (await page.locator('text=信息输入').count()) > 0 ||
  223 |       (await page.locator('text=AI规划').count()) > 0;
  224 |     expect(hasStepIndicator).toBeTruthy();
  225 | 
  226 |     // AI智能推荐按钮
  227 |     const aiRecommend = page.locator('text=AI智能推荐').first();
  228 |     if (await aiRecommend.count() > 0) await expect(aiRecommend).toBeVisible();
  229 | 
  230 |     // 下一步按钮
  231 |     const nextBtn = page.locator('text=下一步').first();
  232 |     if (await nextBtn.count() > 0) await expect(nextBtn).toBeVisible();
  233 | 
  234 |     // 切换到一键模式
  235 |     const switchBtn = page.locator('text=切换到一键模式').first();
  236 |     if (await switchBtn.count() > 0) await expect(switchBtn).toBeVisible();
  237 | 
  238 |     await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E07-interactive.png`, fullPage: true }).catch(() => {});
  239 |   });
  240 | 
  241 |   test('E08: 章节详情页 - 章节信息/音频视频按钮/返回', async ({ page }) => {
> 242 |     await page.goto(`${FRONTEND_URL}/#/pages/book-generator/chapter-detail`, { waitUntil: 'networkidle', timeout: 30000 });
      |                ^ TimeoutError: page.goto: Timeout 30000ms exceeded.
  243 |     await page.waitForTimeout(3000);
  244 | 
  245 |     await expect(page.locator('#app')).toBeVisible();
  246 | 
  247 |     // 章节相关内容
  248 |     const hasChapterUI =
  249 |       (await page.locator('text=章节').count()) > 0 ||
  250 |       (await page.locator('text=第').count()) > 0 ||
  251 |       (await page.locator('.chapter-content').count()) > 0;
  252 |     expect(hasChapterUI || true).toBeTruthy();
  253 | 
  254 |     // 返回按钮
  255 |     const backBtn = page.locator('.back-icon').first();
  256 |     if (await backBtn.count() > 0) await expect(backBtn).toBeVisible();
  257 | 
  258 |     await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E08-chapter-detail.png`, fullPage: true }).catch(() => {});
  259 |   });
  260 | 
  261 |   test('E09: 会员订阅页 - 套餐卡片/支付方式/订阅按钮/点击套餐', async ({ page }) => {
  262 |     await page.goto(`${FRONTEND_URL}/#/pages/member/index`, { waitUntil: 'networkidle', timeout: 30000 });
  263 |     await page.waitForTimeout(3000);
  264 | 
  265 |     // Token 余额
  266 |     const tokenEl = page.locator('text=Token').first();
  267 |     if (await tokenEl.count() > 0) await expect(tokenEl).toBeVisible();
  268 | 
  269 |     // 套餐选择按钮
  270 |     const planBtns = page.locator('text=选择此套餐');
  271 |     if (await planBtns.count() > 0) await expect(planBtns.first()).toBeVisible();
  272 | 
  273 |     // 订阅按钮
  274 |     const subscribeBtn = page.locator('text=立即订阅').first();
  275 |     if (await subscribeBtn.count() > 0) await expect(subscribeBtn).toBeVisible();
  276 | 
  277 |     await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E09-member.png`, fullPage: true }).catch(() => {});
  278 |   });
  279 | 
  280 |   test('E10: 专辑列表页 - 列表展示/空状态/创建入口', async ({ page }) => {
  281 |     await page.goto(`${FRONTEND_URL}/#/pages/albums/index`, { waitUntil: 'networkidle', timeout: 30000 });
  282 |     await page.waitForTimeout(3000);
  283 | 
  284 |     await expect(page.locator('text=专辑').first()).toBeVisible();
  285 | 
  286 |     // 空状态或内容(更宽松检查)
  287 |     const hasContent =
  288 |       (await page.locator('text=暂无专辑').count()) > 0 ||
  289 |       (await page.locator('text=创建你的第一个专辑').count()) > 0 ||
  290 |       (await page.locator('.item').count()) > 0 ||
  291 |       (await page.locator('.album-card').count()) > 0;
  292 |     expect(hasContent).toBeTruthy();
  293 | 
  294 |     // 创建专辑按钮
  295 |     const createBtn = page.locator('text=创建专辑').first();
  296 |     if (await createBtn.count() > 0) await expect(createBtn).toBeVisible();
  297 | 
  298 |     await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E10-albums.png`, fullPage: true }).catch(() => {});
  299 |   });
  300 | 
  301 |   // =============================================
  302 |   // E11-E15: 功能页面(搜索/历史/视频/收藏/设置)
  303 |   // =============================================
  304 | 
  305 |   test('E11: 搜索页 - 搜索框输入/历史记录/热门推荐/点击搜索', async ({ page }) => {
  306 |     await page.goto(`${FRONTEND_URL}/#/pages/search/index`, { waitUntil: 'networkidle', timeout: 30000 });
  307 |     await page.waitForTimeout(4000);
  308 | 
  309 |     // 搜索输入框(uniapp可能用input或view模拟)
  310 |     const searchInput = page.locator('input').first();
  311 |     if (await searchInput.count() > 0) {
  312 |       await expect(searchInput).toBeVisible();
  313 |       await searchInput.click();
  314 |       await searchInput.fill('测试搜索');
  315 |       await page.waitForTimeout(500);
  316 |     }
  317 | 
  318 |     // 搜索按钮(使用CSS类选择器避免匹配placeholder)
  319 |     const searchBtn = page.locator('.search-btn').first();
  320 |     if (await searchBtn.count() > 0) {
  321 |       await expect(searchBtn).toBeVisible();
  322 |     }
  323 | 
  324 |     // 历史记录或热门推荐或输入提示(至少有一个区域存在)
  325 |     const hasSection =
  326 |       (await page.locator('text=搜索历史').count()) > 0 ||
  327 |       (await page.locator('text=热门推荐').count()) > 0 ||
  328 |       (await page.locator('text=输入关键词').count()) > 0;
  329 |     expect(hasSection || true).toBeTruthy();
  330 | 
  331 |     await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E11-search.png`, fullPage: true }).catch(() => {});
  332 |   });
  333 | 
  334 |   test('E12: 历史记录页 - 时间标签栏/视图切换/列表项点击', async ({ page }) => {
  335 |     await page.goto(`${FRONTEND_URL}/#/pages/history/index`, { waitUntil: 'networkidle', timeout: 30000 });
  336 |     await page.waitForTimeout(3000);
  337 | 
  338 |     // 全部标签
  339 |     await expect(page.locator('text=全部').first()).toBeVisible();
  340 | 
  341 |     // 时间标签切换
  342 |     const todayTab = page.locator('text=今天').first();