import { test, expect, type Page } from '@playwright/test'; /** * P6: 缺失路径 E2E 测试补充 * * 覆盖范围: * 1. 滚动进度条(ScrollProgress)行为 * 2. 联系表单完整成功提交流程 * 3. 首页 → 产品列表 → 产品详情完整用户路径 * 4. 服务详情页滚动进度条 */ test.setTimeout(60000); // ==================== 1. 滚动进度条测试 ==================== test.describe('滚动进度条 - 全局组件行为', () => { test('页面顶部时进度条缩放为 0', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(1500); const progressBar = page.locator('[data-testid="scroll-progress"]'); await expect(progressBar).toBeVisible(); const progressFill = progressBar.locator('> div').first(); const scale = await progressFill.evaluate((el) => { const transform = (el as HTMLElement).style.transform; const match = transform.match(/scaleX\(([\d.]+)\)/); return match ? parseFloat(match[1]) : 0; }); expect(scale).toBe(0); }); test('滚动页面时进度条缩放增加', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(1500); const progressBar = page.locator('[data-testid="scroll-progress"]'); const progressFill = progressBar.locator('> div').first(); const getScale = () => progressFill.evaluate((el) => { const transform = (el as HTMLElement).style.transform; const match = transform.match(/scaleX\(([\d.]+)\)/); return match ? parseFloat(match[1]) : 0; }); // 先获取顶部缩放 const topScale = await getScale(); // 滚动到页面底部 await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); await page.waitForTimeout(500); // 再次获取缩放 const bottomScale = await getScale(); expect(bottomScale).toBeGreaterThan(topScale); }); test('进度条颜色使用品牌色变量', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(1500); const progressFill = page.locator('[data-testid="scroll-progress"] > div').first(); const background = await progressFill.evaluate((el) => window.getComputedStyle(el).backgroundColor); // 应存在背景色(具体颜色取决于 CSS 变量解析) expect(background).toBeTruthy(); expect(background).not.toBe('rgba(0, 0, 0, 0)'); }); }); // ==================== 2. 联系表单完整提交流程 ==================== test.describe('联系表单 - 完整成功提交', () => { test.beforeEach(async ({ page }) => { await page.goto('/contact', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2500); // 关闭 Cookie 同意横幅,避免覆盖底部表单元素导致点击错位 const acceptAllButton = page.locator('button:has-text("接受所有")').first(); if (await acceptAllButton.count() > 0 && await acceptAllButton.isVisible()) { await acceptAllButton.click(); await page.waitForTimeout(500); } }); async function fillContactForm(page: Page) { await page.locator('[data-testid="name-input"]').fill('测试用户'); await page.locator('[data-testid="phone-input"]').fill('13800138000'); await page.locator('[data-testid="email-input"]').fill('test@example.com'); await page.locator('[data-testid="subject-input"]').fill('E2E 测试咨询'); await page.locator('[data-testid="message-input"]').fill('这是一条由 Playwright E2E 测试发送的消息。'); } async function clickSubmitButton(page: Page) { const submitButton = page.locator('[data-testid="submit-button"]'); // 按钮位于页面底部,先滚动到视口并等待布局稳定,避免点击被底部固定栏截获 await submitButton.scrollIntoViewIfNeeded(); await page.waitForTimeout(300); // 使用原生 DOM click() 触发提交:Playwright 的 locator.click() 在该按钮上会被内部 flex 内容 // 的命中测试干扰,导致事件未正确冒泡到 form 的 React onSubmit 处理器。 await page.evaluate(() => { const btn = document.querySelector('[data-testid="submit-button"]') as HTMLButtonElement | null; btn?.click(); }); } test('填写有效信息并提交成功', async ({ page }) => { let routeHit = false; // 拦截表单提交 API,返回成功响应 await page.route('/api/contact', async (route) => { routeHit = true; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, message: '发送成功' }), }); }); await fillContactForm(page); await clickSubmitButton(page); // 等待成功状态出现 await page.waitForTimeout(1500); console.log('Route hit in success test:', routeHit); // 验证成功提示或成功状态存在 const bodyText = await page.locator('body').textContent(); const hasSuccessIndicator = bodyText!.includes('消息已发送') || bodyText!.includes('感谢') || bodyText!.includes('发送成功'); expect(hasSuccessIndicator).toBe(true); }); test('提交时显示加载状态', async ({ page }) => { let routeHit = false; // 延迟 API 响应以观察加载状态 await page.route('/api/contact', async (route) => { routeHit = true; await new Promise((resolve) => setTimeout(resolve, 800)); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, message: '发送成功' }), }); }); await fillContactForm(page); const submitButton = page.locator('[data-testid="submit-button"]'); await submitButton.scrollIntoViewIfNeeded(); await page.waitForTimeout(300); // 隐藏可能覆盖底部元素的固定返回顶部按钮 await page.evaluate(() => { const backToTop = document.querySelector('[aria-label="返回顶部"]') as HTMLElement | null; if (backToTop) backToTop.style.display = 'none'; }); // 使用原生 DOM click() 触发提交,避免 Playwright 命中测试在按钮 flex 内容上失效 await page.evaluate(() => { const btn = document.querySelector('[data-testid="submit-button"]') as HTMLButtonElement | null; btn?.click(); }); await page.waitForTimeout(100); console.log('disabled 100ms after click:', await submitButton.isDisabled(), 'routeHit:', routeHit); // 提交后立即检查按钮是否被禁用 await expect(submitButton).toBeDisabled({ timeout: 5000 }); }); }); // ==================== 3. 完整用户路径 ==================== test.describe('关键用户路径 - 首页 → 产品 → 详情', () => { test('从首页导航到产品列表页', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); // 桌面端导航中「产品」为下拉菜单,鼠标悬停展开后选择「查看全部产品」 const desktopNav = page.locator('[data-testid="desktop-navigation"]').first(); const productDropdown = desktopNav.locator('button:has-text("产品")').first(); await expect(productDropdown).toBeVisible({ timeout: 5000 }); await productDropdown.hover(); await page.waitForTimeout(300); const viewAllLink = desktopNav.locator('a:has-text("查看全部产品")').first(); await expect(viewAllLink).toBeVisible({ timeout: 5000 }); // 鼠标悬停在链接上,防止下拉菜单因 mouseleave 关闭 await viewAllLink.hover(); await page.waitForTimeout(200); await viewAllLink.click(); await page.waitForURL(/\/products/, { timeout: 10000 }); await expect(page).toHaveURL(/\/products/); const pageTitle = page.locator('h1, h2').first(); await expect(pageTitle).toBeVisible(); }); test('从产品列表页进入产品详情页', async ({ page }) => { await page.goto('/products', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); const productLinks = page.locator('a[href*="/products/"]:not([href$="/products"])'); const linkCount = await productLinks.count(); expect(linkCount).toBeGreaterThan(0); const firstProduct = productLinks.first(); await firstProduct.click(); await page.waitForURL(/\/products\/.+/, { timeout: 10000 }); await expect(page).toHaveURL(/\/products\/.+/); // 详情页应包含返回按钮或产品标题 const detailContent = page.locator('main').first(); await expect(detailContent).toBeVisible(); const detailText = await detailContent.textContent(); expect(detailText!.length).toBeGreaterThan(100); }); test('从产品详情页可跳转回产品列表', async ({ page }) => { await page.goto('/products/erp', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); // 查找返回或面包屑链接 const backLink = page.locator('a[href="/products"], a:has-text("返回")').first(); if (await backLink.count() > 0 && await backLink.isVisible()) { await backLink.click(); await page.waitForURL(/\/products/, { timeout: 10000 }); await expect(page).toHaveURL(/\/products/); } }); }); // ==================== 4. 服务详情页滚动进度条 ==================== test.describe('服务详情页 - 滚动进度条', () => { test('服务详情页存在滚动进度条', async ({ page }) => { await page.goto('/services', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); const serviceLinks = page.locator('a[href*="/services/"]:not([href$="/services"])'); if (await serviceLinks.count() > 0) { await serviceLinks.first().click(); await page.waitForURL(/\/services\/.+/, { timeout: 10000 }); } else { await page.goto('/services/digital-transformation', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); } // 服务详情页应渲染 ScrollProgress const progressBar = page.locator('[data-testid="scroll-progress"]'); await expect(progressBar).toBeVisible(); const progressFill = progressBar.locator('> div').first(); // 滚动后缩放应变化 await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); await page.waitForTimeout(500); const scale = await progressFill.evaluate((el) => { const transform = (el as HTMLElement).style.transform; const match = transform.match(/scaleX\(([\d.]+)\)/); return match ? parseFloat(match[1]) : 0; }); expect(scale).toBeGreaterThan(0); }); });