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 配置收敛。
This commit is contained in:
+103
-139
@@ -1,4 +1,4 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { test, expect, type Page } from './fixtures';
|
||||
|
||||
/**
|
||||
* 移动端用户旅程 E2E 测试
|
||||
@@ -270,6 +270,8 @@ test.describe('UJ-02 Mobile: 行业客户浏览解决方案到产品的旅程',
|
||||
// ==================== UJ-04 Mobile: 新闻读者浏览旅程 ====================
|
||||
test.describe('UJ-04 Mobile: 新闻读者浏览旅程(列表 → 详情 → 返回)', () => {
|
||||
test('@mobile @journey @regression 读者从新闻列表浏览到详情页并返回', async ({ page }) => {
|
||||
// N-27 修复:同桌面版 UJ-04(user-journey.spec.ts)——原 `if (articleCount > 0)`
|
||||
// 包住整段旅程,新闻消失时静默通过。种子恰有 2 篇已发布新闻,改为全程硬断言。
|
||||
// === Step 1: 访问新闻列表 ===
|
||||
await page.goto('/news', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
@@ -280,199 +282,161 @@ test.describe('UJ-04 Mobile: 新闻读者浏览旅程(列表 → 详情 →
|
||||
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);
|
||||
const firstArticle = page.locator('a[href*="/news/"]').first();
|
||||
await expect(firstArticle).toBeVisible({ timeout: 5000 });
|
||||
|
||||
if (articleCount > 0) {
|
||||
// === Step 2: 点击第一篇新闻进入详情页 ===
|
||||
const firstArticle = articleLinks.first();
|
||||
await firstArticle.click();
|
||||
// === Step 2: 点击第一篇新闻进入详情页 ===
|
||||
await Promise.all([
|
||||
page.waitForURL(/\/news\/.+/, { timeout: 15000, waitUntil: 'commit' }),
|
||||
firstArticle.click(),
|
||||
]);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
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);
|
||||
|
||||
// === 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);
|
||||
|
||||
const detailContent = page.locator('main').first();
|
||||
await expect(detailContent).toBeVisible();
|
||||
const detailText = await detailContent.textContent();
|
||||
expect(detailText!.length).toBeGreaterThan(50);
|
||||
// === Step 4: 通过面包屑返回新闻列表 ===
|
||||
// news-detail-content-v3.tsx:68-71 恒渲染 breadcrumb nav 中的「新闻动态」链接
|
||||
const breadcrumbNewsLink = page.locator('nav[aria-label="breadcrumb"] a[href="/news"]');
|
||||
await expect(breadcrumbNewsLink).toBeVisible();
|
||||
await Promise.all([
|
||||
page.waitForURL(/\/news\/?$/, { timeout: 15000, waitUntil: 'commit' }),
|
||||
breadcrumbNewsLink.click(),
|
||||
]);
|
||||
|
||||
// === 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);
|
||||
}
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(page).toHaveURL(/\/news\/?$/);
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== UJ-05 Mobile: 案例浏览者筛选旅程 ====================
|
||||
test.describe('UJ-05 Mobile: 案例浏览者筛选旅程(列表 → 筛选 → 详情)', () => {
|
||||
test('@mobile @journey @regression 客户从案例列表筛选并查看详情', async ({ page }) => {
|
||||
// === Step 1: 访问案例列表 ===
|
||||
test('@mobile @journey @regression 客户访问案例页,零案例种子下经共创空态转化到联系页', async ({ page }) => {
|
||||
// N-27 修复:同桌面版 UJ-05。种子刻意 0 案例(prisma/seed.ts:628-631),
|
||||
// 原 `if (caseLinkCount > 0)` + else 兜底 ⇒ 案例区为空也绿。
|
||||
// 现断言当前真实契约:空态卡存在、筛选器缺席,并走完空态转化出口到联系页。
|
||||
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');
|
||||
await expect(casesTitle).toContainText('以成果为目标');
|
||||
|
||||
// === 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);
|
||||
}
|
||||
// 零案例契约:行业筛选按钮组与案例卡片必须不存在
|
||||
await expect(page.getByRole('group', { name: '按行业筛选' })).toHaveCount(0);
|
||||
await expect(page.locator('a[href*="/cases/"]')).toHaveCount(0);
|
||||
|
||||
// === Step 3: 点击案例进入详情页 ===
|
||||
const caseLinks = page.locator('a[href*="/cases/"]');
|
||||
const caseLinkCount = await caseLinks.count();
|
||||
console.log('UJ-05 Mobile case links found:', caseLinkCount);
|
||||
// 空态转化卡:预约咨询 CTA 恒定渲染(cases-content-v3.tsx:334-336)
|
||||
const emptyPrimaryCta = page.locator('[data-testid="cases-empty-primary-cta"]');
|
||||
await expect(emptyPrimaryCta).toBeVisible();
|
||||
await expect(emptyPrimaryCta).toHaveAttribute('href', '/contact');
|
||||
|
||||
if (caseLinkCount > 0) {
|
||||
const firstCase = caseLinks.first();
|
||||
await firstCase.click();
|
||||
// === 点击转化出口,验证抵达联系页 ===
|
||||
await Promise.all([
|
||||
page.waitForURL((url) => new URL(url).pathname === '/contact', { timeout: 15000, waitUntil: 'commit' }),
|
||||
emptyPrimaryCta.click(),
|
||||
]);
|
||||
await expect(page).toHaveURL((url) => url.pathname === '/contact');
|
||||
|
||||
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);
|
||||
}
|
||||
const contactTitle = page.locator('h1').first();
|
||||
await expect(contactTitle).toBeVisible({ timeout: 10000 });
|
||||
const contactText = await page.locator('main').first().textContent();
|
||||
expect(contactText!.length).toBeGreaterThan(50);
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== UJ-10 Mobile: 深度搜索者旅程 ====================
|
||||
test.describe('UJ-10 Mobile: 深度搜索者旅程(分类浏览 → 逐篇阅读 → 内容发现)', () => {
|
||||
test('@mobile @journey @regression 用户通过分类筛选浏览多篇文章并发现相关内容', async ({ page }) => {
|
||||
// === Step 1: 访问新闻列表 ===
|
||||
// N-27 修复:同桌面版 UJ-10(user-journey.spec.ts)——原 Step 2/3/4/5/6 全部被
|
||||
// `if (isVisible)`/`if (count > 0)` 包住。分类按钮组与 2 篇种子新闻(各属一类)
|
||||
// 均为恒定契约 ⇒ 全程硬断言。
|
||||
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);
|
||||
// 验证分类筛选按钮存在且可点(news-content-v3.tsx:159-167 无条件渲染)
|
||||
const companyNewsBtn = page.locator('button', { hasText: '公司新闻' }).first();
|
||||
const devNewsBtn = page.locator('button', { hasText: '研发动态' }).first();
|
||||
await expect(companyNewsBtn).toBeVisible();
|
||||
await expect(devNewsBtn).toBeVisible();
|
||||
|
||||
// === Step 2: 点击"公司新闻"分类筛选 ===
|
||||
const companyNewsBtn = page.locator('button:has-text("公司新闻")').first();
|
||||
if (await companyNewsBtn.isVisible()) {
|
||||
await companyNewsBtn.click();
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
// === Step 2: 点击"公司新闻"分类筛选 → 恰收窄到 1 篇 ===
|
||||
await companyNewsBtn.click();
|
||||
await expect(page.locator('a[href*="/news/"]')).toHaveCount(1);
|
||||
|
||||
// === Step 3: 阅读第一篇新闻文章 ===
|
||||
const firstArticle = page.locator('a[href*="/news/"]').first();
|
||||
if (await firstArticle.count() > 0 && await firstArticle.isVisible()) {
|
||||
await firstArticle.click();
|
||||
await expect(firstArticle).toContainText('正式成立');
|
||||
await Promise.all([
|
||||
page.waitForURL(/\/news\/.+/, { timeout: 15000, waitUntil: 'commit' }),
|
||||
firstArticle.click(),
|
||||
]);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.waitForURL(/\/news\/.+/, { timeout: 10000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const detailTitle = page.locator('h1').first();
|
||||
await expect(detailTitle).toContainText('正式成立');
|
||||
|
||||
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 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');
|
||||
}
|
||||
}
|
||||
// 两篇新闻分属不同分类 ⇒ 同分类相关新闻恒为空,区域必须缺席
|
||||
await expect(page.getByRole('heading', { name: '相关新闻' })).toHaveCount(0);
|
||||
|
||||
// === 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);
|
||||
}
|
||||
const devNewsBtn2 = page.locator('button', { hasText: '研发动态' }).first();
|
||||
await expect(devNewsBtn2).toBeVisible();
|
||||
await devNewsBtn2.click();
|
||||
await expect(page.locator('a[href*="/news/"]')).toHaveCount(1);
|
||||
|
||||
// === Step 5: 阅读第二篇新闻文章 ===
|
||||
const secondArticle = page.locator('a[href*="/news/"]').first();
|
||||
if (await secondArticle.count() > 0 && await secondArticle.isVisible()) {
|
||||
await secondArticle.click();
|
||||
await Promise.all([
|
||||
page.waitForURL(/\/news\/.+/, { timeout: 15000, waitUntil: 'commit' }),
|
||||
secondArticle.click(),
|
||||
]);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
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);
|
||||
}
|
||||
const secondDetailTitle = page.locator('h1').first();
|
||||
await expect(secondDetailTitle).toContainText('数字化转型');
|
||||
const secondDetailContent = page.locator('main').first();
|
||||
await expect(secondDetailContent).toBeVisible();
|
||||
const secondDetailText = await secondDetailContent.textContent();
|
||||
expect(secondDetailText!.length).toBeGreaterThan(100);
|
||||
|
||||
// === 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 allBtn = page.locator('button', { hasText: '全部' }).first();
|
||||
await expect(allBtn).toBeVisible();
|
||||
await allBtn.click();
|
||||
|
||||
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);
|
||||
// 全部种子新闻恰好 2 篇
|
||||
await expect(page.locator('a[href*="/news/"]')).toHaveCount(2);
|
||||
|
||||
// 最终验证
|
||||
const bodyText = await page.locator('body').textContent();
|
||||
expect(bodyText!.length).toBeGreaterThan(0);
|
||||
console.log('UJ-10 Mobile completed successfully');
|
||||
// 最终验证:搜索发现能力('数字化转型' 恰命中 1 篇)
|
||||
const searchInput = page.getByLabel('搜索新闻');
|
||||
await expect(searchInput).toBeVisible();
|
||||
await searchInput.fill('数字化转型');
|
||||
await expect(page.locator('a[href*="/news/"]')).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user