# 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 ```yaml - 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 ```ts 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(); ```