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:
2026-09-28 10:48:08 +08:00
parent a366bd1400
commit 6bb7c557ee
142 changed files with 6597 additions and 2653 deletions
+103 -139
View File
@@ -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);
});
});