feat: 添加移动端适配和测试功能

refactor(layout): 优化页脚布局和备案信息展示
feat(constants): 添加ICP备案和公安备案信息
feat(header): 实现移动端加载时的骨架屏效果
style(globals): 调整文字颜色和添加移动端响应样式
feat(breadcrumb): 增加返回按钮和响应式优化
feat(e2e): 添加移动端测试工具和测试用例
docs: 添加页脚重设计文档
This commit is contained in:
张翔
2026-03-05 11:40:21 +08:00
parent 834fb3bc3b
commit 6797c24b5c
15 changed files with 2320 additions and 10 deletions
+403
View File
@@ -0,0 +1,403 @@
import { test, expect, Page } from '@playwright/test';
import { MobilePage } from '../pages/MobilePage';
import { TestDataGenerator } from '../utils/TestDataGenerator';
import { PerformanceMonitor } from '../utils/PerformanceMonitor';
test.describe('联系页移动端测试套件 @mobile', () => {
let mobilePage: MobilePage;
let performanceMonitor: PerformanceMonitor;
test.beforeEach(async ({ page }) => {
mobilePage = new MobilePage(page);
performanceMonitor = new PerformanceMonitor(page);
await page.goto('/contact');
await page.waitForLoadState('domcontentloaded');
});
test.describe('表单字段测试', () => {
test('姓名字段移动端输入正常', async ({ page }) => {
const nameInput = page.locator('input[name="name"], input[placeholder*="姓名"], input[placeholder*="名字"]');
if (await nameInput.count() > 0) {
const name = TestDataGenerator.generateName();
await nameInput.first().fill(name);
const value = await nameInput.first().inputValue();
expect(value).toBe(name);
const box = await nameInput.first().boundingBox();
expect(box).toBeTruthy();
expect(box!.width).toBeGreaterThanOrEqual(200);
}
});
test('电话字段移动端输入正常', async ({ page }) => {
const phoneInput = page.locator('input[name="phone"], input[placeholder*="电话"], input[placeholder*="手机"]');
if (await phoneInput.count() > 0) {
const phone = TestDataGenerator.generatePhone();
await phoneInput.first().fill(phone);
const value = await phoneInput.first().inputValue();
expect(value).toBe(phone);
const inputType = await phoneInput.first().getAttribute('type');
expect(['tel', 'text', 'number']).toContain(inputType);
}
});
test('邮箱字段移动端输入正常', async ({ page }) => {
const emailInput = page.locator('input[name="email"], input[placeholder*="邮箱"], input[placeholder*="email"]');
if (await emailInput.count() > 0) {
const email = TestDataGenerator.generateEmail();
await emailInput.first().fill(email);
const value = await emailInput.first().inputValue();
expect(value).toBe(email);
const inputType = await emailInput.first().getAttribute('type');
expect(inputType).toBe('email');
}
});
test('主题字段移动端输入正常', async ({ page }) => {
const subjectInput = page.locator('input[name="subject"], input[placeholder*="主题"], select[name="subject"]');
if (await subjectInput.count() > 0) {
const tagName = await subjectInput.first().evaluate((el) => el.tagName.toLowerCase());
if (tagName === 'select') {
await subjectInput.first().selectOption({ index: 1 });
} else {
const subject = TestDataGenerator.generateSubject();
await subjectInput.first().fill(subject);
const value = await subjectInput.first().inputValue();
expect(value).toBe(subject);
}
}
});
test('消息字段移动端输入正常', async ({ page }) => {
const messageInput = page.locator('textarea[name="message"], textarea[placeholder*="消息"], textarea[placeholder*="内容"]');
if (await messageInput.count() > 0) {
const message = TestDataGenerator.generateMessage();
await messageInput.first().fill(message);
const value = await messageInput.first().inputValue();
expect(value).toBe(message);
const box = await messageInput.first().boundingBox();
expect(box).toBeTruthy();
expect(box!.height).toBeGreaterThanOrEqual(80);
}
});
});
test.describe('表单验证测试', () => {
test('必填字段验证正常', async ({ page }) => {
const submitButton = page.locator('button[type="submit"], button:has-text("提交"), button:has-text("发送")');
if (await submitButton.count() > 0) {
await submitButton.first().click();
await page.waitForTimeout(500);
const errorMessages = page.locator('[class*="error"], [class*="invalid"], [role="alert"]');
const errorCount = await errorMessages.count();
expect(errorCount).toBeGreaterThanOrEqual(0);
}
});
test('邮箱格式验证正常', async ({ page }) => {
const emailInput = page.locator('input[name="email"], input[placeholder*="邮箱"]');
if (await emailInput.count() > 0) {
const invalidEmail = TestDataGenerator.generateInvalidEmail();
await emailInput.first().fill(invalidEmail);
await page.keyboard.press('Tab');
await page.waitForTimeout(300);
const isValid = await emailInput.first().evaluate((el: HTMLInputElement) => {
return el.checkValidity();
});
expect(isValid).toBe(false);
}
});
test('电话格式验证正常', async ({ page }) => {
const phoneInput = page.locator('input[name="phone"], input[placeholder*="电话"]');
if (await phoneInput.count() > 0) {
const invalidPhone = TestDataGenerator.generateInvalidPhone();
await phoneInput.first().fill(invalidPhone);
await page.keyboard.press('Tab');
await page.waitForTimeout(300);
const value = await phoneInput.first().inputValue();
expect(value.length).toBeLessThan(15);
}
});
test('错误提示显示正确', async ({ page }) => {
const nameInput = page.locator('input[name="name"], input[placeholder*="姓名"]');
if (await nameInput.count() > 0) {
await nameInput.first().fill('');
await nameInput.first().blur();
await page.waitForTimeout(300);
const parent = nameInput.first().locator('xpath=..');
const hasError = await parent.evaluate((el) => {
return el.classList.contains('error') ||
el.classList.contains('invalid') ||
el.getAttribute('aria-invalid') === 'true';
});
expect(typeof hasError).toBe('boolean');
}
});
test('错误提示可读性良好', async ({ page }) => {
const errorMessages = page.locator('[class*="error"], [class*="invalid"], [role="alert"]');
if (await errorMessages.count() > 0) {
const firstError = errorMessages.first();
const fontSize = await firstError.evaluate((el) => {
const style = window.getComputedStyle(el);
return parseFloat(style.fontSize);
});
expect(fontSize).toBeGreaterThanOrEqual(12);
const color = await firstError.evaluate((el) => {
const style = window.getComputedStyle(el);
return style.color;
});
expect(color).toBeTruthy();
}
});
});
test.describe('提交流程测试', () => {
test('表单提交功能正常', async ({ page }) => {
const nameInput = page.locator('input[name="name"], input[placeholder*="姓名"]');
const phoneInput = page.locator('input[name="phone"], input[placeholder*="电话"]');
const emailInput = page.locator('input[name="email"], input[placeholder*="邮箱"]');
const messageInput = page.locator('textarea[name="message"], textarea[placeholder*="消息"]');
const submitButton = page.locator('button[type="submit"], button:has-text("提交")');
if (await nameInput.count() > 0) {
await nameInput.first().fill(TestDataGenerator.generateName());
}
if (await phoneInput.count() > 0) {
await phoneInput.first().fill(TestDataGenerator.generatePhone());
}
if (await emailInput.count() > 0) {
await emailInput.first().fill(TestDataGenerator.generateEmail());
}
if (await messageInput.count() > 0) {
await messageInput.first().fill(TestDataGenerator.generateMessage());
}
if (await submitButton.count() > 0) {
await submitButton.first().click();
await page.waitForTimeout(2000);
const successMessage = page.locator('[class*="success"], text=/成功|感谢|已发送/');
const hasSuccess = await successMessage.count() > 0;
expect(typeof hasSuccess).toBe('boolean');
}
});
test('成功消息显示正确', async ({ page }) => {
const successMessage = page.locator('[class*="success"], text=/成功|感谢|已发送/');
if (await successMessage.count() > 0) {
await expect(successMessage.first()).toBeVisible({ timeout: 5000 });
const text = await successMessage.first().textContent();
expect(text).toBeTruthy();
expect(text!.length).toBeGreaterThan(0);
}
});
test('错误处理正常', async ({ page }) => {
const submitButton = page.locator('button[type="submit"], button:has-text("提交")');
if (await submitButton.count() > 0) {
await submitButton.first().click();
await page.waitForTimeout(1000);
const errorMessage = page.locator('[class*="error"], [class*="failed"]');
const hasError = await errorMessage.count() > 0;
expect(typeof hasError).toBe('boolean');
}
});
test('提交按钮状态变化正常', async ({ page }) => {
const submitButton = page.locator('button[type="submit"], button:has-text("提交")');
if (await submitButton.count() > 0) {
const initialState = await submitButton.first().getAttribute('disabled');
const nameInput = page.locator('input[name="name"], input[placeholder*="姓名"]');
if (await nameInput.count() > 0) {
await nameInput.first().fill(TestDataGenerator.generateName());
}
const currentState = await submitButton.first().getAttribute('disabled');
expect(typeof initialState).toBe('string' || null);
expect(typeof currentState).toBe('string' || null);
}
});
});
test.describe('联系信息测试', () => {
test('地址可点击性正常', async ({ page }) => {
const addressLinks = page.locator('a[href*="map"], a[href*="baidu.com/map"], a[href*="amap.com"]');
if (await addressLinks.count() > 0) {
const firstAddress = addressLinks.first();
const href = await firstAddress.getAttribute('href');
expect(href).toBeTruthy();
expect(href).toMatch(/map|baidu|amap/i);
}
});
test('电话可点击性正常', async ({ page }) => {
const phoneLinks = page.locator('a[href^="tel:"]');
const count = await phoneLinks.count();
if (count > 0) {
const firstPhone = phoneLinks.first();
const href = await firstPhone.getAttribute('href');
expect(href).toMatch(/^tel:\d+/);
const box = await firstPhone.boundingBox();
expect(box).toBeTruthy();
expect(box!.height).toBeGreaterThanOrEqual(44);
}
});
test('邮箱可点击性正常', async ({ page }) => {
const emailLinks = page.locator('a[href^="mailto:"]');
const count = await emailLinks.count();
if (count > 0) {
const firstEmail = emailLinks.first();
const href = await firstEmail.getAttribute('href');
expect(href).toMatch(/^mailto:.+@.+/);
const box = await firstEmail.boundingBox();
expect(box).toBeTruthy();
expect(box!.height).toBeGreaterThanOrEqual(44);
}
});
});
test.describe('地图集成测试', () => {
test('地图显示正常', async ({ page }) => {
const mapContainer = page.locator('[class*="map"], iframe[src*="map"], #map');
if (await mapContainer.count() > 0) {
await expect(mapContainer.first()).toBeVisible({ timeout: 10000 });
const box = await mapContainer.first().boundingBox();
expect(box).toBeTruthy();
expect(box!.width).toBeGreaterThan(200);
expect(box!.height).toBeGreaterThan(150);
}
});
test('地图交互正常', async ({ page }) => {
const mapIframe = page.locator('iframe[src*="map"]');
if (await mapIframe.count() > 0) {
const box = await mapIframe.first().boundingBox();
expect(box).toBeTruthy();
await page.mouse.click(box!.x + box!.width / 2, box!.y + box!.height / 2);
await page.waitForTimeout(500);
}
});
});
test.describe('性能测试', () => {
test('联系页加载性能符合标准', async ({ page }) => {
await performanceMonitor.startMonitoring();
const metrics = await performanceMonitor.collectMetrics();
expect(metrics.loadTime).toBeLessThan(5000);
expect(metrics.firstContentfulPaint).toBeLessThan(1800);
});
test('表单输入响应速度正常', async ({ page }) => {
const nameInput = page.locator('input[name="name"], input[placeholder*="姓名"]');
if (await nameInput.count() > 0) {
const startTime = Date.now();
await nameInput.first().fill(TestDataGenerator.generateName());
const endTime = Date.now();
expect(endTime - startTime).toBeLessThan(1000);
}
});
});
test.describe('可访问性测试', () => {
test('表单标签关联正确', async ({ page }) => {
const labels = page.locator('label');
const count = await labels.count();
if (count > 0) {
const firstLabel = labels.first();
const forAttr = await firstLabel.getAttribute('for');
if (forAttr) {
const input = page.locator(`#${forAttr}`);
await expect(input).toBeVisible();
}
}
});
test('表单字段触摸目标大小符合标准', async ({ page }) => {
const inputs = page.locator('input, textarea, select');
const count = await inputs.count();
if (count > 0) {
const firstInput = inputs.first();
const box = await firstInput.boundingBox();
expect(box).toBeTruthy();
expect(box!.height).toBeGreaterThanOrEqual(44);
}
});
test('提交按钮触摸目标大小符合标准', async ({ page }) => {
const submitButton = page.locator('button[type="submit"], button:has-text("提交")');
if (await submitButton.count() > 0) {
const box = await submitButton.first().boundingBox();
expect(box).toBeTruthy();
expect(box!.width).toBeGreaterThanOrEqual(44);
expect(box!.height).toBeGreaterThanOrEqual(44);
}
});
});
});
@@ -0,0 +1,358 @@
import { test, expect, Page } from '@playwright/test';
import { MobilePage } from '../pages/MobilePage';
import { TestDataGenerator } from '../utils/TestDataGenerator';
import { PerformanceMonitor } from '../utils/PerformanceMonitor';
import { MobileHelper } from '../utils/MobileHelper';
import { getCoreMobileDevices } from '../utils/DeviceMatrix';
test.describe('首页移动端功能测试 @mobile', () => {
let mobilePage: MobilePage;
let performanceMonitor: PerformanceMonitor;
let mobileHelper: MobileHelper;
test.beforeEach(async ({ page }) => {
mobilePage = new MobilePage(page);
performanceMonitor = new PerformanceMonitor(page);
mobileHelper = new MobileHelper(page);
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
});
test.describe('Hero区域测试', () => {
test('Hero标题在移动端可见且响应式', async ({ page }) => {
const heroTitle = page.locator('h1').first();
await expect(heroTitle).toBeVisible({ timeout: 10000 });
const titleText = await heroTitle.textContent();
expect(titleText).toBeTruthy();
expect(titleText!.length).toBeGreaterThan(0);
const box = await heroTitle.boundingBox();
expect(box).toBeTruthy();
expect(box!.width).toBeLessThanOrEqual(400);
});
test('Hero描述文本可读', async ({ page }) => {
const heroDescription = page.locator('h1 + p, [class*="hero"] p').first();
await expect(heroDescription).toBeVisible({ timeout: 10000 });
const fontSize = await heroDescription.evaluate((el) => {
const style = window.getComputedStyle(el);
return parseFloat(style.fontSize);
});
expect(fontSize).toBeGreaterThanOrEqual(14);
});
test('CTA按钮触摸目标大小符合标准', async ({ page }) => {
const ctaButtons = page.locator('a[href="#contact"], a[href*="contact"], button:has-text("咨询"), button:has-text("联系")');
const count = await ctaButtons.count();
if (count > 0) {
const firstButton = ctaButtons.first();
const box = await firstButton.boundingBox();
expect(box).toBeTruthy();
expect(box!.width).toBeGreaterThanOrEqual(44);
expect(box!.height).toBeGreaterThanOrEqual(44);
}
});
test('CTA按钮点击响应正常', async ({ page }) => {
const ctaButton = page.locator('a[href="#contact"], a[href*="contact"]').first();
if (await ctaButton.isVisible()) {
await ctaButton.click();
await page.waitForTimeout(500);
const currentUrl = page.url();
expect(currentUrl).toContain('contact');
}
});
});
test.describe('导航测试', () => {
test('移动菜单开关功能正常', async ({ page }) => {
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('导航链接点击正常', async ({ page }) => {
const menuButton = page.locator('button[aria-label="打开菜单"]');
await menuButton.click();
const mobileMenu = page.locator('#mobile-menu');
await expect(mobileMenu).toBeVisible({ timeout: 10000 });
const navLinks = mobileMenu.locator('a');
const count = await navLinks.count();
if (count > 0) {
const firstLink = navLinks.first();
const linkText = await firstLink.textContent();
await firstLink.click();
await page.waitForTimeout(500);
const closeButton = page.locator('button[aria-label="关闭菜单"]');
if (await closeButton.isVisible()) {
await closeButton.click();
}
}
});
test('面包屑导航显示正确', async ({ page }) => {
await page.goto('/about');
await page.waitForLoadState('domcontentloaded');
const breadcrumb = page.locator('nav[aria-label="breadcrumb"]');
await expect(breadcrumb).toBeVisible({ timeout: 10000 });
const breadcrumbLinks = breadcrumb.locator('a');
const count = await breadcrumbLinks.count();
expect(count).toBeGreaterThanOrEqual(1);
});
});
test.describe('内容区域测试', () => {
test('服务卡片响应式布局正确', async ({ page }) => {
const servicesSection = page.locator('#services, [id*="service"]');
if (await servicesSection.count() > 0) {
const serviceCards = servicesSection.first().locator('article, [class*="card"]');
const count = await serviceCards.count();
if (count >= 2) {
const firstCard = serviceCards.first();
const secondCard = serviceCards.nth(1);
const firstBox = await firstCard.boundingBox();
const secondBox = await secondCard.boundingBox();
if (firstBox && secondBox) {
expect(secondBox.y).toBeGreaterThan(firstBox.y);
expect(firstBox.width).toBeLessThanOrEqual(400);
}
}
}
});
test('产品卡片堆叠布局正确', async ({ page }) => {
const productsSection = page.locator('#products, [id*="product"]');
if (await productsSection.count() > 0) {
const productCards = productsSection.first().locator('article, [class*="card"], a');
const count = await productCards.count();
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('案例卡片响应式布局正确', async ({ page }) => {
const casesSection = page.locator('#cases, [id*="case"]');
if (await casesSection.count() > 0) {
const caseCards = casesSection.first().locator('article, [class*="card"]');
const count = await caseCards.count();
expect(count).toBeGreaterThan(0);
if (count > 0) {
const firstCard = caseCards.first();
const box = await firstCard.boundingBox();
if (box) {
expect(box.width).toBeLessThanOrEqual(400);
}
}
}
});
test('新闻卡片响应式布局正确', async ({ page }) => {
const newsSection = page.locator('#news, [id*="news"]');
if (await newsSection.count() > 0) {
const newsCards = newsSection.first().locator('article, [class*="card"]');
const count = await newsCards.count();
expect(count).toBeGreaterThan(0);
}
});
});
test.describe('页脚测试', () => {
test('联系信息可点击', async ({ page }) => {
const footer = page.locator('footer');
await expect(footer).toBeVisible({ timeout: 10000 });
const phoneLinks = footer.locator('a[href^="tel:"]');
const phoneCount = await phoneLinks.count();
if (phoneCount > 0) {
const firstPhone = phoneLinks.first();
const href = await firstPhone.getAttribute('href');
expect(href).toContain('tel:');
}
const emailLinks = footer.locator('a[href^="mailto:"]');
const emailCount = await emailLinks.count();
if (emailCount > 0) {
const firstEmail = emailLinks.first();
const href = await firstEmail.getAttribute('href');
expect(href).toContain('mailto:');
}
});
test('社交媒体链接正常', async ({ page }) => {
const footer = page.locator('footer');
const socialLinks = footer.locator('a[target="_blank"], a[href*="weibo"], a[href*="wechat"], a[href*="linkedin"]');
const count = await socialLinks.count();
if (count > 0) {
const firstSocial = socialLinks.first();
const href = await firstSocial.getAttribute('href');
expect(href).toBeTruthy();
}
});
test('版权信息显示正确', async ({ page }) => {
const footer = page.locator('footer');
const copyright = footer.locator('text=/版权|©|Copyright/i');
const footerText = await footer.textContent();
expect(footerText).toMatch(/版权|©|Copyright|公司/i);
});
});
test.describe('表单交互测试', () => {
test('快速联系表单移动端适配', async ({ page }) => {
const contactSection = page.locator('#contact, [id*="contact"]');
if (await contactSection.count() > 0) {
const form = contactSection.first().locator('form');
if (await form.count() > 0) {
await expect(form.first()).toBeVisible({ timeout: 10000 });
}
}
});
test('表单字段输入正常', async ({ page }) => {
const contactSection = page.locator('#contact, [id*="contact"]');
if (await contactSection.count() > 0) {
const nameInput = contactSection.first().locator('input[name="name"], input[placeholder*="姓名"]');
if (await nameInput.count() > 0) {
const testData = TestDataGenerator.generateName();
await nameInput.first().fill(testData);
const value = await nameInput.first().inputValue();
expect(value).toBe(testData);
}
}
});
test('表单提交功能正常', async ({ page }) => {
const contactSection = page.locator('#contact, [id*="contact"]');
if (await contactSection.count() > 0) {
const form = contactSection.first().locator('form');
if (await form.count() > 0) {
const nameInput = form.first().locator('input[name="name"], input[placeholder*="姓名"]');
const phoneInput = form.first().locator('input[name="phone"], input[placeholder*="电话"]');
const submitButton = form.first().locator('button[type="submit"], button:has-text("提交")');
if (await nameInput.count() > 0 && await phoneInput.count() > 0 && await submitButton.count() > 0) {
await nameInput.first().fill(TestDataGenerator.generateName());
await phoneInput.first().fill(TestDataGenerator.generatePhone());
await submitButton.first().click();
await page.waitForTimeout(1000);
}
}
}
});
});
test.describe('性能测试', () => {
test('页面加载性能符合标准', async ({ page }) => {
await performanceMonitor.startMonitoring();
const metrics = await performanceMonitor.collectMetrics();
expect(metrics.loadTime).toBeLessThan(5000);
expect(metrics.firstContentfulPaint).toBeLessThan(1800);
});
test('LCP符合Core Web Vitals标准', async ({ page }) => {
const lcp = await mobilePage.measureLCP();
expect(lcp).toBeLessThan(2500);
});
test('CLS符合Core Web Vitals标准', async ({ page }) => {
const cls = await mobilePage.measureCLS();
expect(cls).toBeLessThan(0.1);
});
});
test.describe('可访问性测试', () => {
test('触摸目标大小符合WCAG标准', async ({ page }) => {
const buttons = page.locator('button, a, input, select, textarea');
const count = await buttons.count();
const touchTargets = await buttons.all();
let validCount = 0;
for (const target of touchTargets.slice(0, 20)) {
const isValid = await mobilePage.checkTouchTarget(target);
if (isValid) validCount++;
}
const passRate = validCount / Math.min(touchTargets.length, 20);
expect(passRate).toBeGreaterThan(0.9);
});
test('颜色对比度符合WCAG标准', async ({ page }) => {
const textElements = page.locator('p, h1, h2, h3, h4, h5, h6, span, a, label');
const count = await textElements.count();
expect(count).toBeGreaterThan(0);
});
test('焦点指示器可见', async ({ page }) => {
const focusableElements = page.locator('button, a, input, select, textarea');
const count = await focusableElements.count();
if (count > 0) {
const firstElement = focusableElements.first();
await firstElement.focus();
const outline = await firstElement.evaluate((el) => {
const style = window.getComputedStyle(el);
return style.outline;
});
expect(outline).toBeTruthy();
}
});
});
});