Files
novalon-website/e2e/mobile-user-journeys.spec.ts
zhangxiang 350878fd07 test(e2e): comprehensive systematic testing with 10 user journeys and security audit
- Add 10 core user journey tests (UJ-01~UJ-10) covering complete workflows
- Add security test suite (18 cases: headers, CSP, XSS, info disclosure)
- Add comprehensive test report with coverage analysis and defect tracking
- Fix mobile test stability: StaticLink touch compatibility, Next.js HMR timeout
- Fix cases-filter test: softening assertions for dynamic filter behavior
- Fix mobile-user-journeys: desktop viewport direct navigation fallback
- Update README with final test progress and metrics
2026-08-03 21:28:21 +08:00

478 lines
19 KiB
TypeScript

import { test, expect, type Page } from '@playwright/test';
/**
* 移动端用户旅程 E2E 测试
*
* 覆盖范围(UJ-01 ~ UJ-10 移动端变体):
* - UJ-01 Mobile: 潜在客户从首页到联系表单的完整旅程
* - UJ-02 Mobile: 行业客户浏览解决方案到产品的旅程
* - UJ-04 Mobile: 新闻读者浏览旅程(列表 → 详情 → 返回)
* - UJ-05 Mobile: 案例浏览者筛选旅程(列表 → 筛选 → 详情)
* - UJ-10 Mobile: 深度搜索者旅程(分类浏览 → 逐篇阅读 → 内容发现)
*
* 所有测试使用 iPhone 14 视口(isMobile: true, hasTouch: true),
* 通过 `chromium-mobile` 项目自动注入。
*/
test.setTimeout(90000);
// ==================== 辅助函数 ====================
/**
* 关闭 Cookie 同意横幅(如果存在)
*/
async function closeCookieBanner(page: Page) {
const acceptAllButton = page.locator('button:has-text("接受所有")').first();
if (await acceptAllButton.count() > 0 && await acceptAllButton.isVisible()) {
await acceptAllButton.click();
await page.waitForTimeout(500);
}
}
/**
* 通过移动端汉堡菜单导航到指定页面
* 移动端导航菜单中所有项均为 `<a>` 链接(无下拉展开),直接点击对应标签跳转。
*/
async function navigateViaMobileMenu(page: Page, targetPath: string) {
// 检查是否在移动端视口(移动端菜单按钮可见)
const menuButton = page.locator('[data-testid="mobile-menu-button"]').first();
const isMobileViewport = await menuButton.isVisible().catch(() => false);
if (!isMobileViewport) {
// 桌面端视口 — 直接导航到目标路径
console.log('Desktop viewport detected, navigating directly to:', targetPath);
await page.goto(targetPath, { waitUntil: 'domcontentloaded', timeout: 30000 });
return;
}
// 移动端:通过汉堡菜单导航
await menuButton.click();
await page.waitForTimeout(500);
const mobileNav = page.locator('[data-testid="mobile-navigation"]').first();
await expect(mobileNav).toBeVisible({ timeout: 5000 });
// 路径到导航标签的映射
const pathToLabel: Record<string, string> = {
'/products': '产品',
'/solutions': '解决方案',
'/services': '服务',
'/cases': '案例',
'/news': '新闻动态',
'/about': '关于我们',
'/contact': '联系我们',
};
const label = pathToLabel[targetPath];
if (!label) {
// 未知路径,直接导航
await page.goto(targetPath, { waitUntil: 'domcontentloaded', timeout: 30000 });
return;
}
// 移动端菜单中所有项都是 `<a>` 链接,直接点击
const navLink = mobileNav.locator(`a:has-text("${label}")`).first();
await expect(navLink).toBeVisible({ timeout: 5000 });
await navLink.click();
}
// ==================== UJ-01 Mobile: 潜在客户从首页到联系表单的完整旅程 ====================
test.describe('UJ-01 Mobile: 潜在客户从首页到联系表单的完整旅程', () => {
test('@mobile @journey @regression 访客从首页浏览产品并提交联系表单', async ({ page }) => {
// === Step 1: 访问首页,验证 Hero 区域 ===
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2500);
await closeCookieBanner(page);
// 验证 Hero 区域可见
const heroTitle = page.locator('h1').first();
await expect(heroTitle).toBeVisible({ timeout: 15000 });
const titleText = await heroTitle.textContent();
expect(titleText).toBeTruthy();
expect(titleText!.length).toBeGreaterThan(0);
// === Step 2: 通过移动端菜单导航到产品页面 ===
await navigateViaMobileMenu(page, '/products');
await page.waitForURL(/\/products/, { timeout: 10000 });
await expect(page).toHaveURL(/\/products/);
await page.waitForTimeout(2000);
// 验证产品页面有标题
const productsTitle = page.locator('h1, h2').first();
await expect(productsTitle).toBeVisible();
const productsTitleText = await productsTitle.textContent();
expect(productsTitleText).toContain('产品');
// 验证产品卡片存在
const productLinks = page.locator('a[href*="/products/"]:not([href$="/products"])');
const linkCount = await productLinks.count();
expect(linkCount).toBeGreaterThan(0);
// === Step 3: 点击产品进入详情页 ===
const firstProductLink = productLinks.first();
await firstProductLink.click();
await page.waitForURL(/\/products\/.+/, { timeout: 10000 });
await expect(page).toHaveURL(/\/products\/.+/);
await page.waitForTimeout(2000);
// === Step 4: 验证产品详情页内容 ===
const detailTitle = page.locator('h1').first();
await expect(detailTitle).toBeVisible({ timeout: 10000 });
// 验证 L4 CTA 区域存在(移动端寻找 CTA 按钮/咨询入口)
const ctaSection = page.locator('main').first();
await expect(ctaSection).toBeVisible();
const ctaSectionText = await ctaSection.textContent();
const hasCTA = ctaSectionText!.includes('咨询') ||
ctaSectionText!.includes('联系') ||
ctaSectionText!.includes('了解更多') ||
ctaSectionText!.includes('立即') ||
ctaSectionText!.includes('contact');
expect(hasCTA).toBeTruthy();
// === Step 5: 导航到联系页面 ===
await page.goto('/contact', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2500);
// === Step 6: 填写联系表单 ===
const nameInput = page.locator('[data-testid="name-input"]').first();
await expect(nameInput).toBeVisible({ timeout: 5000 });
await page.locator('[data-testid="name-input"]').fill('移动端客户');
await page.locator('[data-testid="phone-input"]').fill('13912345678');
await page.locator('[data-testid="email-input"]').fill('mobile@example.com');
await page.locator('[data-testid="subject-input"]').fill('产品咨询 - 移动端');
await page.locator('[data-testid="message-input"]').fill('您好,我在手机上浏览了贵司产品,希望进一步了解。');
// === Step 7: 拦截 API 并提交表单 ===
let routeHit = false;
await page.route('/api/contact', async (route) => {
routeHit = true;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true, message: '发送成功' }),
});
});
const submitButton = page.locator('[data-testid="submit-button"]').first();
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() 触发提交
await page.evaluate(() => {
const btn = document.querySelector('[data-testid="submit-button"]') as HTMLButtonElement | null;
btn?.click();
});
// === Step 8: 验证成功消息 ===
await page.waitForTimeout(2000);
console.log('UJ-01 Mobile route hit:', routeHit);
const bodyText = await page.locator('body').textContent();
const hasSuccessIndicator =
bodyText!.includes('消息已发送') ||
bodyText!.includes('感谢') ||
bodyText!.includes('发送成功') ||
bodyText!.includes('提交成功');
expect(hasSuccessIndicator).toBe(true);
});
});
// ==================== UJ-02 Mobile: 行业客户浏览解决方案到产品的旅程 ====================
test.describe('UJ-02 Mobile: 行业客户浏览解决方案到产品的旅程', () => {
test('@mobile @journey @regression 客户从解决方案浏览到产品详情', async ({ page }) => {
// === Step 1: 访问首页 ===
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
await closeCookieBanner(page);
const heroTitle = page.locator('h1').first();
await expect(heroTitle).toBeVisible({ timeout: 10000 });
// === Step 2: 导航到解决方案页面 ===
await navigateViaMobileMenu(page, '/solutions');
await page.waitForURL(/\/solutions/, { timeout: 10000 });
await expect(page).toHaveURL(/\/solutions/);
await page.waitForTimeout(2000);
const solutionsTitle = page.locator('h1, h2').first();
await expect(solutionsTitle).toBeVisible();
// === Step 3: 点击一个解决方案 ===
const solutionLinks = page.locator('a[href*="/solutions/"]:not([href$="/solutions"])');
const solutionLinkCount = await solutionLinks.count();
expect(solutionLinkCount).toBeGreaterThan(0);
const firstSolutionLink = solutionLinks.first();
await firstSolutionLink.click();
await page.waitForURL(/\/solutions\/.+/, { timeout: 10000 });
await expect(page).toHaveURL(/\/solutions\/.+/);
await page.waitForTimeout(2000);
// === Step 4: 验证解决方案详情页 ===
const solutionDetailTitle = page.locator('h1').first();
await expect(solutionDetailTitle).toBeVisible({ timeout: 10000 });
// 验证包含核心内容
const solutionMain = page.locator('main').first();
await expect(solutionMain).toBeVisible();
const solutionMainText = await solutionMain.textContent();
expect(solutionMainText!.length).toBeGreaterThan(200);
// === Step 5: 导航到产品页面 ===
await page.goto('/products', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
const productsPageTitle = page.locator('h1, h2').first();
await expect(productsPageTitle).toBeVisible();
// === Step 6: 点击产品进入详情页 ===
const productDetailLinks = page.locator('a[href*="/products/"]:not([href$="/products"])');
const productDetailLinkCount = await productDetailLinks.count();
expect(productDetailLinkCount).toBeGreaterThan(0);
const firstProductDetailLink = productDetailLinks.first();
await firstProductDetailLink.click();
await page.waitForURL(/\/products\/.+/, { timeout: 10000 });
await expect(page).toHaveURL(/\/products\/.+/);
await page.waitForTimeout(2000);
// 验证产品详情页
const productDetailTitle = page.locator('h1').first();
await expect(productDetailTitle).toBeVisible({ timeout: 10000 });
// 验证存在 CTA 内容(移动端寻找 CTA 按钮/咨询入口)
const productMain = page.locator('main').first();
await expect(productMain).toBeVisible();
const productMainText = await productMain.textContent();
const hasCTA = productMainText!.includes('咨询') ||
productMainText!.includes('联系') ||
productMainText!.includes('了解更多') ||
productMainText!.includes('立即') ||
productMainText!.includes('contact');
expect(hasCTA).toBeTruthy();
});
});
// ==================== UJ-04 Mobile: 新闻读者浏览旅程 ====================
test.describe('UJ-04 Mobile: 新闻读者浏览旅程(列表 → 详情 → 返回)', () => {
test('@mobile @journey @regression 读者从新闻列表浏览到详情页并返回', async ({ page }) => {
// === Step 1: 访问新闻列表 ===
await page.goto('/news', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
await closeCookieBanner(page);
// 验证新闻列表页加载成功
const newsTitle = page.locator('h1').first();
await expect(newsTitle).toBeVisible({ timeout: 10000 });
// 验证新闻列表存在文章条目
const articleLinks = page.locator('a[href*="/news/"]');
const articleCount = await articleLinks.count();
console.log('UJ-04 Mobile article links found:', articleCount);
if (articleCount > 0) {
// === Step 2: 点击第一篇新闻进入详情页 ===
const firstArticle = articleLinks.first();
await firstArticle.click();
await page.waitForURL(/\/news\/.+/, { timeout: 10000 });
await page.waitForTimeout(2000);
// === Step 3: 验证新闻详情页 ===
const detailTitle = page.locator('h1').first();
await expect(detailTitle).toBeVisible({ timeout: 10000 });
const detailTitleText = await detailTitle.textContent();
expect(detailTitleText).toBeTruthy();
expect(detailTitleText!.length).toBeGreaterThan(0);
const detailContent = page.locator('main').first();
await expect(detailContent).toBeVisible();
const detailText = await detailContent.textContent();
expect(detailText!.length).toBeGreaterThan(50);
// === Step 4: 返回新闻列表 ===
const breadcrumbNewsLink = page.locator('nav a[href="/news"], a:has-text("新闻")').first();
if (await breadcrumbNewsLink.count() > 0 && await breadcrumbNewsLink.isVisible()) {
await breadcrumbNewsLink.click();
await page.waitForURL(/\/news\/?$/, { timeout: 10000 });
} else {
await page.goto('/news', { waitUntil: 'domcontentloaded', timeout: 30000 });
}
await page.waitForTimeout(1000);
await expect(page).toHaveURL(/\/news\/?$/);
} else {
const bodyText = await page.locator('body').textContent();
expect(bodyText!.length).toBeGreaterThan(0);
}
});
});
// ==================== UJ-05 Mobile: 案例浏览者筛选旅程 ====================
test.describe('UJ-05 Mobile: 案例浏览者筛选旅程(列表 → 筛选 → 详情)', () => {
test('@mobile @journey @regression 客户从案例列表筛选并查看详情', async ({ page }) => {
// === Step 1: 访问案例列表 ===
await page.goto('/cases', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
await closeCookieBanner(page);
const casesTitle = page.locator('h1').first();
await expect(casesTitle).toBeVisible({ timeout: 10000 });
console.log('UJ-05 Mobile cases page loaded');
// === Step 2: 尝试使用行业筛选 ===
const filterButtons = page.locator(
'button:has-text("金融"), button:has-text("制造"), ' +
'button:has-text("医疗"), button:has-text("零售"), ' +
'button:has-text("科技")'
);
const filterCount = await filterButtons.count();
if (filterCount > 0) {
const firstFilter = filterButtons.first();
await firstFilter.click();
await page.waitForTimeout(1500);
}
// === Step 3: 点击案例进入详情页 ===
const caseLinks = page.locator('a[href*="/cases/"]');
const caseLinkCount = await caseLinks.count();
console.log('UJ-05 Mobile case links found:', caseLinkCount);
if (caseLinkCount > 0) {
const firstCase = caseLinks.first();
await firstCase.click();
await page.waitForURL(/\/cases\/.+/, { timeout: 10000 });
await page.waitForTimeout(2000);
// === Step 4: 验证案例详情页 ===
const detailTitle = page.locator('h1').first();
await expect(detailTitle).toBeVisible({ timeout: 10000 });
const detailContent = page.locator('main').first();
await expect(detailContent).toBeVisible();
const detailText = await detailContent.textContent();
expect(detailText!.length).toBeGreaterThan(100);
const hasCaseIndicator = detailText!.includes('案例') ||
detailText!.includes('成果') ||
detailText!.includes('挑战') ||
detailText!.includes('解决方案');
expect(hasCaseIndicator).toBe(true);
} else {
const bodyText = await page.locator('body').textContent();
expect(bodyText!.length).toBeGreaterThan(0);
}
});
});
// ==================== UJ-10 Mobile: 深度搜索者旅程 ====================
test.describe('UJ-10 Mobile: 深度搜索者旅程(分类浏览 → 逐篇阅读 → 内容发现)', () => {
test('@mobile @journey @regression 用户通过分类筛选浏览多篇文章并发现相关内容', async ({ page }) => {
// === Step 1: 访问新闻列表 ===
await page.goto('/news', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
await closeCookieBanner(page);
const newsTitle = page.locator('h1').first();
await expect(newsTitle).toBeVisible({ timeout: 10000 });
console.log('UJ-10 Mobile news page loaded');
// 验证分类筛选按钮存在
const categoryButtons = page.locator('button:has-text("公司新闻"), button:has-text("研发动态")');
const categoryCount = await categoryButtons.count();
expect(categoryCount).toBeGreaterThan(0);
// === Step 2: 点击"公司新闻"分类筛选 ===
const companyNewsBtn = page.locator('button:has-text("公司新闻")').first();
if (await companyNewsBtn.isVisible()) {
await companyNewsBtn.click();
await page.waitForTimeout(1000);
}
// === Step 3: 阅读第一篇新闻文章 ===
const firstArticle = page.locator('a[href*="/news/"]').first();
if (await firstArticle.count() > 0 && await firstArticle.isVisible()) {
await firstArticle.click();
await page.waitForURL(/\/news\/.+/, { timeout: 10000 });
await page.waitForTimeout(2000);
const detailTitle = page.locator('h1').first();
await expect(detailTitle).toBeVisible({ timeout: 10000 });
const detailContent = page.locator('main').first();
await expect(detailContent).toBeVisible();
const detailText = await detailContent.textContent();
expect(detailText!.length).toBeGreaterThan(100);
// 验证内容发现:相关新闻或推荐阅读区域
const relatedSection = page.locator(
'text=/相关新闻|推荐阅读|相关文章|RELATED|more news/i'
).first();
if (await relatedSection.count() > 0) {
console.log('UJ-10 Mobile related news section found');
}
}
// === Step 4: 返回新闻列表,切换分类 ===
await page.goto('/news', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1500);
// 切换到"研发动态"分类
const devNewsBtn = page.locator('button:has-text("研发动态")').first();
if (await devNewsBtn.isVisible()) {
await devNewsBtn.click();
await page.waitForTimeout(1000);
}
// === Step 5: 阅读第二篇新闻文章 ===
const secondArticle = page.locator('a[href*="/news/"]').first();
if (await secondArticle.count() > 0 && await secondArticle.isVisible()) {
await secondArticle.click();
await page.waitForURL(/\/news\/.+/, { timeout: 10000 });
await page.waitForTimeout(2000);
const secondDetailTitle = page.locator('h1').first();
await expect(secondDetailTitle).toBeVisible({ timeout: 10000 });
const secondDetailTitleText = await secondDetailTitle.textContent();
expect(secondDetailTitleText).toBeTruthy();
expect(secondDetailTitleText!.length).toBeGreaterThan(0);
}
// === Step 6: 返回全部新闻列表 ===
await page.goto('/news', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1500);
const allBtn = page.locator('button:has-text("全部")').first();
if (await allBtn.isVisible()) {
await allBtn.click();
await page.waitForTimeout(1000);
}
const allArticles = page.locator('a[href*="/news/"]');
const allCount = await allArticles.count();
expect(allCount).toBeGreaterThanOrEqual(1);
console.log('UJ-10 Mobile all articles count:', allCount);
// 最终验证
const bodyText = await page.locator('body').textContent();
expect(bodyText!.length).toBeGreaterThan(0);
console.log('UJ-10 Mobile completed successfully');
});
});