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
+138
View File
@@ -450,4 +450,142 @@ export class BasePage {
});
});
}
async tapElement(selector: string): Promise<void> {
const element = this.page.locator(selector);
const box = await element.boundingBox();
if (box) {
const x = box.x + box.width / 2;
const y = box.y + box.height / 2;
await this.page.touchscreen.tap(x, y);
} else {
await element.click();
}
}
async swipe(start: { x: number; y: number }, end: { x: number; y: number }): Promise<void> {
await this.page.touchscreen.tap(start.x, start.y);
await this.page.touchscreen.touchMove(end.x, end.y);
await this.page.touchscreen.touchEnd();
}
async longPress(selector: string, duration: number = 1000): Promise<void> {
const element = this.page.locator(selector);
const box = await element.boundingBox();
if (box) {
const x = box.x + box.width / 2;
const y = box.y + box.height / 2;
await this.page.touchscreen.tap(x, y);
await this.page.waitForTimeout(duration);
} else {
await element.click();
}
}
async pinchZoom(selector: string, scale: number = 1.5): Promise<void> {
const element = this.page.locator(selector);
const box = await element.boundingBox();
if (box) {
const centerX = box.x + box.width / 2;
const centerY = box.y + box.height / 2;
const finger1 = { x: centerX - 50, y: centerY };
const finger2 = { x: centerX + 50, y: centerY };
await this.page.touchscreen.tap(finger1.x, finger1.y);
await this.page.touchscreen.tap(finger2.x, finger2.y);
const newFinger1 = { x: centerX - 50 / scale, y: centerY };
const newFinger2 = { x: centerX + 50 / scale, y: centerY };
await this.page.touchscreen.touchMove(newFinger1.x, newFinger1.y);
await this.page.touchscreen.touchMove(newFinger2.x, newFinger2.y);
await this.page.touchscreen.touchEnd();
await this.page.touchscreen.touchEnd();
}
}
async waitForMobileLoad(): Promise<void> {
await this.page.waitForLoadState('domcontentloaded');
await this.page.waitForTimeout(500);
}
async checkTouchTarget(selector: string): Promise<boolean> {
const element = this.page.locator(selector);
const box = await element.boundingBox();
if (box) {
return box.width >= 44 && box.height >= 44;
}
return false;
}
async captureMobileScreenshot(name: string): Promise<void> {
const screenshotDir = 'test-results/mobile-screenshots';
if (!fs.existsSync(screenshotDir)) {
fs.mkdirSync(screenshotDir, { recursive: true });
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${timestamp}-${name}.png`;
await this.page.screenshot({
path: path.join(screenshotDir, filename),
fullPage: true
});
}
async measureLCP(): Promise<number> {
const vitals = await this.getCoreWebVitals();
return vitals.largestContentfulPaint;
}
async measureFID(): Promise<number> {
const vitals = await this.getCoreWebVitals();
return vitals.firstInputDelay;
}
async measureCLS(): Promise<number> {
const vitals = await this.getCoreWebVitals();
return vitals.cumulativeLayoutShift;
}
async measureTTI(): Promise<number> {
const metrics = await this.measurePerformance();
return metrics.domContentLoaded;
}
async measureTTFB(): Promise<number> {
const timing = await this.page.evaluate(() => {
return performance.timing;
});
return timing.responseStart - timing.navigationStart;
}
async handleError(error: Error, context: string): Promise<void> {
const errorInfo = {
timestamp: new Date().toISOString(),
context,
message: error.message,
stack: error.stack,
url: this.page.url(),
};
await this.log(`Error in ${context}: ${error.message}`, 'error');
await this.captureMobileScreenshot(`error-${context}`);
}
async logAction(action: string, details: any): Promise<void> {
const logInfo = {
timestamp: new Date().toISOString(),
action,
details,
url: this.page.url(),
};
await this.log(`Action: ${action}`, 'info');
}
}
+128
View File
@@ -0,0 +1,128 @@
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';
export class MobilePage extends BasePage {
constructor(page: Page) {
super(page);
}
async handleMobileMenu(): Promise<void> {
const menuButton = this.page.locator('button[aria-label="打开菜单"], button[aria-label="关闭菜单"]');
if (await menuButton.isVisible()) {
await this.tapElement('button[aria-label="打开菜单"], button[aria-label="关闭菜单"]');
await this.page.waitForSelector('#mobile-menu', { state: 'visible' });
}
}
async closeMobileMenu(): Promise<void> {
const closeButton = this.page.locator('button[aria-label="关闭菜单"]');
if (await closeButton.isVisible()) {
await this.tapElement('button[aria-label="关闭菜单"]');
await this.page.waitForSelector('#mobile-menu', { state: 'hidden' });
}
}
async handleMobileNavigation(linkText: string): Promise<void> {
await this.handleMobileMenu();
const link = this.page.locator(`#mobile-menu a:has-text("${linkText}")`);
await this.tapElement(`#mobile-menu a:has-text("${linkText}")`);
await this.closeMobileMenu();
}
async handleMobileForm(formData: Record<string, string>): Promise<void> {
for (const [fieldName, value] of Object.entries(formData)) {
const field = this.page.locator(`input[name="${fieldName}"], textarea[name="${fieldName}"]`);
if (await field.isVisible()) {
await field.fill(value);
await this.page.waitForTimeout(100);
}
}
}
async handleMobileScroll(direction: 'up' | 'down' = 'down'): Promise<void> {
const viewportSize = this.page.viewportSize();
if (viewportSize) {
const startY = direction === 'down' ? viewportSize.height * 0.8 : viewportSize.height * 0.2;
const endY = direction === 'down' ? viewportSize.height * 0.2 : viewportSize.height * 0.8;
const centerX = viewportSize.width / 2;
await this.swipe(
{ x: centerX, y: startY },
{ x: centerX, y: endY }
);
await this.page.waitForTimeout(500);
}
}
async handleMobileGestures(): Promise<void> {
await this.logAction('Testing mobile gestures', {});
const testElement = this.page.locator('body');
const box = await testElement.boundingBox();
if (box) {
const centerX = box.x + box.width / 2;
const centerY = box.y + box.height / 2;
await this.swipe(
{ x: centerX, y: centerY + 100 },
{ x: centerX, y: centerY - 100 }
);
await this.page.waitForTimeout(500);
await this.swipe(
{ x: centerX - 100, y: centerY },
{ x: centerX + 100, y: centerY }
);
await this.page.waitForTimeout(500);
}
}
async verifyMobileMenuOpen(): Promise<boolean> {
const mobileMenu = this.page.locator('#mobile-menu');
return await mobileMenu.isVisible();
}
async verifyMobileMenuClosed(): Promise<boolean> {
const mobileMenu = this.page.locator('#mobile-menu');
return !(await mobileMenu.isVisible());
}
async getMobileMenuItems(): Promise<string[]> {
const items = this.page.locator('#mobile-menu a');
const count = await items.count();
const itemTexts: string[] = [];
for (let i = 0; i < count; i++) {
const text = await items.nth(i).textContent();
if (text) {
itemTexts.push(text);
}
}
return itemTexts;
}
async tapMobileMenuItem(itemText: string): Promise<void> {
const menuItem = this.page.locator(`#mobile-menu a:has-text("${itemText}")`);
await this.tapElement(`#mobile-menu a:has-text("${itemText}")`);
}
async isMobileMenuVisible(): Promise<boolean> {
const menuButton = this.page.locator('button[aria-label="打开菜单"]');
return await menuButton.isVisible();
}
async waitForMobileMenuAnimation(): Promise<void> {
await this.page.waitForTimeout(300);
}
}