Files
novalon-website/e2e/p6-missing-paths.spec.ts
T
zhangxiang 6bb7c557ee test(qa): e2e 装配修正 + 集成/门禁测试与工具链重构
- e2e:修死选择器与蜜罐误命中、补断言、GA4/security-headers 用例诚实标注
  skip 边界;新增 assert-helpers/fixtures/hydrated/primary-nav/touch-targets
  等复用桩。
- 集成层:config/test/jest.integration.config.js + tests-integration/ 真库
  一次性 SQLite 用例,teardown 守卫开发库指纹。
- 门禁工具链:jest.setup 归位到根、scripts/accessibility 全站 axe 节点计数、
  check-brand-text-token/check-motion-constraints 机械守卫、stryker/lighthouse
  配置收敛。
2026-09-28 10:48:08 +08:00

284 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect, type Page } from './fixtures';
/**
* P6: 缺失路径 E2E 测试补充
*
* 覆盖范围:
* 1. 滚动进度条(ScrollProgress)行为
* 2. 联系表单完整成功提交流程
* 3. 首页 → 产品列表 → 产品详情完整用户路径
* 4. 服务详情页滚动进度条
*/
test.setTimeout(60000);
// 本套件走桌面导航(desktop-navigation 在 md 断点以下 display:none):
// 固定桌面视口,使 chromium-mobile 项目也按用例真实语义验证引擎而非布局。
test.use({ viewport: { width: 1440, height: 900 } });
// ==================== 1. 滚动进度条测试 ====================
test.describe('滚动进度条 - 全局组件行为', { tag: '@regression' }, () => {
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('联系表单 - 完整成功提交', { tag: '@regression' }, () => {
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('关键用户路径 - 首页 → 产品 → 详情', { tag: ['@smoke', '@regression'] }, () => {
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 }) => {
// erp 为 seed 中已发布产品(prisma/dev.db ContentItem slug='erp')。
// N-27 修复:原守卫 `if (count > 0 && isVisible)` ⇒ 返回链接消失时静默通过。
// product-detail-content-v3.tsx:97 恒定渲染 data-testid="product-detail-hero-secondary"
// 的「查看更多产品」CTA(href="/products"),以此做硬断言 + 真跳转。
await page.goto('/products/erp', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
const backLink = page.locator('[data-testid="product-detail-hero-secondary"]');
await expect(backLink).toBeVisible();
await expect(backLink).toHaveAttribute('href', '/products');
await Promise.all([
page.waitForURL((url) => new URL(url).pathname === '/products', { waitUntil: 'commit', timeout: 15000 }),
backLink.click(),
]);
await expect(page).toHaveURL((url) => url.pathname === '/products');
});
});
// ==================== 4. 服务详情页滚动进度条 ====================
test.describe('服务详情页 - 滚动进度条', { tag: '@regression' }, () => {
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);
});
});