Files
novalon-website/e2e/mobile.spec.ts
T
zhangxiang 3e629bd9ee test(e2e): add GA4 tracking, mobile, user journey tests and update visual baselines
- Add GA4 event tracking E2E tests (4 cases: page view, form submit, button click, product page)
- Add mobile-specific E2E tests (16 cases: navigation, form, product browsing)
- Add user journey tests (UJ-01: homepage to contact, UJ-02: product discovery)
- Fix E2E test selectors and assertions for consistency
- Update visual regression baselines across all browsers (chromium/firefox/webkit)
- Update Playwright config for new test files
2026-07-31 20:24:57 +08:00

330 lines
12 KiB
TypeScript

import { test, expect, devices } from '@playwright/test';
/**
* 移动端专项 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('text=产品').first();
await productLink.click();
await page.waitForURL(/\/products/, { timeout: 10000 });
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('text=关于我们').first();
await aboutLink.click();
await page.waitForURL(/\/about/, { timeout: 10000 });
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: 'domcontentloaded', timeout: 30000 });
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();
// 检查卡片边界在视口内
const box = await card.boundingBox();
expect(box).not.toBeNull();
if (box) {
expect(box.x).toBeGreaterThanOrEqual(0);
expect(box.x + box.width).toBeLessThanOrEqual(390); // iPhone 14 宽度
}
}
});
test('移动端无水平滚动条', async ({ page }) => {
// 检查是否出现水平滚动条
const hasHorizontalScroll = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(hasHorizontalScroll).toBeFalsy();
});
test('移动端点击产品卡片进入详情页', async ({ page }) => {
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 });
expect(page.url()).toMatch(/\/products\/[^/]+$/);
});
test('移动端产品详情页内容完整', async ({ page }) => {
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 });
// 详情页主要内容应渲染
const mainContent = page.locator('main').first();
await expect(mainContent).toBeVisible({ timeout: 10000 });
const contentText = await mainContent.textContent();
expect(contentText!.length).toBeGreaterThan(100);
// 移动端详情页应包含返回操作
const backLink = page.locator('a[href="/products"], a:has-text("返回")').first();
if (await backLink.count() > 0) {
await expect(backLink).toBeVisible();
}
});
test('移动端产品详情页可返回列表', async ({ page }) => {
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 });
// 查找返回或面包屑链接
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$/);
}
});
});