- 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
330 lines
12 KiB
TypeScript
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$/);
|
|
}
|
|
});
|
|
}); |