feat: 添加面包屑导航组件并优化页面布局

refactor: 重构页面结构和导航逻辑

fix: 修复移动端菜单导航和滚动行为

perf: 优化图片加载性能和资源请求

test: 添加端到端测试和性能测试用例

docs: 更新.gitignore文件

chore: 更新依赖和配置

style: 优化代码格式和类型安全

ci: 调整Playwright测试超时时间

build: 更新Next.js配置和构建选项
This commit is contained in:
张翔
2026-02-28 09:09:04 +08:00
parent 9d01e0982f
commit 9451814ca4
60 changed files with 4078 additions and 148 deletions
+205
View File
@@ -0,0 +1,205 @@
import { test, expect } from '../../fixtures/base.fixture';
test.describe('Mobile UX Tests', () => {
test.use({ viewport: { width: 375, height: 667 } });
test('Mobile menu opens and closes correctly', async ({ page }) => {
test.setTimeout(120000);
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
const menuButton = page.locator('button[aria-label="打开菜单"], button[aria-label="关闭菜单"]');
await expect(menuButton).toBeVisible({ timeout: 10000 });
await menuButton.click();
const mobileMenu = page.locator('#mobile-menu');
await expect(mobileMenu).toBeVisible({ timeout: 10000 });
const closeButton = page.locator('button[aria-label="关闭菜单"]');
await closeButton.click();
await expect(mobileMenu).not.toBeVisible({ timeout: 10000 });
});
test('Mobile menu navigation works', async ({ page }) => {
test.setTimeout(120000);
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
const menuButton = page.locator('button[aria-label="打开菜单"]');
await expect(menuButton).toBeVisible({ timeout: 10000 });
await menuButton.click();
const mobileMenu = page.locator('#mobile-menu');
await expect(mobileMenu).toBeVisible({ timeout: 10000 });
await page.getByRole('link', { name: '关于我们' }).first().click();
await expect(page).toHaveURL(/.*about.*/, { timeout: 30000 });
});
test('Mobile menu closes on outside click', async ({ page }) => {
test.setTimeout(120000);
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
const menuButton = page.locator('button[aria-label="打开菜单"]');
await expect(menuButton).toBeVisible({ timeout: 10000 });
await menuButton.click();
const mobileMenu = page.locator('#mobile-menu');
await expect(mobileMenu).toBeVisible({ timeout: 10000 });
await page.keyboard.press('Escape');
await expect(mobileMenu).not.toBeVisible({ timeout: 10000 });
});
test('Mobile viewport renders correctly', async ({ page }) => {
test.setTimeout(120000);
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
const header = page.locator('header');
await expect(header).toBeVisible({ timeout: 10000 });
const desktopNav = page.locator('nav.hidden.md\\:flex');
await expect(desktopNav).not.toBeVisible();
const menuButton = page.locator('button[aria-label="打开菜单"]');
await expect(menuButton).toBeVisible({ timeout: 10000 });
});
test('Touch targets are appropriately sized', async ({ page }) => {
test.setTimeout(120000);
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
const menuButton = page.locator('button[aria-label="打开菜单"]');
await expect(menuButton).toBeVisible({ timeout: 10000 });
await menuButton.click();
const mobileMenu = page.locator('#mobile-menu');
await expect(mobileMenu).toBeVisible({ timeout: 10000 });
const links = await mobileMenu.locator('a').all();
for (const link of links) {
const box = await link.boundingBox();
if (box) {
expect(box.height).toBeGreaterThanOrEqual(44);
}
}
});
test('Mobile page scrolls smoothly', async ({ page }) => {
test.setTimeout(120000);
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
const scrollY = await page.evaluate(() => window.scrollY);
expect(scrollY).toBe(0);
await page.evaluate(() => {
window.scrollTo({ top: 500, behavior: 'smooth' });
});
await page.waitForTimeout(500);
const newScrollY = await page.evaluate(() => window.scrollY);
expect(newScrollY).toBeGreaterThan(0);
});
test('Mobile images are responsive', async ({ page }) => {
test.setTimeout(120000);
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
const images = await page.locator('img').all();
for (const image of images) {
const box = await image.boundingBox();
if (box) {
expect(box.width).toBeLessThanOrEqual(400);
}
}
});
test('Mobile text is readable', async ({ page }) => {
test.setTimeout(120000);
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
const textElements = await page.locator('p, h1, h2, h3, h4, h5, h6').all();
for (const element of textElements.slice(0, 10)) {
const fontSize = await element.evaluate((el) => {
const style = window.getComputedStyle(el);
return parseFloat(style.fontSize);
});
expect(fontSize).toBeGreaterThanOrEqual(14);
}
});
test('Mobile About page renders correctly', async ({ page }) => {
test.setTimeout(120000);
await page.goto('/about');
await page.waitForLoadState('domcontentloaded');
const header = page.locator('header');
await expect(header).toBeVisible({ timeout: 10000 });
const breadcrumb = page.locator('nav[aria-label="breadcrumb"]');
await expect(breadcrumb).toBeVisible({ timeout: 10000 });
});
test('Mobile Products page cards stack vertically', async ({ page }) => {
test.setTimeout(120000);
await page.goto('/products');
await page.waitForLoadState('domcontentloaded');
const productCards = page.locator('a[href^="/products/"]');
const count = await productCards.count();
expect(count).toBeGreaterThan(0);
if (count >= 2) {
const firstCard = productCards.first();
const secondCard = productCards.nth(1);
const firstBox = await firstCard.boundingBox();
const secondBox = await secondCard.boundingBox();
if (firstBox && secondBox) {
expect(secondBox.y).toBeGreaterThan(firstBox.y);
}
}
});
test('Mobile Contact page form is usable', async ({ page }) => {
test.setTimeout(120000);
await page.goto('/contact');
await page.waitForLoadState('domcontentloaded');
const nameInput = page.locator('input[name="name"], input[placeholder*="姓名"], input[placeholder*="名字"]');
if (await nameInput.count() > 0) {
await expect(nameInput.first()).toBeVisible({ timeout: 10000 });
}
const submitButton = page.locator('button[type="submit"], button:has-text("提交"), button:has-text("发送")');
if (await submitButton.count() > 0) {
await expect(submitButton.first()).toBeVisible({ timeout: 10000 });
}
});
test('Mobile keyboard navigation works', async ({ page }) => {
test.setTimeout(120000);
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
const focusedElement = page.locator(':focus');
await expect(focusedElement).toBeVisible({ timeout: 10000 });
});
});