# 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: regression\23-frontend-e2e.spec.ts >> 前端 UI 完整验收 >> E03: 书籍生成列表页 - 空状态/创建按钮/点击创建 - Location: regression\23-frontend-e2e.spec.ts:95:7 # Error details ``` Error: expect(locator).toBeVisible() failed Locator: locator('text=书籍生成').first() Expected: visible Timeout: 15000ms Error: element(s) not found Call log: - Expect "toBeVisible" with timeout 15000ms - waiting for locator('text=书籍生成').first() ``` # Test source ```ts 1 | /** 2 | * L2 回归测试:前端 UI 功能验收 - 完整版 3 | * 4 | * 测试范围: 5 | * 1. 所有页面加载与渲染 6 | * 2. 所有可点击元素的交互 7 | * 3. TabBar 导航切换 8 | * 9 | * 风格:Navigate → Locate → Click/Assert → Screenshot 10 | */ 11 | import { test, expect } from '@playwright/test'; 12 | 13 | const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:5173'; 14 | const SCREENSHOT_DIR = 'test-results/screenshots'; 15 | 16 | test.describe('前端 UI 完整验收', () => { 17 | // ============================================= 18 | // E01-E05: TabBar 主页面(首页/生成/书籍/我的/播放) 19 | // ============================================= 20 | 21 | test('E01: 首页 - 搜索栏/通知铃铛/最近收听/点击搜索', async ({ page }) => { 22 | await page.goto(`${FRONTEND_URL}/#/pages/index/index`, { waitUntil: 'networkidle', timeout: 30000 }); 23 | await page.waitForTimeout(4000); 24 | 25 | // 搜索栏(uniapp用text模拟placeholder) 26 | const searchBar = page.locator('.search-bar').first(); 27 | await expect(searchBar).toBeVisible(); 28 | 29 | // 搜索图标 30 | const searchIcon = page.locator('.search-icon').first(); 31 | if (await searchIcon.count() > 0) await expect(searchIcon).toBeVisible(); 32 | 33 | // 搜索占位文字 34 | const searchPlaceholder = page.locator('.search-placeholder').first(); 35 | if (await searchPlaceholder.count() > 0) await expect(searchPlaceholder).toBeVisible(); 36 | 37 | // 通知铃铛 38 | const bell = page.locator('.bell-icon').first(); 39 | if (await bell.count() > 0) await expect(bell).toBeVisible(); 40 | 41 | // 页面主体存在(骨架屏/内容/空状态至少有一个) 42 | const hasContent = 43 | (await page.locator('.recent-section').count()) > 0 || 44 | (await page.locator('.onboarding').count()) > 0 || 45 | (await page.locator('.empty').count()) > 0 || 46 | (await page.locator('.skeleton-grid').count()) > 0 || 47 | (await page.locator('.album-grid').count()) > 0; 48 | expect(hasContent).toBeTruthy(); 49 | 50 | // 点击搜索栏区域 51 | await searchBar.click(); 52 | await page.waitForTimeout(1000); 53 | 54 | // 验证导航到搜索页 55 | await expect(page.locator('#app')).toBeVisible(); 56 | 57 | await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E01-index.png`, fullPage: true }).catch(() => {}); 58 | }); 59 | 60 | test('E02: 生成音频页 - 文本输入/音色选择/生成按钮/点击交互', async ({ page }) => { 61 | await page.goto(`${FRONTEND_URL}/#/pages/create/index`, { waitUntil: 'networkidle', timeout: 30000 }); 62 | await page.waitForTimeout(3000); 63 | 64 | // 页面标题 65 | await expect(page.locator('text=开始生成').first()).toBeVisible(); 66 | 67 | // 文本输入框(uniapp textarea) 68 | const textarea = page.locator('textarea').first(); 69 | if (await textarea.count() > 0) { 70 | await expect(textarea).toBeVisible(); 71 | await textarea.click(); 72 | await textarea.fill('测试文本'); 73 | await page.waitForTimeout(300); 74 | } 75 | 76 | // AI 生成内容按钮 77 | const aiBtn = page.locator('text=AI 生成内容').first(); 78 | if (await aiBtn.count() > 0) await expect(aiBtn).toBeVisible(); 79 | 80 | // 音色选择区卡片 81 | const voiceSection = page.locator('text=选择音色').first(); 82 | if (await voiceSection.count() > 0) await expect(voiceSection).toBeVisible(); 83 | 84 | // 参数调节区卡片 85 | const paramsSection = page.locator('text=参数调节').first(); 86 | if (await paramsSection.count() > 0) await expect(paramsSection).toBeVisible(); 87 | 88 | // 生成音频按钮 89 | const generateBtn = page.locator('text=开始生成').last(); 90 | await expect(generateBtn).toBeVisible(); 91 | 92 | await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E02-create.png`, fullPage: true }).catch(() => {}); 93 | }); 94 | 95 | test('E03: 书籍生成列表页 - 空状态/创建按钮/点击创建', async ({ page }) => { 96 | await page.goto(`${FRONTEND_URL}/#/pages/book-generator/index`, { waitUntil: 'networkidle', timeout: 30000 }); 97 | await page.waitForTimeout(3000); 98 | > 99 | await expect(page.locator('text=书籍生成').first()).toBeVisible(); | ^ Error: expect(locator).toBeVisible() failed 100 | 101 | // 创建新书籍按钮 102 | const createBtn = page.locator('text=创建新书籍').first(); 103 | if (await createBtn.count() > 0) await expect(createBtn).toBeVisible(); 104 | 105 | // 空状态或列表(更宽松的检查) 106 | const hasContent = 107 | (await page.locator('text=暂无书籍').count()) > 0 || 108 | (await page.locator('text=创建第一本').count()) > 0 || 109 | (await page.locator('.item').count()) > 0 || 110 | (await page.locator('.album-card').count()) > 0 || 111 | (await page.locator('.book-card').count()) > 0; 112 | expect(hasContent).toBeTruthy(); 113 | 114 | await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E03-book-list.png`, fullPage: true }).catch(() => {}); 115 | }); 116 | 117 | test('E04: 我的页面 - 用户卡片/菜单项点击/退出登录', async ({ page }) => { 118 | await page.goto(`${FRONTEND_URL}/#/pages/mine/index`, { waitUntil: 'networkidle', timeout: 30000 }); 119 | await page.waitForTimeout(3000); 120 | 121 | await expect(page.locator('#app')).toBeVisible(); 122 | 123 | // 点击"书籍生成"按钮,验证跳转到书籍生成页 124 | const bookGenBtn = page.locator('text=书籍生成').first(); 125 | if (await bookGenBtn.count() > 0) { 126 | await bookGenBtn.click(); 127 | await page.waitForTimeout(2000); 128 | await expect(page.locator('#app')).toBeVisible(); 129 | const currentUrl = page.url(); 130 | console.log('书籍生成跳转后URL:', currentUrl); 131 | // 验证跳转到书籍生成相关页面 132 | expect(currentUrl.includes('book-generator')).toBeTruthy(); 133 | // 截图 134 | await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E04-bookgenerator.png`, fullPage: true }).catch(() => {}); 135 | } 136 | 137 | // 点击"历史记录"按钮 138 | await page.goto(`${FRONTEND_URL}/#/pages/mine/index`, { waitUntil: 'networkidle', timeout: 30000 }); 139 | await page.waitForTimeout(2000); 140 | const historyBtn = page.locator('text=历史记录').first(); 141 | if (await historyBtn.count() > 0) { 142 | await historyBtn.click(); 143 | await page.waitForTimeout(2000); 144 | await expect(page.locator('#app')).toBeVisible(); 145 | console.log('历史记录跳转后URL:', page.url()); 146 | await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E04-history.png`, fullPage: true }).catch(() => {}); 147 | } 148 | 149 | // 点击"会员中心"按钮 150 | await page.goto(`${FRONTEND_URL}/#/pages/mine/index`, { waitUntil: 'networkidle', timeout: 30000 }); 151 | await page.waitForTimeout(2000); 152 | const memberBtn = page.locator('text=会员中心').first(); 153 | if (await memberBtn.count() > 0) { 154 | await memberBtn.click(); 155 | await page.waitForTimeout(2000); 156 | await expect(page.locator('#app')).toBeVisible(); 157 | console.log('会员中心跳转后URL:', page.url()); 158 | await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E04-member.png`, fullPage: true }).catch(() => {}); 159 | } 160 | 161 | // 点击"设置"按钮 162 | await page.goto(`${FRONTEND_URL}/#/pages/mine/index`, { waitUntil: 'networkidle', timeout: 30000 }); 163 | await page.waitForTimeout(2000); 164 | const settingsBtn = page.locator('text=设置').first(); 165 | if (await settingsBtn.count() > 0) { 166 | await settingsBtn.click(); 167 | await page.waitForTimeout(2000); 168 | await expect(page.locator('#app')).toBeVisible(); 169 | console.log('设置跳转后URL:', page.url()); 170 | await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E04-settings.png`, fullPage: true }).catch(() => {}); 171 | } 172 | 173 | // 退出登录按钮 174 | await page.goto(`${FRONTEND_URL}/#/pages/mine/index`, { waitUntil: 'networkidle', timeout: 30000 }); 175 | await page.waitForTimeout(2000); 176 | const logoutBtn = page.locator('text=退出登录').first(); 177 | if (await logoutBtn.count() > 0) { 178 | await logoutBtn.click(); 179 | await page.waitForTimeout(1000); 180 | console.log('退出登录后URL:', page.url()); 181 | } 182 | 183 | await page.screenshot({ path: `${SCREENSHOT_DIR}/e2e-E04-mine.png`, fullPage: true }).catch(() => {}); 184 | }); 185 | 186 | test('E05: 播放器页面 - 播放控件/播放列表/音频操作', async ({ page }) => { 187 | await page.goto(`${FRONTEND_URL}/#/pages/player/index`, { waitUntil: 'networkidle', timeout: 30000 }); 188 | await page.waitForTimeout(3000); 189 | 190 | await expect(page.locator('#app')).toBeVisible(); 191 | 192 | // 播放器UI存在 193 | const hasPlayerUI = 194 | (await page.locator('text=正在播放').count()) > 0 || 195 | (await page.locator('text=播放列表').count()) > 0 || 196 | (await page.locator('text=暂无音频').count()) > 0 || 197 | (await page.locator('.player').count()) > 0; 198 | expect(hasPlayerUI || true).toBeTruthy(); 199 | ```