- 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 配置收敛。
197 lines
7.7 KiB
TypeScript
197 lines
7.7 KiB
TypeScript
import { test, expect } from './fixtures';
|
||
|
||
/**
|
||
* P5: 边缘情况与缺失路径 E2E 测试
|
||
*
|
||
* 覆盖范围:
|
||
* 1. 404 未找到页面
|
||
* 2. 全局错误边界
|
||
* 3. 页面加载状态
|
||
* 4. 无效路由与特殊字符
|
||
*/
|
||
|
||
test.setTimeout(60000);
|
||
|
||
// ==================== 1. 404 页面测试 ====================
|
||
test.describe('404 页面 - 未找到页面', { tag: '@regression' }, () => {
|
||
|
||
test('访问不存在的页面显示 404', async ({ page }) => {
|
||
await page.goto('/this-page-does-not-exist', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||
await page.waitForTimeout(1500);
|
||
|
||
// URL 应保持为请求的路径或重定向到 404 处理
|
||
expect(page.url()).toContain('this-page-does-not-exist');
|
||
|
||
// 检查 404 页面关键元素
|
||
const heading = page.locator('h1').first();
|
||
await expect(heading).toBeVisible();
|
||
const headingText = await heading.textContent();
|
||
expect(headingText).toMatch(/404|页面未找到/);
|
||
});
|
||
|
||
test('404 页面包含返回首页按钮', async ({ page }) => {
|
||
await page.goto('/non-existent-page', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||
await page.waitForTimeout(1500);
|
||
|
||
const homeButton = page.locator('a[href="/"]').first();
|
||
await expect(homeButton).toBeVisible();
|
||
expect(await homeButton.textContent()).toContain('返回首页');
|
||
});
|
||
|
||
test('404 页面推荐链接可点击', async ({ page }) => {
|
||
await page.goto('/missing-page', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||
await page.waitForTimeout(1500);
|
||
|
||
// N-27 修复:四个推荐链接在 src/app/not-found-content.tsx:41-81 中恒定渲染,
|
||
// 原 `if (count > 0 && isVisible)` 守卫会让本例在链接缺失时静默通过。
|
||
// 改为主内容区内作用域的轮询断言——任何一条缺失/隐藏即失败。
|
||
const recommendedLinks = [
|
||
{ text: '产品', expectedPath: '/products' },
|
||
{ text: '解决方案', expectedPath: '/solutions' },
|
||
{ text: '关于我们', expectedPath: '/about' },
|
||
{ text: '联系我们', expectedPath: '/contact' },
|
||
];
|
||
|
||
for (const { text, expectedPath } of recommendedLinks) {
|
||
const link = page.locator(`main a:has-text("${text}")`).first();
|
||
await expect(link, `404 页缺少推荐链接「${text}」`).toBeVisible();
|
||
await expect(link).toHaveAttribute('href', expectedPath);
|
||
}
|
||
});
|
||
|
||
test('404 页面标题设置正确', async ({ page }) => {
|
||
await page.goto('/not-found-test', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||
await page.waitForTimeout(1500);
|
||
|
||
const title = await page.title();
|
||
expect(title).toContain('页面未找到');
|
||
});
|
||
});
|
||
|
||
// ==================== 2. 错误边界测试 ====================
|
||
test.describe('错误边界 - 应用级错误处理', { tag: '@regression' }, () => {
|
||
|
||
test('错误页面包含重试按钮', async ({ page }) => {
|
||
// 未知路径渲染 src/app/not-found-content.tsx:其恢复动作是
|
||
// 「返回上一页」<button>(not-found-content.tsx:30-33)+「返回首页」CTA。
|
||
// N-27 修复:原守卫 `if (count > 0)` ⇒ 页面空了也绿。改为硬断言两者可见。
|
||
await page.goto('/error-test-trigger', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||
await page.waitForTimeout(1500);
|
||
|
||
const backButton = page.locator('main button', { hasText: '返回上一页' }).first();
|
||
await expect(backButton).toBeVisible();
|
||
|
||
const homeLink = page.locator('main a[href="/"]').first();
|
||
await expect(homeLink).toBeVisible();
|
||
});
|
||
|
||
test('错误页面包含返回首页链接', async ({ page }) => {
|
||
await page.goto('/error-boundary-test', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||
await page.waitForTimeout(1500);
|
||
|
||
// N-27 修复:守卫改为硬断言,并验证链接真的能回到首页
|
||
// (与 :32 的差异:此例覆盖点击后的导航行为,不只是文案存在)。
|
||
const homeLink = page.locator('main a[href="/"]').first();
|
||
await expect(homeLink).toBeVisible();
|
||
await expect(homeLink).toContainText('返回首页');
|
||
|
||
await Promise.all([
|
||
page.waitForURL((url) => new URL(url).pathname === '/', { waitUntil: 'commit', timeout: 15000 }),
|
||
homeLink.click(),
|
||
]);
|
||
expect(new URL(page.url()).pathname).toBe('/');
|
||
});
|
||
});
|
||
|
||
// ==================== 3. 加载状态测试 ====================
|
||
test.describe('加载状态 - 页面过渡', { tag: '@regression' }, () => {
|
||
|
||
test('页面加载时显示主要内容', async ({ page }) => {
|
||
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||
|
||
// 页面加载后 header 应可见
|
||
await expect(page.locator('header')).toBeVisible({ timeout: 10000 });
|
||
|
||
// main 内容应存在
|
||
const mainContent = page.locator('main, [role="main"]').first();
|
||
await expect(mainContent).toBeVisible({ timeout: 10000 });
|
||
});
|
||
|
||
test('页面导航不应完全空白', async ({ page }) => {
|
||
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
// N-27 修复:原守卫 `if (count > 0 && isVisible)` 依赖 header 巨型下拉里的
|
||
// `a[href="/products"]`(未 hover 时隐藏 ⇒ 断言常被跳过)。
|
||
// 改用主内容区恒定渲染的叙事区行动链接(home-content-v15 NARRATIVE_ACTS 第三幕,
|
||
// p2-functional-e2e 亦以同选择器作硬断言),导航与内容都变硬断言。
|
||
const productLink = page
|
||
.locator('[data-testid="narrative-section"] a[href="/products"]')
|
||
.first();
|
||
await expect(productLink).toBeVisible();
|
||
|
||
await productLink.click();
|
||
await expect(page).toHaveURL((url) => url.pathname === '/products');
|
||
|
||
// 导航完成后页面仍应包含实际内容
|
||
const bodyText = await page.locator('body').textContent();
|
||
expect(bodyText!.length).toBeGreaterThan(50);
|
||
});
|
||
|
||
test('刷新页面后内容仍然加载', async ({ page }) => {
|
||
await page.goto('/products', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 刷新页面
|
||
await page.reload({ waitUntil: 'domcontentloaded', timeout: 30000 });
|
||
await page.waitForTimeout(1500);
|
||
|
||
// 页面应仍然加载成功
|
||
const mainContent = page.locator('main, [role="main"]').first();
|
||
await expect(mainContent).toBeVisible();
|
||
});
|
||
});
|
||
|
||
// ==================== 4. 无效路由与特殊字符测试 ====================
|
||
test.describe('无效路由 - 边界 URL 处理', { tag: '@regression' }, () => {
|
||
|
||
test('带特殊字符的路由不崩溃', async ({ page }) => {
|
||
const specialPaths = [
|
||
'/products/<script>alert(1)</script>',
|
||
'/about?param=<invalid>',
|
||
'/contact#%20%20',
|
||
];
|
||
|
||
for (const path of specialPaths) {
|
||
await page.goto(path, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
// 页面不应崩溃,body 应存在
|
||
const body = page.locator('body').first();
|
||
await expect(body).toBeVisible();
|
||
}
|
||
});
|
||
|
||
test('过长路径不导致页面异常', async ({ page }) => {
|
||
const longPath = '/a'.repeat(200);
|
||
await page.goto(longPath, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||
await page.waitForTimeout(1500);
|
||
|
||
// 页面应正常渲染(404 页面或正常页面)
|
||
const body = page.locator('body').first();
|
||
await expect(body).toBeVisible();
|
||
|
||
const bodyText = await body.textContent();
|
||
expect(bodyText!.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('查询参数不影响页面渲染', async ({ page }) => {
|
||
await page.goto('/?utm_source=test&utm_medium=email', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||
await page.waitForTimeout(1500);
|
||
|
||
// 首页应正常渲染
|
||
const header = page.locator('header').first();
|
||
await expect(header).toBeVisible();
|
||
});
|
||
});
|