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); } } /** * 通过移动端汉堡菜单导航到指定页面 * 移动端导航菜单中所有项均为 `` 链接(无下拉展开),直接点击对应标签跳转。 */ 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 = { '/products': '产品', '/solutions': '解决方案', '/services': '服务', '/cases': '案例', '/news': '新闻动态', '/about': '关于我们', '/contact': '联系我们', }; const label = pathToLabel[targetPath]; if (!label) { // 未知路径,直接导航 await page.goto(targetPath, { waitUntil: 'domcontentloaded', timeout: 30000 }); return; } // 移动端菜单中所有项都是 `` 链接,直接点击 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'); }); });