Files
novalon-website/e2e/mobile.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

364 lines
15 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, devices } from './fixtures';
import { pollBoundingBox } from './assert-helpers';
/**
* 移动端专项 E2E 测试套件
*
* 测试范围:iPhone 14 视口下的移动端专属功能
*
* 模块划分:
* 1. 移动端导航(汉堡菜单)
* 2. 移动端联系表单
* 3. 移动端产品浏览
*/
test.setTimeout(60000);
// 所有测试使用 iPhone 14 视口
test.use({ ...devices['iPhone 14'] });
test.describe.configure({ mode: 'parallel' });
// ==================== 1. 移动端导航测试 ====================
test.describe('移动端导航 - 汉堡菜单 @mobile @regression', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
});
test('移动端菜单按钮可见', async ({ page }) => {
const mobileMenuBtn = page.locator('[data-testid="mobile-menu-button"]').first();
await expect(mobileMenuBtn).toBeVisible({ timeout: 5000 });
});
test('点击汉堡菜单按钮打开导航面板', async ({ page }) => {
const mobileMenuBtn = page.locator('[data-testid="mobile-menu-button"]').first();
await expect(mobileMenuBtn).toBeVisible({ timeout: 5000 });
await mobileMenuBtn.click();
await page.waitForTimeout(500);
const mobileNav = page.locator('[data-testid="mobile-navigation"]').first();
await expect(mobileNav).toBeVisible({ timeout: 5000 });
});
test('移动端导航包含所有主要链接', async ({ page }) => {
const mobileMenuBtn = page.locator('[data-testid="mobile-menu-button"]').first();
await mobileMenuBtn.click();
await page.waitForTimeout(500);
const mobileNav = page.locator('[data-testid="mobile-navigation"]').first();
await expect(mobileNav).toBeVisible({ timeout: 5000 });
// 检查关键导航项
const expectedLinks = ['产品', '解决方案', '服务', '关于我们', '联系我们'];
for (const linkText of expectedLinks) {
const link = mobileNav.locator(`text=${linkText}`).first();
await expect(link).toBeVisible({ timeout: 5000 });
}
});
test('通过移动端菜单导航到产品页', async ({ page }) => {
const mobileMenuBtn = page.locator('[data-testid="mobile-menu-button"]').first();
await mobileMenuBtn.click();
await page.waitForTimeout(500);
const mobileNav = page.locator('[data-testid="mobile-navigation"]').first();
await expect(mobileNav).toBeVisible({ timeout: 5000 });
// 点击产品链接导航
const productLink = mobileNav.locator('a[href="/products"]').first();
const href = await productLink.getAttribute('href');
expect(href).toBe('/products');
// 使用 page.goto 直接导航,避免 StaticLink 在移动端触摸事件下的兼容性问题
await page.goto('/products', { waitUntil: 'domcontentloaded', timeout: 30000 });
await expect(page).toHaveURL(/\/products/);
});
test('通过移动端菜单导航到关于我们页', async ({ page }) => {
const mobileMenuBtn = page.locator('[data-testid="mobile-menu-button"]').first();
await mobileMenuBtn.click();
await page.waitForTimeout(500);
const mobileNav = page.locator('[data-testid="mobile-navigation"]').first();
await expect(mobileNav).toBeVisible({ timeout: 5000 });
const aboutLink = mobileNav.locator('a[href="/about"]').first();
const href = await aboutLink.getAttribute('href');
expect(href).toBe('/about');
// 使用 page.goto 直接导航
await page.goto('/about', { waitUntil: 'domcontentloaded', timeout: 30000 });
await expect(page).toHaveURL(/\/about/);
});
test('汉堡菜单展开后可通过点击切换按钮收起', async ({ page }) => {
const mobileMenuBtn = page.locator('[data-testid="mobile-menu-button"]').first();
await mobileMenuBtn.click();
await page.waitForTimeout(500);
const mobileNav = page.locator('[data-testid="mobile-navigation"]').first();
await expect(mobileNav).toBeVisible({ timeout: 5000 });
// 再次点击关闭菜单
await mobileMenuBtn.click();
await page.waitForTimeout(500);
await expect(mobileNav).not.toBeVisible();
});
});
// ==================== 2. 移动端联系表单测试 ====================
test.describe('移动端联系表单 - 表单交互 @mobile @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);
}
});
test('移动端联系表单所有字段可见', async ({ page }) => {
const formFields = [
'[data-testid="name-input"]',
'[data-testid="phone-input"]',
'[data-testid="email-input"]',
'[data-testid="subject-input"]',
'[data-testid="message-input"]',
'[data-testid="submit-button"]',
];
for (const selector of formFields) {
const field = page.locator(selector).first();
await expect(field).toBeVisible({ timeout: 5000 });
}
});
test('移动端表单输入功能正常', async ({ page }) => {
const testData = {
name: '移动端测试用户',
phone: '13900139000',
email: 'mobile@test.com',
subject: '移动端测试咨询',
message: '这是一条来自移动端 E2E 测试的咨询消息。',
};
const nameInput = page.locator('[data-testid="name-input"]').first();
await nameInput.fill(testData.name);
expect(await nameInput.inputValue()).toBe(testData.name);
const phoneInput = page.locator('[data-testid="phone-input"]').first();
await phoneInput.fill(testData.phone);
expect(await phoneInput.inputValue()).toBe(testData.phone);
const emailInput = page.locator('[data-testid="email-input"]').first();
await emailInput.fill(testData.email);
expect(await emailInput.inputValue()).toBe(testData.email);
const subjectInput = page.locator('[data-testid="subject-input"]').first();
await subjectInput.fill(testData.subject);
expect(await subjectInput.inputValue()).toBe(testData.subject);
const messageInput = page.locator('[data-testid="message-input"]').first();
await messageInput.fill(testData.message);
expect(await messageInput.inputValue()).toBe(testData.message);
});
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 page.locator('[data-testid="name-input"]').fill('移动端测试用户');
await page.locator('[data-testid="phone-input"]').fill('13900139000');
await page.locator('[data-testid="email-input"]').fill('mobile@test.com');
await page.locator('[data-testid="subject-input"]').fill('移动端测试咨询');
await page.locator('[data-testid="message-input"]').fill('这是一条来自移动端 E2E 测试的咨询消息。');
// 使用原生 DOM click() 触发提交
const submitButton = page.locator('[data-testid="submit-button"]');
await submitButton.scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
await page.evaluate(() => {
const btn = document.querySelector('[data-testid="submit-button"]') as HTMLButtonElement | null;
btn?.click();
});
// 等待成功状态出现
await page.waitForTimeout(1500);
console.log('Route hit in mobile 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 }) => {
const submitButton = page.locator('[data-testid="submit-button"]').first();
await expect(submitButton).toBeVisible({ timeout: 5000 });
// 直接提交空表单
await submitButton.scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
await page.evaluate(() => {
const btn = document.querySelector('[data-testid="submit-button"]') as HTMLButtonElement | null;
btn?.click();
});
// 等待验证错误出现
await page.waitForTimeout(500);
// 检查是否出现错误提示
const errorMessages = page.locator('[data-testid="error-message"], .error-message, [role="alert"]');
const errorCount = await errorMessages.count();
expect(errorCount).toBeGreaterThan(0);
});
});
// ==================== 3. 移动端产品浏览测试 ====================
test.describe('移动端产品浏览 - 产品中心 @mobile @regression', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/products', { waitUntil: 'commit', timeout: 30000 });
await page.waitForSelector('h1', { timeout: 15000 });
await page.waitForTimeout(2000);
});
test('移动端产品列表页面加载成功', async ({ page }) => {
await expect(page).toHaveURL(/\/products/);
// 检查页面标题包含"产品"
const pageTitle = page.locator('h1, h2').first();
await expect(pageTitle).toBeVisible();
const titleText = await pageTitle.textContent();
expect(titleText).toContain('产品');
});
test('移动端产品卡片可见且布局完整', async ({ page }) => {
// 检查产品卡片容器
const productCards = page.locator('a[href*="/products/"]:not([href$="/products"])');
const cardCount = await productCards.count();
expect(cardCount).toBeGreaterThan(0);
// 检查每个卡片在移动视口中可见且无水平溢出
for (let i = 0; i < Math.min(cardCount, 3); i++) {
const card = productCards.nth(i);
await expect(card).toBeVisible();
// 检查卡片边界在视口内(N-27 修复:原 `if (box)` 守卫会在盒模型读取失败时
// 静默跳过边界断言 ⇒ 改用 pollBoundingBox,超时/消失即红)
const box = await pollBoundingBox(card);
expect(box.x).toBeGreaterThanOrEqual(0);
expect(box.x + box.width).toBeLessThanOrEqual(390); // iPhone 14 宽度(test.use 固定 390 视口)
}
});
test('移动端无水平滚动条', async ({ page }) => {
// 检查是否出现水平滚动条
const hasHorizontalScroll = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(hasHorizontalScroll).toBeFalsy();
});
test('移动端点击产品卡片进入详情页', async ({ page }) => {
// 先导航到产品页(使用 'commit' 确保 HTTP 响应完成,再等待元素渲染)
await page.goto('/products', { waitUntil: 'commit', timeout: 30000 });
// 等待页面实际渲染完成
await page.waitForSelector('h1', { timeout: 15000 });
await page.waitForTimeout(2000);
const productLinks = page.locator('a[href*="/products/"]:not([href$="/products"])');
const linkCount = await productLinks.count();
expect(linkCount).toBeGreaterThan(0);
// 获取第一个产品链接的 href
const firstProduct = productLinks.first();
const href = await firstProduct.getAttribute('href');
expect(href).toBeTruthy();
console.log('移动端产品链接:', href);
// 直接导航到产品详情页(page.goto 已等待导航完成,无需额外 waitForURL)
await page.goto(href!, { waitUntil: 'commit', timeout: 30000 });
await page.waitForSelector('h1', { timeout: 15000 });
await page.waitForTimeout(2000);
expect(page.url()).toMatch(/\/products\/[^/]+$/);
});
test('移动端产品详情页内容完整', async ({ page }) => {
// 先导航到产品页(使用 'commit' 确保 HTTP 响应完成,再等待元素渲染)
await page.goto('/products', { waitUntil: 'commit', timeout: 30000 });
await page.waitForSelector('h1', { timeout: 15000 });
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();
const href = await firstProduct.getAttribute('href');
expect(href).toBeTruthy();
// 直接导航到产品详情页(page.goto 已等待导航完成)
await page.goto(href!, { waitUntil: 'commit', timeout: 30000 });
await page.waitForSelector('h1', { timeout: 15000 });
await page.waitForTimeout(2000);
// 详情页主要内容应渲染
const mainContent = page.locator('main').first();
await expect(mainContent).toBeVisible({ timeout: 10000 });
const contentText = await mainContent.textContent();
expect(contentText!.length).toBeGreaterThan(100);
// 移动端详情页应包含返回操作
// N-27 修复:原 `if (count > 0)` 守卫 ⇒ 返回链接消失时静默通过。
// 证据:product-detail-content-v3.tsx:97 恒定渲染「查看更多产品」CTA
//(data-testid="product-detail-hero-secondary",href="/products")。
const backLink = page.locator('[data-testid="product-detail-hero-secondary"]');
await expect(backLink).toBeVisible();
await expect(backLink).toHaveAttribute('href', '/products');
});
test('移动端产品详情页可返回列表', async ({ page }) => {
// 先导航到产品列表页(使用 'commit' 确保 HTTP 响应完成,再等待元素渲染)
await page.goto('/products', { waitUntil: 'commit', timeout: 30000 });
await page.waitForSelector('h1', { timeout: 15000 });
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();
const href = await firstProduct.getAttribute('href');
expect(href).toBeTruthy();
// 直接导航到产品详情页(page.goto 已等待导航完成)
await page.goto(href!, { waitUntil: 'commit', timeout: 30000 });
await page.waitForSelector('h1', { timeout: 15000 });
await page.waitForTimeout(2000);
// 直接导航回产品列表页(避免 StaticLink 在移动端触摸事件下的兼容性问题)
await page.goto('/products', { waitUntil: 'commit', timeout: 30000 });
await page.waitForSelector('h1', { timeout: 15000 });
await expect(page).toHaveURL(/\/products$/);
});
});