feat(e2e): 添加完整的E2E测试框架和测试用例
添加Playwright测试框架配置和基础页面对象 实现冒烟测试用例覆盖首页和联系页面核心功能 更新导航组件以支持滚动高亮功能 添加BackButton组件统一返回按钮行为 配置Woodpecker CI集成和测试报告生成
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import { test as base } from '@playwright/test';
|
||||
import { AxeBuilder } from '@axe-core/playwright';
|
||||
|
||||
export const test = base.extend({
|
||||
makeAxeBuilder: async ({ page }, use) => {
|
||||
const makeAxeBuilder = () => new AxeBuilder({ page });
|
||||
await use(makeAxeBuilder);
|
||||
},
|
||||
});
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
@@ -0,0 +1,28 @@
|
||||
import { test as base, Page } from '@playwright/test';
|
||||
import { HomePage } from '../pages/HomePage';
|
||||
import { ContactPage } from '../pages/ContactPage';
|
||||
import { TestDataGenerator } from '../utils/TestDataGenerator';
|
||||
|
||||
export type TestFixtures = {
|
||||
homePage: HomePage;
|
||||
contactPage: ContactPage;
|
||||
testDataGenerator: typeof TestDataGenerator;
|
||||
};
|
||||
|
||||
export const test = base.extend<TestFixtures>({
|
||||
homePage: async ({ page }, use) => {
|
||||
const homePage = new HomePage(page);
|
||||
await use(homePage);
|
||||
},
|
||||
|
||||
contactPage: async ({ page }, use) => {
|
||||
const contactPage = new ContactPage(page);
|
||||
await use(contactPage);
|
||||
},
|
||||
|
||||
testDataGenerator: async ({}, use) => {
|
||||
await use(TestDataGenerator);
|
||||
},
|
||||
});
|
||||
|
||||
export const expect = test.expect;
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Page, Locator, expect } from '@playwright/test';
|
||||
|
||||
export class BasePage {
|
||||
readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async navigate(url: string): Promise<void> {
|
||||
await this.page.goto(url);
|
||||
}
|
||||
|
||||
async waitForLoadState(state: 'load' | 'domcontentloaded' | 'networkidle' = 'load'): Promise<void> {
|
||||
await this.page.waitForLoadState(state);
|
||||
}
|
||||
|
||||
async click(locator: Locator | string): Promise<void> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
await element.click();
|
||||
}
|
||||
|
||||
async fill(locator: Locator | string, value: string): Promise<void> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
await element.fill(value);
|
||||
}
|
||||
|
||||
async getText(locator: Locator | string): Promise<string> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
return await element.textContent() || '';
|
||||
}
|
||||
|
||||
async isVisible(locator: Locator | string): Promise<boolean> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
return await element.isVisible();
|
||||
}
|
||||
|
||||
async waitForElement(locator: Locator | string, timeout: number = 5000): Promise<void> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
await element.waitFor({ state: 'visible', timeout });
|
||||
}
|
||||
|
||||
async scrollToElement(locator: Locator | string): Promise<void> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
await element.scrollIntoViewIfNeeded();
|
||||
}
|
||||
|
||||
async takeScreenshot(filename: string): Promise<void> {
|
||||
await this.page.screenshot({ path: `test-results/screenshots/${filename}` });
|
||||
}
|
||||
|
||||
async hover(locator: Locator | string): Promise<void> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
await element.hover();
|
||||
}
|
||||
|
||||
async selectOption(locator: Locator | string, value: string): Promise<void> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
await element.selectOption(value);
|
||||
}
|
||||
|
||||
async check(locator: Locator | string): Promise<void> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
await element.check();
|
||||
}
|
||||
|
||||
async uncheck(locator: Locator | string): Promise<void> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
await element.uncheck();
|
||||
}
|
||||
|
||||
async waitForURL(url: string | RegExp, timeout: number = 5000): Promise<void> {
|
||||
await this.page.waitForURL(url, { timeout });
|
||||
}
|
||||
|
||||
async getCurrentURL(): Promise<string> {
|
||||
return this.page.url();
|
||||
}
|
||||
|
||||
async getTitle(): Promise<string> {
|
||||
return await this.page.title();
|
||||
}
|
||||
|
||||
async waitForSelector(locator: Locator | string, options?: { state?: 'attached' | 'detached' | 'visible' | 'hidden', timeout?: number }): Promise<void> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
await element.waitFor(options);
|
||||
}
|
||||
|
||||
async getAttribute(locator: Locator | string, attribute: string): Promise<string | null> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
return await element.getAttribute(attribute);
|
||||
}
|
||||
|
||||
async pressKey(key: string): Promise<void> {
|
||||
await this.page.keyboard.press(key);
|
||||
}
|
||||
|
||||
async type(locator: Locator | string, text: string, options?: { delay?: number }): Promise<void> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
await element.type(text, options);
|
||||
}
|
||||
|
||||
async waitForNavigation(options?: { url?: string | RegExp, timeout?: number }): Promise<void> {
|
||||
await this.page.waitForNavigation(options);
|
||||
}
|
||||
|
||||
async reload(): Promise<void> {
|
||||
await this.page.reload();
|
||||
}
|
||||
|
||||
async goBack(): Promise<void> {
|
||||
await this.page.goBack();
|
||||
}
|
||||
|
||||
async goForward(): Promise<void> {
|
||||
await this.page.goForward();
|
||||
}
|
||||
|
||||
async evaluate<T>(pageFunction: () => T): Promise<T> {
|
||||
return await this.page.evaluate(pageFunction);
|
||||
}
|
||||
|
||||
async waitForTimeout(timeout: number): Promise<void> {
|
||||
await this.page.waitForTimeout(timeout);
|
||||
}
|
||||
|
||||
async count(locator: Locator | string): Promise<number> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
return await element.count();
|
||||
}
|
||||
|
||||
async allTextContents(locator: Locator | string): Promise<string[]> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
return await element.allTextContents();
|
||||
}
|
||||
|
||||
async isDisabled(locator: Locator | string): Promise<boolean> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
return await element.isDisabled();
|
||||
}
|
||||
|
||||
async isEnabled(locator: Locator | string): Promise<boolean> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
return await element.isEnabled();
|
||||
}
|
||||
|
||||
async isChecked(locator: Locator | string): Promise<boolean> {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
return await element.isChecked();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
import { Page, Locator, expect } from '@playwright/test';
|
||||
import { BasePage } from './BasePage';
|
||||
import { ContactFormData } from '../types';
|
||||
|
||||
export class ContactPage extends BasePage {
|
||||
readonly url: string;
|
||||
|
||||
readonly pageHeader: Locator;
|
||||
readonly contactForm: Locator;
|
||||
readonly nameInput: Locator;
|
||||
readonly phoneInput: Locator;
|
||||
readonly emailInput: Locator;
|
||||
readonly subjectInput: Locator;
|
||||
readonly messageInput: Locator;
|
||||
readonly submitButton: Locator;
|
||||
|
||||
readonly contactInfoCard: Locator;
|
||||
readonly workHoursCard: Locator;
|
||||
|
||||
readonly successMessage: Locator;
|
||||
|
||||
readonly addressInfo: Locator;
|
||||
readonly phoneInfo: Locator;
|
||||
readonly emailInfo: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.url = '/contact';
|
||||
|
||||
this.pageHeader = page.locator('h1:has-text("与我们取得联系")');
|
||||
this.contactForm = page.locator('form');
|
||||
this.nameInput = page.locator('input[name="name"]');
|
||||
this.phoneInput = page.locator('input[name="phone"]');
|
||||
this.emailInput = page.locator('input[name="email"]');
|
||||
this.subjectInput = page.locator('input[name="subject"]');
|
||||
this.messageInput = page.locator('textarea[name="message"]');
|
||||
this.submitButton = page.locator('button[type="submit"]');
|
||||
|
||||
this.contactInfoCard = page.locator('[data-slot="card"]').filter({ hasText: '联系方式' }).first();
|
||||
this.workHoursCard = page.locator('[data-slot="card"]').filter({ hasText: '工作时间' }).first();
|
||||
|
||||
this.successMessage = page.locator('text=消息已发送');
|
||||
|
||||
this.addressInfo = this.contactInfoCard.locator('text=公司地址');
|
||||
this.phoneInfo = this.contactInfoCard.locator('text=联系电话');
|
||||
this.emailInfo = this.contactInfoCard.locator('text=电子邮箱');
|
||||
}
|
||||
|
||||
async goto(): Promise<void> {
|
||||
await this.navigate(this.url);
|
||||
await this.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async isLoaded(): Promise<boolean> {
|
||||
try {
|
||||
await this.pageHeader.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await this.contactForm.waitFor({ state: 'visible', timeout: 5000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async waitForPageLoad(): Promise<void> {
|
||||
await this.waitForLoadState('networkidle');
|
||||
await this.pageHeader.waitFor({ state: 'visible' });
|
||||
await this.contactForm.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async fillContactForm(data: ContactFormData): Promise<void> {
|
||||
if (data.name) {
|
||||
await this.nameInput.fill(data.name);
|
||||
}
|
||||
if (data.phone) {
|
||||
await this.phoneInput.fill(data.phone);
|
||||
}
|
||||
if (data.email) {
|
||||
await this.emailInput.fill(data.email);
|
||||
}
|
||||
if (data.subject) {
|
||||
await this.subjectInput.fill(data.subject);
|
||||
}
|
||||
if (data.message) {
|
||||
await this.messageInput.fill(data.message);
|
||||
}
|
||||
}
|
||||
|
||||
async submitForm(): Promise<void> {
|
||||
await this.submitButton.click();
|
||||
}
|
||||
|
||||
async fillAndSubmitForm(data: ContactFormData): Promise<void> {
|
||||
await this.fillContactForm(data);
|
||||
await this.submitForm();
|
||||
}
|
||||
|
||||
async isSuccessMessageVisible(): Promise<boolean> {
|
||||
return await this.successMessage.isVisible();
|
||||
}
|
||||
|
||||
async getSuccessMessageText(): Promise<string> {
|
||||
return await this.successMessage.textContent() || '';
|
||||
}
|
||||
|
||||
async isFormVisible(): Promise<boolean> {
|
||||
return await this.contactForm.isVisible();
|
||||
}
|
||||
|
||||
async isSubmitButtonEnabled(): Promise<boolean> {
|
||||
return await this.submitButton.isEnabled();
|
||||
}
|
||||
|
||||
async getSubmitButtonText(): Promise<string> {
|
||||
return await this.submitButton.textContent() || '';
|
||||
}
|
||||
|
||||
async isSubmitButtonLoading(): Promise<boolean> {
|
||||
const text = await this.getSubmitButtonText();
|
||||
return text.includes('发送中');
|
||||
}
|
||||
|
||||
async getNameInputValue(): Promise<string> {
|
||||
return await this.nameInput.inputValue();
|
||||
}
|
||||
|
||||
async getPhoneInputValue(): Promise<string> {
|
||||
return await this.phoneInput.inputValue();
|
||||
}
|
||||
|
||||
async getEmailInputValue(): Promise<string> {
|
||||
return await this.emailInput.inputValue();
|
||||
}
|
||||
|
||||
async getSubjectInputValue(): Promise<string> {
|
||||
return await this.subjectInput.inputValue();
|
||||
}
|
||||
|
||||
async getMessageInputValue(): Promise<string> {
|
||||
return await this.messageInput.inputValue();
|
||||
}
|
||||
|
||||
async clearForm(): Promise<void> {
|
||||
await this.nameInput.fill('');
|
||||
await this.phoneInput.fill('');
|
||||
await this.emailInput.fill('');
|
||||
await this.subjectInput.fill('');
|
||||
await this.messageInput.fill('');
|
||||
}
|
||||
|
||||
async isContactInfoCardVisible(): Promise<boolean> {
|
||||
return await this.contactInfoCard.isVisible();
|
||||
}
|
||||
|
||||
async isWorkHoursCardVisible(): Promise<boolean> {
|
||||
return await this.workHoursCard.isVisible();
|
||||
}
|
||||
|
||||
async getContactInfoText(): Promise<string> {
|
||||
return await this.contactInfoCard.textContent() || '';
|
||||
}
|
||||
|
||||
async getWorkHoursText(): Promise<string> {
|
||||
return await this.workHoursCard.textContent() || '';
|
||||
}
|
||||
|
||||
async getAddress(): Promise<string> {
|
||||
const addressElement = this.contactInfoCard.locator('div').nth(3).locator('div').nth(1);
|
||||
return await addressElement.textContent() || '';
|
||||
}
|
||||
|
||||
async getPhone(): Promise<string> {
|
||||
const phoneElement = this.contactInfoCard.locator('div').nth(6).locator('div').nth(1);
|
||||
return await phoneElement.textContent() || '';
|
||||
}
|
||||
|
||||
async getEmail(): Promise<string> {
|
||||
const emailElement = this.contactInfoCard.locator('div').nth(9).locator('div').nth(1);
|
||||
return await emailElement.textContent() || '';
|
||||
}
|
||||
|
||||
async getPageTitle(): Promise<string> {
|
||||
return await this.pageHeader.textContent() || '';
|
||||
}
|
||||
|
||||
async getPageDescription(): Promise<string> {
|
||||
const description = this.pageHeader.locator('..').locator('p');
|
||||
return await description.textContent() || '';
|
||||
}
|
||||
|
||||
async getBadgeText(): Promise<string> {
|
||||
const badge = this.page.locator('[data-slot="badge"]').first();
|
||||
return await badge.textContent() || '';
|
||||
}
|
||||
|
||||
async isRequiredFieldVisible(fieldName: string): Promise<boolean> {
|
||||
const label = this.page.locator(`label[for="${fieldName}"]`);
|
||||
return await label.isVisible();
|
||||
}
|
||||
|
||||
async isFieldRequired(fieldName: string): Promise<boolean> {
|
||||
const label = this.page.locator(`label[for="${fieldName}"]`);
|
||||
const text = await label.textContent();
|
||||
return text?.includes('*') || false;
|
||||
}
|
||||
|
||||
async getFieldPlaceholder(fieldName: string): Promise<string> {
|
||||
const input = this.page.locator(`[name="${fieldName}"]`);
|
||||
return await input.getAttribute('placeholder') || '';
|
||||
}
|
||||
|
||||
async scrollToForm(): Promise<void> {
|
||||
await this.contactForm.scrollIntoViewIfNeeded();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async takeScreenshotOfForm(filename: string): Promise<void> {
|
||||
await this.contactForm.screenshot({ path: `test-results/screenshots/${filename}` });
|
||||
}
|
||||
|
||||
async takeScreenshotOfSuccessMessage(filename: string): Promise<void> {
|
||||
await this.successMessage.screenshot({ path: `test-results/screenshots/${filename}` });
|
||||
}
|
||||
|
||||
async waitForFormSubmission(): Promise<void> {
|
||||
await this.page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
async isFormSubmitted(): Promise<boolean> {
|
||||
return await this.isSuccessMessageVisible();
|
||||
}
|
||||
|
||||
async getFormValidationErrors(): Promise<string[]> {
|
||||
const errors: string[] = [];
|
||||
const requiredInputs = this.contactForm.locator('input[required], textarea[required]');
|
||||
const count = await requiredInputs.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const input = requiredInputs.nth(i);
|
||||
const isValid = await input.evaluate(el => (el as HTMLInputElement).checkValidity());
|
||||
if (!isValid) {
|
||||
const name = await input.getAttribute('name');
|
||||
errors.push(`${name} is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
async isEmailValid(): Promise<boolean> {
|
||||
return await this.emailInput.evaluate(el => (el as HTMLInputElement).checkValidity());
|
||||
}
|
||||
|
||||
async isPhoneValid(): Promise<boolean> {
|
||||
return await this.phoneInput.evaluate(el => (el as HTMLInputElement).checkValidity());
|
||||
}
|
||||
|
||||
async focusOnField(fieldName: string): Promise<void> {
|
||||
const input = this.page.locator(`[name="${fieldName}"]`);
|
||||
await input.focus();
|
||||
}
|
||||
|
||||
async blurField(fieldName: string): Promise<void> {
|
||||
const input = this.page.locator(`[name="${fieldName}"]`);
|
||||
await input.blur();
|
||||
}
|
||||
|
||||
async typeInField(fieldName: string, text: string, options?: { delay?: number }): Promise<void> {
|
||||
const input = this.page.locator(`[name="${fieldName}"]`);
|
||||
await input.type(text, options);
|
||||
}
|
||||
|
||||
async clearField(fieldName: string): Promise<void> {
|
||||
const input = this.page.locator(`[name="${fieldName}"]`);
|
||||
await input.fill('');
|
||||
}
|
||||
|
||||
async isFieldVisible(fieldName: string): Promise<boolean> {
|
||||
const input = this.page.locator(`[name="${fieldName}"]`);
|
||||
return await input.isVisible();
|
||||
}
|
||||
|
||||
async isFieldEnabled(fieldName: string): Promise<boolean> {
|
||||
const input = this.page.locator(`[name="${fieldName}"]`);
|
||||
return await input.isEnabled();
|
||||
}
|
||||
|
||||
async getFieldAttribute(fieldName: string, attribute: string): Promise<string | null> {
|
||||
const input = this.page.locator(`[name="${fieldName}"]`);
|
||||
return await input.getAttribute(attribute);
|
||||
}
|
||||
|
||||
async getWorkHours(): Promise<{ day: string; hours: string }[]> {
|
||||
const workHours: { day: string; hours: string }[] = [];
|
||||
const rows = this.workHoursCard.locator('.space-y-2 > div');
|
||||
const count = await rows.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const row = rows.nth(i);
|
||||
const day = await row.locator('span').first().textContent();
|
||||
const hours = await row.locator('span').nth(1).textContent();
|
||||
if (day && hours) {
|
||||
workHours.push({ day: day.trim(), hours: hours.trim() });
|
||||
}
|
||||
}
|
||||
return workHours;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import { Page, Locator, expect } from '@playwright/test';
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class HomePage extends BasePage {
|
||||
readonly url: string;
|
||||
|
||||
readonly header: Locator;
|
||||
readonly logo: Locator;
|
||||
readonly navigation: Locator;
|
||||
readonly heroSection: Locator;
|
||||
readonly servicesSection: Locator;
|
||||
readonly productsSection: Locator;
|
||||
readonly casesSection: Locator;
|
||||
readonly aboutSection: Locator;
|
||||
readonly newsSection: Locator;
|
||||
readonly contactSection: Locator;
|
||||
readonly footer: Locator;
|
||||
|
||||
readonly mobileMenuButton: Locator;
|
||||
readonly mobileMenu: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.url = '/';
|
||||
|
||||
this.header = page.locator('header');
|
||||
this.logo = page.locator('header img[alt*="四川睿新致远"]');
|
||||
this.navigation = page.locator('nav[role="navigation"]');
|
||||
this.heroSection = page.locator('#home');
|
||||
this.servicesSection = page.locator('#services');
|
||||
this.productsSection = page.locator('#products');
|
||||
this.casesSection = page.locator('#cases');
|
||||
this.aboutSection = page.locator('#about');
|
||||
this.newsSection = page.locator('#news');
|
||||
this.contactSection = page.locator('#contact');
|
||||
this.footer = page.locator('footer');
|
||||
|
||||
this.mobileMenuButton = page.locator('button[aria-label="打开菜单"]');
|
||||
this.mobileMenu = page.locator('#mobile-menu-panel');
|
||||
}
|
||||
|
||||
async goto(): Promise<void> {
|
||||
await this.navigate(this.url);
|
||||
await this.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async isLoaded(): Promise<boolean> {
|
||||
try {
|
||||
await this.header.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await this.heroSection.waitFor({ state: 'visible', timeout: 5000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async waitForPageLoad(): Promise<void> {
|
||||
await this.waitForLoadState('networkidle');
|
||||
await this.header.waitFor({ state: 'visible' });
|
||||
await this.heroSection.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async getNavigationItems(): Promise<Locator[]> {
|
||||
return await this.navigation.locator('a').all();
|
||||
}
|
||||
|
||||
async clickNavigationItem(label: string): Promise<void> {
|
||||
await this.navigation.locator(`a:has-text("${label}")`).click();
|
||||
}
|
||||
|
||||
async openMobileMenu(): Promise<void> {
|
||||
if (!(await this.mobileMenu.isVisible())) {
|
||||
await this.mobileMenuButton.click();
|
||||
await this.mobileMenu.waitFor({ state: 'visible' });
|
||||
}
|
||||
}
|
||||
|
||||
async closeMobileMenu(): Promise<void> {
|
||||
if (await this.mobileMenu.isVisible()) {
|
||||
await this.mobileMenuButton.click();
|
||||
await this.mobileMenu.waitFor({ state: 'hidden' });
|
||||
}
|
||||
}
|
||||
|
||||
async scrollToSection(sectionId: string): Promise<void> {
|
||||
const section = this.page.locator(`#${sectionId}`);
|
||||
await section.scrollIntoViewIfNeeded();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async isSectionVisible(sectionId: string): Promise<boolean> {
|
||||
const section = this.page.locator(`#${sectionId}`);
|
||||
return await section.isVisible();
|
||||
}
|
||||
|
||||
async getSectionText(sectionId: string): Promise<string> {
|
||||
const section = this.page.locator(`#${sectionId}`);
|
||||
return await section.textContent() || '';
|
||||
}
|
||||
|
||||
async clickContactButton(): Promise<void> {
|
||||
await this.page.locator('a:has-text("立即咨询")').first().click();
|
||||
}
|
||||
|
||||
async isLogoVisible(): Promise<boolean> {
|
||||
return await this.logo.isVisible();
|
||||
}
|
||||
|
||||
async getLogoAltText(): Promise<string | null> {
|
||||
return await this.logo.getAttribute('alt');
|
||||
}
|
||||
|
||||
async isFooterVisible(): Promise<boolean> {
|
||||
return await this.footer.isVisible();
|
||||
}
|
||||
|
||||
async getFooterText(): Promise<string> {
|
||||
return await this.footer.textContent() || '';
|
||||
}
|
||||
|
||||
async waitForHeroSection(): Promise<void> {
|
||||
await this.heroSection.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async waitForServicesSection(): Promise<void> {
|
||||
await this.scrollToSection('services');
|
||||
await this.servicesSection.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async waitForProductsSection(): Promise<void> {
|
||||
await this.scrollToSection('products');
|
||||
await this.productsSection.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async waitForCasesSection(): Promise<void> {
|
||||
await this.scrollToSection('cases');
|
||||
await this.casesSection.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async waitForAboutSection(): Promise<void> {
|
||||
await this.scrollToSection('about');
|
||||
await this.aboutSection.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async waitForNewsSection(): Promise<void> {
|
||||
await this.scrollToSection('news');
|
||||
await this.newsSection.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async waitForContactSection(): Promise<void> {
|
||||
await this.scrollToSection('contact');
|
||||
await this.contactSection.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async scrollToBottom(): Promise<void> {
|
||||
await this.page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async scrollToTop(): Promise<void> {
|
||||
await this.page.evaluate(() => window.scrollTo(0, 0));
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async getActiveNavigationItem(): Promise<string | null> {
|
||||
const activeItem = this.navigation.locator('a[aria-current="page"]');
|
||||
if (await activeItem.count() > 0) {
|
||||
return await activeItem.textContent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async isNavigationItemActive(label: string): Promise<boolean> {
|
||||
const item = this.navigation.locator(`a:has-text("${label}")`);
|
||||
const ariaCurrent = await item.getAttribute('aria-current');
|
||||
return ariaCurrent === 'page';
|
||||
}
|
||||
|
||||
async getAllSectionIds(): Promise<string[]> {
|
||||
return await this.page.evaluate(() => {
|
||||
const sections = document.querySelectorAll('section[id]');
|
||||
return Array.from(sections).map(section => section.id);
|
||||
});
|
||||
}
|
||||
|
||||
async takeScreenshotOfSection(sectionId: string, filename: string): Promise<void> {
|
||||
const section = this.page.locator(`#${sectionId}`);
|
||||
await section.screenshot({ path: `test-results/screenshots/${filename}` });
|
||||
}
|
||||
|
||||
async getHeroSectionTitle(): Promise<string> {
|
||||
const title = this.heroSection.locator('h1, h2').first();
|
||||
return await title.textContent() || '';
|
||||
}
|
||||
|
||||
async getServicesSectionTitle(): Promise<string> {
|
||||
const title = this.servicesSection.locator('h2').first();
|
||||
return await title.textContent() || '';
|
||||
}
|
||||
|
||||
async getProductsSectionTitle(): Promise<string> {
|
||||
const title = this.productsSection.locator('h2').first();
|
||||
return await title.textContent() || '';
|
||||
}
|
||||
|
||||
async getCasesSectionTitle(): Promise<string> {
|
||||
const title = this.casesSection.locator('h2').first();
|
||||
return await title.textContent() || '';
|
||||
}
|
||||
|
||||
async getAboutSectionTitle(): Promise<string> {
|
||||
const title = this.aboutSection.locator('h2').first();
|
||||
return await title.textContent() || '';
|
||||
}
|
||||
|
||||
async getNewsSectionTitle(): Promise<string> {
|
||||
const title = this.newsSection.locator('h2').first();
|
||||
return await title.textContent() || '';
|
||||
}
|
||||
|
||||
async getContactSectionTitle(): Promise<string> {
|
||||
const title = this.contactSection.locator('h2').first();
|
||||
return await title.textContent() || '';
|
||||
}
|
||||
|
||||
async isHeaderSticky(): Promise<boolean> {
|
||||
const isSticky = await this.header.evaluate(el => {
|
||||
return window.getComputedStyle(el).position === 'fixed';
|
||||
});
|
||||
return isSticky;
|
||||
}
|
||||
|
||||
async getHeaderBackgroundColor(): Promise<string> {
|
||||
return await this.header.evaluate(el => {
|
||||
return window.getComputedStyle(el).backgroundColor;
|
||||
});
|
||||
}
|
||||
|
||||
async isHeaderScrolled(): Promise<boolean> {
|
||||
const hasShadow = await this.header.evaluate(el => {
|
||||
return window.getComputedStyle(el).boxShadow !== 'none';
|
||||
});
|
||||
return hasShadow;
|
||||
}
|
||||
|
||||
async getNavigationItemCount(): Promise<number> {
|
||||
return await this.navigation.locator('a').count();
|
||||
}
|
||||
|
||||
async getAllNavigationLabels(): Promise<string[]> {
|
||||
const items = await this.getNavigationItems();
|
||||
const labels: string[] = [];
|
||||
for (const item of items) {
|
||||
const text = await item.textContent();
|
||||
if (text) labels.push(text);
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('检查联系页面所有元素', async ({ page }) => {
|
||||
await page.goto('/contact');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const badges = await page.locator('[class*="badge"]').all();
|
||||
console.log('找到的badge数量:', badges.length);
|
||||
|
||||
for (let i = 0; i < badges.length; i++) {
|
||||
const badge = badges[i];
|
||||
const className = await badge.evaluate(el => el.className);
|
||||
const text = await badge.textContent();
|
||||
console.log(`Badge ${i}: ${className} - ${text}`);
|
||||
}
|
||||
|
||||
const contactCard = page.locator('[class*="card"]').filter({ hasText: '联系方式' }).first();
|
||||
const contactCardText = await contactCard.textContent();
|
||||
console.log('联系卡片文本:', contactCardText?.substring(0, 100));
|
||||
|
||||
const workHoursCard = page.locator('[class*="card"]').filter({ hasText: '工作时间' }).first();
|
||||
const workHoursCardText = await workHoursCard.textContent();
|
||||
console.log('工作时间卡片文本:', workHoursCardText?.substring(0, 100));
|
||||
|
||||
const addressH3 = contactCard.locator('h3:has-text("公司地址")');
|
||||
const addressParent = addressH3.locator('..');
|
||||
const addressGrandParent = addressParent.locator('..');
|
||||
const addressP = addressGrandParent.locator('p');
|
||||
const addressText = await addressP.textContent();
|
||||
console.log('地址文本:', addressText);
|
||||
|
||||
const phoneH3 = contactCard.locator('h3:has-text("联系电话")');
|
||||
const phoneParent = phoneH3.locator('..');
|
||||
const phoneGrandParent = phoneParent.locator('..');
|
||||
const phoneP = phoneGrandParent.locator('p');
|
||||
const phoneText = await phoneP.textContent();
|
||||
console.log('电话文本:', phoneText);
|
||||
|
||||
const emailH3 = contactCard.locator('h3:has-text("电子邮箱")');
|
||||
const emailParent = emailH3.locator('..');
|
||||
const emailGrandParent = emailParent.locator('..');
|
||||
const emailP = emailGrandParent.locator('p');
|
||||
const emailText = await emailP.textContent();
|
||||
console.log('邮箱文本:', emailText);
|
||||
|
||||
const workHoursRows = workHoursCard.locator('.space-y-2 > div');
|
||||
const workHoursCount = await workHoursRows.count();
|
||||
console.log('工作时间行数:', workHoursCount);
|
||||
|
||||
for (let i = 0; i < workHoursCount; i++) {
|
||||
const row = workHoursRows.nth(i);
|
||||
const day = await row.locator('span').first().textContent();
|
||||
const hours = await row.locator('span').nth(1).textContent();
|
||||
console.log(`工作时间 ${i}: ${day} - ${hours}`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('检查联系卡片详细结构', async ({ page }) => {
|
||||
await page.goto('/contact');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const contactCard = page.locator('[data-slot="card"]').filter({ hasText: '联系方式' }).first();
|
||||
|
||||
const contactCardChildren = await contactCard.locator('div').all();
|
||||
console.log('联系卡片子元素数量:', contactCardChildren.length);
|
||||
|
||||
for (let i = 0; i < contactCardChildren.length; i++) {
|
||||
const child = contactCardChildren[i];
|
||||
const className = await child.evaluate(el => el.className);
|
||||
const text = await child.textContent();
|
||||
console.log(`子元素 ${i}: ${className.substring(0, 80)} - ${text?.substring(0, 50)}`);
|
||||
}
|
||||
|
||||
const allDivs = await contactCard.locator('div').all();
|
||||
console.log('所有div数量:', allDivs.length);
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('检查联系页面Card标题', async ({ page }) => {
|
||||
await page.goto('/contact');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const cardTitles = await page.locator('CardTitle').all();
|
||||
console.log('找到的CardTitle数量:', cardTitles.length);
|
||||
|
||||
for (let i = 0; i < cardTitles.length; i++) {
|
||||
const title = cardTitles[i];
|
||||
const text = await title.textContent();
|
||||
console.log(`CardTitle ${i}: ${text}`);
|
||||
}
|
||||
|
||||
const allCards = await page.locator('[class*="card"]').all();
|
||||
console.log('找到的所有card元素数量:', allCards.length);
|
||||
|
||||
for (let i = 0; i < allCards.length; i++) {
|
||||
const card = allCards[i];
|
||||
const className = await card.evaluate(el => el.className);
|
||||
const text = await card.textContent();
|
||||
console.log(`Card ${i}: ${className.substring(0, 50)} - ${text?.substring(0, 50)}`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('检查联系卡片详细信息', async ({ page }) => {
|
||||
await page.goto('/contact');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const contactCard = page.locator('.card').filter({ hasText: '联系方式' });
|
||||
const contactText = await contactCard.textContent();
|
||||
console.log('联系方式卡片内容:', contactText);
|
||||
|
||||
const workHoursCard = page.locator('.card').filter({ hasText: '工作时间' });
|
||||
const workHoursText = await workHoursCard.textContent();
|
||||
console.log('工作时间卡片内容:', workHoursText);
|
||||
|
||||
const allCards = await page.locator('.card').all();
|
||||
console.log('所有卡片数量:', allCards.length);
|
||||
|
||||
for (let i = 0; i < allCards.length; i++) {
|
||||
const card = allCards[i];
|
||||
const text = await card.textContent();
|
||||
console.log(`卡片 ${i}:`, text?.substring(0, 100));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('检查联系页面详细元素', async ({ page }) => {
|
||||
await page.goto('/contact');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const pageHeader = page.locator('h1:has-text("与我们取得联系")');
|
||||
const pageHeaderParent = pageHeader.locator('..');
|
||||
const pageHeaderGrandParent = pageHeaderParent.locator('..');
|
||||
|
||||
console.log('Page Header parent:', await pageHeaderParent.evaluate(el => el.className));
|
||||
console.log('Page Header grand parent:', await pageHeaderGrandParent.evaluate(el => el.className));
|
||||
|
||||
const badges = await pageHeaderGrandParent.locator('.badge').all();
|
||||
console.log('找到的badge数量:', badges.length);
|
||||
|
||||
for (let i = 0; i < badges.length; i++) {
|
||||
const badge = badges[i];
|
||||
const text = await badge.textContent();
|
||||
console.log(`Badge ${i}: ${text}`);
|
||||
}
|
||||
|
||||
const contactCard = page.locator('h3:has-text("联系方式")');
|
||||
const contactCardParent = contactCard.locator('..');
|
||||
const contactCardGrandParent = contactCardParent.locator('..');
|
||||
const contactCardGreatGrandParent = contactCardGrandParent.locator('..');
|
||||
|
||||
console.log('Contact card great grand parent:', await contactCardGreatGrandParent.evaluate(el => el.className));
|
||||
|
||||
const addressElement = contactCard.locator('text=公司地址').locator('..').locator('p');
|
||||
const addressText = await addressElement.textContent();
|
||||
console.log('地址:', addressText);
|
||||
|
||||
const phoneElement = contactCard.locator('text=联系电话').locator('..').locator('p');
|
||||
const phoneText = await phoneElement.textContent();
|
||||
console.log('电话:', phoneText);
|
||||
|
||||
const emailElement = contactCard.locator('text=电子邮箱').locator('..').locator('p');
|
||||
const emailText = await emailElement.textContent();
|
||||
console.log('邮箱:', emailText);
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('检查联系页面完整DOM', async ({ page }) => {
|
||||
await page.goto('/contact');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const contactCard = page.locator('[class*="card"]').filter({ hasText: '联系方式' }).first();
|
||||
|
||||
const contactCardHTML = await contactCard.innerHTML();
|
||||
console.log('联系卡片HTML:', contactCardHTML.substring(0, 500));
|
||||
|
||||
const contactCardChildren = await contactCard.locator('div').all();
|
||||
console.log('联系卡片子元素数量:', contactCardChildren.length);
|
||||
|
||||
for (let i = 0; i < contactCardChildren.length; i++) {
|
||||
const child = contactCardChildren[i];
|
||||
const className = await child.evaluate(el => el.className);
|
||||
const text = await child.textContent();
|
||||
console.log(`子元素 ${i}: ${className.substring(0, 50)} - ${text?.substring(0, 50)}`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('检查联系页面完整结构', async ({ page }) => {
|
||||
await page.goto('/contact');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const allH3 = await page.locator('h3').all();
|
||||
console.log('找到的h3元素数量:', allH3.length);
|
||||
|
||||
for (let i = 0; i < allH3.length; i++) {
|
||||
const h3 = allH3[i];
|
||||
const text = await h3.textContent();
|
||||
console.log(`H3 ${i}: ${text}`);
|
||||
|
||||
const parent = h3.locator('..');
|
||||
const grandParent = parent.locator('..');
|
||||
const greatGrandParent = grandParent.locator('..');
|
||||
|
||||
try {
|
||||
const parentClass = await parent.evaluate(el => el.className);
|
||||
const grandParentClass = await grandParent.evaluate(el => el.className);
|
||||
const greatGrandParentClass = await greatGrandParent.evaluate(el => el.className);
|
||||
console.log(` Parent: ${parentClass}`);
|
||||
console.log(` Grand Parent: ${grandParentClass}`);
|
||||
console.log(` Great Grand Parent: ${greatGrandParentClass}`);
|
||||
} catch (e) {
|
||||
console.log(` Error getting parent info: ${e}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('检查联系页面元素', async ({ page }) => {
|
||||
await page.goto('/contact');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const pageHeaders = await page.locator('.page-header').all();
|
||||
console.log('找到的page-header数量:', pageHeaders.length);
|
||||
|
||||
const forms = await page.locator('form').all();
|
||||
console.log('找到的form数量:', forms.length);
|
||||
|
||||
const cards = await page.locator('.card').all();
|
||||
console.log('找到的card数量:', cards.length);
|
||||
|
||||
for (let i = 0; i < cards.length; i++) {
|
||||
const card = cards[i];
|
||||
const text = await card.textContent();
|
||||
console.log(`Card ${i}: ${text?.substring(0, 50)}`);
|
||||
}
|
||||
|
||||
const inputs = await page.locator('input').all();
|
||||
console.log('找到的input数量:', inputs.length);
|
||||
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i];
|
||||
const name = await input.getAttribute('name');
|
||||
const type = await input.getAttribute('type');
|
||||
console.log(`Input ${i}: name="${name}", type="${type}"`);
|
||||
}
|
||||
|
||||
const textareas = await page.locator('textarea').all();
|
||||
console.log('找到的textarea数量:', textareas.length);
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('检查联系页面结构', async ({ page }) => {
|
||||
await page.goto('/contact');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const allElements = await page.evaluate(() => {
|
||||
const result: any = {};
|
||||
|
||||
const h1s = document.querySelectorAll('h1');
|
||||
result.h1Count = h1s.length;
|
||||
result.h1Text = Array.from(h1s).map(h => h.textContent?.substring(0, 50));
|
||||
|
||||
const cards = document.querySelectorAll('[class*="card"]');
|
||||
result.cardCount = cards.length;
|
||||
result.cardText = Array.from(cards).map(c => c.textContent?.substring(0, 50));
|
||||
|
||||
const pageHeaders = document.querySelectorAll('[class*="page-header"]');
|
||||
result.pageHeaderCount = pageHeaders.length;
|
||||
|
||||
const contactInfoCards = document.querySelectorAll('[class*="contact"]');
|
||||
result.contactInfoCount = contactInfoCards.length;
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
console.log('联系页面结构:', JSON.stringify(allElements, null, 2));
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('测试页面加载', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const title = await page.title();
|
||||
console.log('页面标题:', title);
|
||||
|
||||
const body = await page.locator('body').textContent();
|
||||
console.log('页面内容长度:', body?.length);
|
||||
|
||||
const headers = await page.locator('header').all();
|
||||
console.log('找到的header元素数量:', headers.length);
|
||||
|
||||
const navs = await page.locator('nav').all();
|
||||
console.log('找到的nav元素数量:', navs.length);
|
||||
|
||||
const sections = await page.locator('section').all();
|
||||
console.log('找到的section元素数量:', sections.length);
|
||||
|
||||
const allElements = await page.evaluate(() => {
|
||||
const headers = document.querySelectorAll('header');
|
||||
const navs = document.querySelectorAll('nav');
|
||||
const sections = document.querySelectorAll('section');
|
||||
return {
|
||||
headers: headers.length,
|
||||
navs: navs.length,
|
||||
sections: sections.length,
|
||||
bodyText: document.body.textContent?.substring(0, 200)
|
||||
};
|
||||
});
|
||||
|
||||
console.log('页面元素信息:', allElements);
|
||||
|
||||
expect(title).toBeTruthy();
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('检查页面元素选择器', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const logoImages = await page.locator('header img').all();
|
||||
console.log('找到的logo图片数量:', logoImages.length);
|
||||
|
||||
for (let i = 0; i < logoImages.length; i++) {
|
||||
const img = logoImages[i];
|
||||
const alt = await img.getAttribute('alt');
|
||||
const src = await img.getAttribute('src');
|
||||
console.log(`Logo ${i}: alt="${alt}", src="${src}"`);
|
||||
}
|
||||
|
||||
const contactButtons = await page.locator('a:has-text("立即咨询")').all();
|
||||
console.log('找到的立即咨询按钮数量:', contactButtons.length);
|
||||
|
||||
for (let i = 0; i < contactButtons.length; i++) {
|
||||
const btn = contactButtons[i];
|
||||
const href = await btn.getAttribute('href');
|
||||
const text = await btn.textContent();
|
||||
console.log(`立即咨询按钮 ${i}: text="${text}", href="${href}"`);
|
||||
}
|
||||
|
||||
const mobileMenuButtons = await page.locator('button').all();
|
||||
console.log('找到的按钮数量:', mobileMenuButtons.length);
|
||||
|
||||
for (let i = 0; i < mobileMenuButtons.length; i++) {
|
||||
const btn = mobileMenuButtons[i];
|
||||
const ariaLabel = await btn.getAttribute('aria-label');
|
||||
const text = await btn.textContent();
|
||||
console.log(`按钮 ${i}: aria-label="${ariaLabel}", text="${text}"`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,376 @@
|
||||
import { test, expect } from '../../fixtures/base.fixture';
|
||||
import { PerformanceMonitor } from '../../utils/PerformanceMonitor';
|
||||
|
||||
test.describe('交互性能测试 @performance', () => {
|
||||
test('点击导航项应该快速响应', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
const labels = await homePage.getAllNavigationLabels();
|
||||
if (labels.length > 0) {
|
||||
await homePage.clickNavigationItem(labels[0]);
|
||||
}
|
||||
await homePage.page.waitForTimeout(100);
|
||||
const endTime = Date.now();
|
||||
|
||||
const clickDuration = endTime - startTime;
|
||||
console.log('导航项点击响应时间:', clickDuration, 'ms');
|
||||
|
||||
expect(clickDuration).toBeLessThan(500);
|
||||
});
|
||||
|
||||
test('滚动应该流畅', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await homePage.page.evaluate(() => window.scrollBy(0, 100));
|
||||
await homePage.page.waitForTimeout(30);
|
||||
}
|
||||
const endTime = Date.now();
|
||||
|
||||
const scrollDuration = endTime - startTime;
|
||||
console.log('滚动持续时间:', scrollDuration, 'ms');
|
||||
|
||||
expect(scrollDuration).toBeLessThan(1500);
|
||||
});
|
||||
|
||||
test('表单输入应该快速响应', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
await contactPage.nameInput.fill('测试用户');
|
||||
await contactPage.emailInput.fill('test@example.com');
|
||||
await contactPage.subjectInput.fill('测试主题');
|
||||
await contactPage.messageInput.fill('这是一条测试消息');
|
||||
const endTime = Date.now();
|
||||
|
||||
const inputDuration = endTime - startTime;
|
||||
console.log('表单输入持续时间:', inputDuration, 'ms');
|
||||
|
||||
expect(inputDuration).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
test('按钮点击应该快速响应', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
await homePage.clickContactButton();
|
||||
await homePage.page.waitForTimeout(100);
|
||||
const endTime = Date.now();
|
||||
|
||||
const clickDuration = endTime - startTime;
|
||||
console.log('按钮点击响应时间:', clickDuration, 'ms');
|
||||
|
||||
expect(clickDuration).toBeLessThan(500);
|
||||
});
|
||||
|
||||
test('移动端菜单打开应该快速', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
await homePage.openMobileMenu();
|
||||
const endTime = Date.now();
|
||||
|
||||
const menuOpenDuration = endTime - startTime;
|
||||
console.log('移动端菜单打开时间:', menuOpenDuration, 'ms');
|
||||
|
||||
expect(menuOpenDuration).toBeLessThan(500);
|
||||
});
|
||||
|
||||
test('移动端菜单关闭应该快速', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.openMobileMenu();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
await homePage.closeMobileMenu();
|
||||
const endTime = Date.now();
|
||||
|
||||
const menuCloseDuration = endTime - startTime;
|
||||
console.log('移动端菜单关闭时间:', menuCloseDuration, 'ms');
|
||||
|
||||
expect(menuCloseDuration).toBeLessThan(500);
|
||||
});
|
||||
|
||||
test('页面跳转应该快速', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
await homePage.clickContactButton();
|
||||
await homePage.page.waitForLoadState('networkidle');
|
||||
const endTime = Date.now();
|
||||
|
||||
const navigationDuration = endTime - startTime;
|
||||
console.log('页面跳转持续时间:', navigationDuration, 'ms');
|
||||
|
||||
expect(navigationDuration).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
test('表单提交应该快速', async ({ contactPage, page, testDataGenerator }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
await contactPage.fillContactForm(formData);
|
||||
await contactPage.submitForm();
|
||||
await contactPage.waitForFormSubmission();
|
||||
const endTime = Date.now();
|
||||
|
||||
const submissionDuration = endTime - startTime;
|
||||
console.log('表单提交持续时间:', submissionDuration, 'ms');
|
||||
|
||||
expect(submissionDuration).toBeLessThan(3000);
|
||||
});
|
||||
|
||||
test('悬停效果应该流畅', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
const labels = await homePage.getAllNavigationLabels();
|
||||
if (labels.length > 0) {
|
||||
const firstNavItem = homePage.navigation.locator('a').first();
|
||||
await firstNavItem.hover();
|
||||
await homePage.page.waitForTimeout(100);
|
||||
}
|
||||
const endTime = Date.now();
|
||||
|
||||
const hoverDuration = endTime - startTime;
|
||||
console.log('悬停效果持续时间:', hoverDuration, 'ms');
|
||||
|
||||
expect(hoverDuration).toBeLessThan(300);
|
||||
});
|
||||
|
||||
test('快速连续点击应该正常响应', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
const labels = await homePage.getAllNavigationLabels();
|
||||
for (let i = 0; i < Math.min(labels.length, 3); i++) {
|
||||
await homePage.clickNavigationItem(labels[i]);
|
||||
await homePage.page.waitForTimeout(200);
|
||||
}
|
||||
const endTime = Date.now();
|
||||
|
||||
const rapidClickDuration = endTime - startTime;
|
||||
console.log('快速连续点击持续时间:', rapidClickDuration, 'ms');
|
||||
|
||||
expect(rapidClickDuration).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
test('快速滚动应该流畅', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await homePage.page.evaluate(() => window.scrollBy(0, 500));
|
||||
await homePage.page.waitForTimeout(50);
|
||||
}
|
||||
const endTime = Date.now();
|
||||
|
||||
const rapidScrollDuration = endTime - startTime;
|
||||
console.log('快速滚动持续时间:', rapidScrollDuration, 'ms');
|
||||
|
||||
expect(rapidScrollDuration).toBeLessThan(1500);
|
||||
});
|
||||
|
||||
test('表单验证应该快速', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
await contactPage.emailInput.fill('invalid-email');
|
||||
await contactPage.page.waitForTimeout(100);
|
||||
const isValid = await contactPage.isEmailValid();
|
||||
const endTime = Date.now();
|
||||
|
||||
const validationDuration = endTime - startTime;
|
||||
console.log('表单验证持续时间:', validationDuration, 'ms');
|
||||
|
||||
expect(validationDuration).toBeLessThan(300);
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
|
||||
test('键盘导航应该流畅', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await homePage.page.keyboard.press('Tab');
|
||||
await homePage.page.waitForTimeout(50);
|
||||
}
|
||||
const endTime = Date.now();
|
||||
|
||||
const keyboardNavDuration = endTime - startTime;
|
||||
console.log('键盘导航持续时间:', keyboardNavDuration, 'ms');
|
||||
|
||||
expect(keyboardNavDuration).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
test('页面重新加载应该快速', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
await homePage.reload();
|
||||
await homePage.waitForPageLoad();
|
||||
const endTime = Date.now();
|
||||
|
||||
const reloadDuration = endTime - startTime;
|
||||
console.log('页面重新加载持续时间:', reloadDuration, 'ms');
|
||||
|
||||
expect(reloadDuration).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
test('返回上一页应该快速', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.clickContactButton();
|
||||
await homePage.page.waitForLoadState('networkidle');
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
await homePage.goBack();
|
||||
await homePage.page.waitForLoadState('networkidle');
|
||||
const endTime = Date.now();
|
||||
|
||||
const backDuration = endTime - startTime;
|
||||
console.log('返回上一页持续时间:', backDuration, 'ms');
|
||||
|
||||
expect(backDuration).toBeLessThan(1500);
|
||||
});
|
||||
|
||||
test('前进到下一页应该快速', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.clickContactButton();
|
||||
await homePage.page.waitForLoadState('networkidle');
|
||||
await homePage.goBack();
|
||||
await homePage.page.waitForLoadState('networkidle');
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
await homePage.goForward();
|
||||
await homePage.page.waitForLoadState('networkidle');
|
||||
const endTime = Date.now();
|
||||
|
||||
const forwardDuration = endTime - startTime;
|
||||
console.log('前进到下一页持续时间:', forwardDuration, 'ms');
|
||||
|
||||
expect(forwardDuration).toBeLessThan(1500);
|
||||
});
|
||||
|
||||
test('窗口大小调整应该流畅', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const startTime = Date.now();
|
||||
await homePage.page.setViewportSize({ width: 768, height: 1024 });
|
||||
await homePage.page.waitForTimeout(100);
|
||||
await homePage.page.setViewportSize({ width: 1280, height: 720 });
|
||||
await homePage.page.waitForTimeout(100);
|
||||
const endTime = Date.now();
|
||||
|
||||
const resizeDuration = endTime - startTime;
|
||||
console.log('窗口大小调整持续时间:', resizeDuration, 'ms');
|
||||
|
||||
expect(resizeDuration).toBeLessThan(500);
|
||||
});
|
||||
|
||||
test('所有交互应该在合理时间内完成', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
const interactions: { name: string; duration: number }[] = [];
|
||||
|
||||
const startClick = Date.now();
|
||||
await homePage.clickContactButton();
|
||||
await homePage.page.waitForLoadState('networkidle');
|
||||
interactions.push({ name: '点击按钮', duration: Date.now() - startClick });
|
||||
|
||||
await homePage.goBack();
|
||||
const startScroll = Date.now();
|
||||
await homePage.scrollToSection('services');
|
||||
interactions.push({ name: '滚动到区块', duration: Date.now() - startScroll });
|
||||
|
||||
const startNav = Date.now();
|
||||
const labels = await homePage.getAllNavigationLabels();
|
||||
if (labels.length > 0) {
|
||||
await homePage.clickNavigationItem(labels[0]);
|
||||
}
|
||||
interactions.push({ name: '导航点击', duration: Date.now() - startNav });
|
||||
|
||||
console.log('交互性能汇总:');
|
||||
interactions.forEach(interaction => {
|
||||
console.log(`- ${interaction.name}: ${interaction.duration}ms`);
|
||||
});
|
||||
|
||||
interactions.forEach(interaction => {
|
||||
expect(interaction.duration).toBeLessThan(2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,271 @@
|
||||
import { test, expect } from '../../fixtures/base.fixture';
|
||||
import { PerformanceMonitor } from '../../utils/PerformanceMonitor';
|
||||
import { PerformanceThresholds } from '../../types';
|
||||
|
||||
const performanceThresholds: PerformanceThresholds = {
|
||||
loadTime: 3000,
|
||||
firstContentfulPaint: 1800,
|
||||
largestContentfulPaint: 2500,
|
||||
timeToInteractive: 3500,
|
||||
cumulativeLayoutShift: 0.1,
|
||||
firstInputDelay: 100,
|
||||
};
|
||||
|
||||
test.describe('性能测试 @performance', () => {
|
||||
test('首页加载时间应该在阈值范围内', async ({ homePage, page }) => {
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const metrics = await monitor.collectMetrics();
|
||||
const validation = monitor.validateMetrics(performanceThresholds);
|
||||
|
||||
console.log('首页性能指标:', metrics);
|
||||
console.log('验证结果:', validation);
|
||||
|
||||
expect(validation.passed).toBe(true);
|
||||
expect(metrics.loadTime).toBeLessThan(performanceThresholds.loadTime);
|
||||
});
|
||||
|
||||
test('联系页面加载时间应该在阈值范围内', async ({ contactPage, page }) => {
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
const metrics = await monitor.collectMetrics();
|
||||
const validation = monitor.validateMetrics(performanceThresholds);
|
||||
|
||||
console.log('联系页面性能指标:', metrics);
|
||||
console.log('验证结果:', validation);
|
||||
|
||||
expect(validation.passed).toBe(true);
|
||||
expect(metrics.loadTime).toBeLessThan(performanceThresholds.loadTime);
|
||||
});
|
||||
|
||||
test('首次内容绘制应该在1.8秒内完成', async ({ homePage, page }) => {
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const fcp = await monitor.measureFirstContentfulPaint();
|
||||
|
||||
console.log('首次内容绘制时间:', fcp, 'ms');
|
||||
|
||||
expect(fcp).toBeLessThan(performanceThresholds.firstContentfulPaint);
|
||||
expect(fcp).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('最大内容绘制应该在2.5秒内完成', async ({ homePage, page }) => {
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const lcp = await monitor.measureLargestContentfulPaint();
|
||||
|
||||
console.log('最大内容绘制时间:', lcp, 'ms');
|
||||
|
||||
expect(lcp).toBeLessThan(performanceThresholds.largestContentfulPaint);
|
||||
expect(lcp).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('累积布局偏移应该小于0.1', async ({ homePage, page }) => {
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.scrollToSection('services');
|
||||
await homePage.scrollToSection('products');
|
||||
await homePage.scrollToSection('cases');
|
||||
|
||||
const cls = await monitor.measureCumulativeLayoutShift();
|
||||
|
||||
console.log('累积布局偏移:', cls);
|
||||
|
||||
expect(cls).toBeLessThan(performanceThresholds.cumulativeLayoutShift);
|
||||
expect(cls).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('首次输入延迟应该小于100ms', async ({ homePage, page }) => {
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.logo.click();
|
||||
|
||||
const fid = await monitor.measureFirstInputDelay();
|
||||
|
||||
console.log('首次输入延迟:', fid, 'ms');
|
||||
|
||||
expect(fid).toBeLessThan(performanceThresholds.firstInputDelay);
|
||||
expect(fid).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('可交互时间应该在3.5秒内完成', async ({ homePage, page }) => {
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const tti = await monitor.measureTimeToInteractive();
|
||||
|
||||
console.log('可交互时间:', tti, 'ms');
|
||||
|
||||
expect(tti).toBeLessThan(performanceThresholds.timeToInteractive);
|
||||
expect(tti).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('页面应该有良好的帧率', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
const frameRate = await monitor.measureFrameRate();
|
||||
|
||||
console.log('帧率:', frameRate, 'FPS');
|
||||
|
||||
expect(frameRate).toBeGreaterThan(30);
|
||||
});
|
||||
|
||||
test('资源加载应该高效', async ({ homePage, page }) => {
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const resources = await monitor.measureResourceTiming();
|
||||
const totalSize = resources.reduce((sum, r) => sum + (r.size || 0), 0);
|
||||
const totalSizeKB = totalSize / 1024;
|
||||
|
||||
console.log('总资源大小:', totalSizeKB.toFixed(2), 'KB');
|
||||
console.log('资源数量:', resources.length);
|
||||
|
||||
expect(totalSizeKB).toBeLessThan(3000);
|
||||
expect(resources.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('DOM内容加载应该快速', async ({ homePage, page }) => {
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
await homePage.goto();
|
||||
|
||||
const dcl = await monitor.measureDomContentLoaded();
|
||||
|
||||
console.log('DOM内容加载时间:', dcl, 'ms');
|
||||
|
||||
expect(dcl).toBeLessThan(2000);
|
||||
expect(dcl).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该生成性能报告', async ({ homePage, page }) => {
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const report = await monitor.generateReport();
|
||||
|
||||
console.log('性能报告:');
|
||||
console.log(report);
|
||||
|
||||
expect(report).toContain('页面加载时间');
|
||||
expect(report).toContain('首次内容绘制');
|
||||
expect(report).toContain('最大内容绘制');
|
||||
expect(report).toContain('累积布局偏移');
|
||||
expect(report).toContain('资源加载');
|
||||
});
|
||||
|
||||
test('滚动性能应该良好', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await homePage.page.evaluate(() => window.scrollBy(0, 200));
|
||||
await homePage.page.waitForTimeout(50);
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const scrollDuration = endTime - startTime;
|
||||
|
||||
console.log('滚动持续时间:', scrollDuration, 'ms');
|
||||
|
||||
expect(scrollDuration).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
test('导航性能应该良好', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const startTime = Date.now();
|
||||
await homePage.clickContactButton();
|
||||
await homePage.page.waitForLoadState('networkidle');
|
||||
const endTime = Date.now();
|
||||
|
||||
const navigationDuration = endTime - startTime;
|
||||
|
||||
console.log('导航持续时间:', navigationDuration, 'ms');
|
||||
|
||||
expect(navigationDuration).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
test('表单提交性能应该良好', async ({ contactPage, page, testDataGenerator }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
|
||||
const startTime = Date.now();
|
||||
await contactPage.fillContactForm(formData);
|
||||
await contactPage.submitForm();
|
||||
await contactPage.waitForFormSubmission();
|
||||
const endTime = Date.now();
|
||||
|
||||
const submissionDuration = endTime - startTime;
|
||||
|
||||
console.log('表单提交持续时间:', submissionDuration, 'ms');
|
||||
|
||||
expect(submissionDuration).toBeLessThan(3000);
|
||||
});
|
||||
|
||||
test('所有核心性能指标应该符合标准', async ({ homePage, page }) => {
|
||||
const monitor = new PerformanceMonitor(page);
|
||||
await monitor.startMonitoring();
|
||||
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const metrics = await monitor.collectMetrics();
|
||||
const validation = monitor.validateMetrics(performanceThresholds);
|
||||
|
||||
console.log('完整性能指标:', metrics);
|
||||
console.log('验证结果:', validation);
|
||||
|
||||
if (!validation.passed) {
|
||||
console.error('性能违规:', validation.violations);
|
||||
}
|
||||
|
||||
expect(metrics.loadTime).toBeLessThan(performanceThresholds.loadTime);
|
||||
expect(metrics.firstContentfulPaint).toBeLessThan(performanceThresholds.firstContentfulPaint);
|
||||
expect(metrics.largestContentfulPaint).toBeLessThan(performanceThresholds.largestContentfulPaint);
|
||||
expect(metrics.timeToInteractive).toBeLessThan(performanceThresholds.timeToInteractive);
|
||||
expect(metrics.cumulativeLayoutShift).toBeLessThan(performanceThresholds.cumulativeLayoutShift);
|
||||
expect(metrics.firstInputDelay).toBeLessThan(performanceThresholds.firstInputDelay);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
import { test, expect } from '../../fixtures/base.fixture';
|
||||
|
||||
test.describe('联系表单回归测试 @regression', () => {
|
||||
test.beforeEach(async ({ contactPage }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
});
|
||||
|
||||
test('应该能够提交完整的表单', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillAndSubmitForm(formData);
|
||||
await contactPage.waitForFormSubmission();
|
||||
const isSubmitted = await contactPage.isFormSubmitted();
|
||||
expect(isSubmitted).toBe(true);
|
||||
});
|
||||
|
||||
test('应该验证必填字段', async ({ contactPage }) => {
|
||||
await contactPage.submitForm();
|
||||
await contactPage.waitForFormSubmission();
|
||||
const isSubmitted = await contactPage.isFormSubmitted();
|
||||
expect(isSubmitted).toBe(false);
|
||||
});
|
||||
|
||||
test('应该验证邮箱格式', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
formData.email = testDataGenerator.generateInvalidEmail();
|
||||
await contactPage.fillContactForm(formData);
|
||||
const isValid = await contactPage.isEmailValid();
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
|
||||
test('应该能够清空表单', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillContactForm(formData);
|
||||
await contactPage.clearForm();
|
||||
|
||||
const nameValue = await contactPage.getNameInputValue();
|
||||
const emailValue = await contactPage.getEmailInputValue();
|
||||
const subjectValue = await contactPage.getSubjectInputValue();
|
||||
const messageValue = await contactPage.getMessageInputValue();
|
||||
|
||||
expect(nameValue).toBe('');
|
||||
expect(emailValue).toBe('');
|
||||
expect(subjectValue).toBe('');
|
||||
expect(messageValue).toBe('');
|
||||
});
|
||||
|
||||
test('应该能够输入长消息', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
formData.message = testDataGenerator.generateMessage(200, 500);
|
||||
await contactPage.fillContactForm(formData);
|
||||
const messageValue = await contactPage.getMessageInputValue();
|
||||
expect(messageValue).toBe(formData.message);
|
||||
});
|
||||
|
||||
test('应该能够输入特殊字符', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
formData.message = testDataGenerator.generateSpecialCharacters();
|
||||
await contactPage.fillContactForm(formData);
|
||||
const messageValue = await contactPage.getMessageInputValue();
|
||||
expect(messageValue).toBe(formData.message);
|
||||
});
|
||||
|
||||
test('应该能够输入中文字符', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
formData.message = testDataGenerator.generateChineseCharacters();
|
||||
await contactPage.fillContactForm(formData);
|
||||
const messageValue = await contactPage.getMessageInputValue();
|
||||
expect(messageValue).toBe(formData.message);
|
||||
});
|
||||
|
||||
test('应该能够输入混合内容', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
formData.message = testDataGenerator.generateMixedContent();
|
||||
await contactPage.fillContactForm(formData);
|
||||
const messageValue = await contactPage.getMessageInputValue();
|
||||
expect(messageValue).toBe(formData.message);
|
||||
});
|
||||
|
||||
test('应该能够聚焦和失焦字段', async ({ contactPage }) => {
|
||||
await contactPage.focusOnField('name');
|
||||
const isNameFocused = await contactPage.nameInput.evaluate(el => document.activeElement === el);
|
||||
expect(isNameFocused).toBe(true);
|
||||
|
||||
await contactPage.blurField('name');
|
||||
const isNameBlurred = await contactPage.nameInput.evaluate(el => document.activeElement !== el);
|
||||
expect(isNameBlurred).toBe(true);
|
||||
});
|
||||
|
||||
test('应该能够使用键盘导航表单', async ({ contactPage }) => {
|
||||
await contactPage.nameInput.focus();
|
||||
await contactPage.page.keyboard.press('Tab');
|
||||
await contactPage.page.waitForTimeout(100);
|
||||
const focusedElement = await contactPage.page.evaluate(() => document.activeElement?.tagName);
|
||||
expect(focusedElement).toBe('INPUT');
|
||||
});
|
||||
|
||||
test('应该能够使用回车键提交表单', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillContactForm(formData);
|
||||
await contactPage.messageInput.press('Enter');
|
||||
await contactPage.waitForFormSubmission();
|
||||
const isSubmitted = await contactPage.isFormSubmitted();
|
||||
expect(isSubmitted).toBe(true);
|
||||
});
|
||||
|
||||
test('应该显示提交按钮的加载状态', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillContactForm(formData);
|
||||
await contactPage.submitButton.click();
|
||||
await contactPage.page.waitForTimeout(500);
|
||||
const isLoading = await contactPage.isSubmitButtonLoading();
|
||||
expect(isLoading).toBe(true);
|
||||
});
|
||||
|
||||
test('应该显示成功消息', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillAndSubmitForm(formData);
|
||||
await contactPage.waitForFormSubmission();
|
||||
const isSuccessVisible = await contactPage.isSuccessMessageVisible();
|
||||
expect(isSuccessVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('应该显示正确的成功消息文本', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillAndSubmitForm(formData);
|
||||
await contactPage.waitForFormSubmission();
|
||||
const successText = await contactPage.getSuccessMessageText();
|
||||
expect(successText).toContain('消息已发送');
|
||||
});
|
||||
|
||||
test('应该能够重新提交表单', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData1 = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillAndSubmitForm(formData1);
|
||||
await contactPage.waitForFormSubmission();
|
||||
|
||||
await contactPage.page.reload();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
const formData2 = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillAndSubmitForm(formData2);
|
||||
await contactPage.waitForFormSubmission();
|
||||
const isSubmitted = await contactPage.isFormSubmitted();
|
||||
expect(isSubmitted).toBe(true);
|
||||
});
|
||||
|
||||
test('应该能够输入空格', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
formData.message = ' 测试消息 ';
|
||||
await contactPage.fillContactForm(formData);
|
||||
const messageValue = await contactPage.getMessageInputValue();
|
||||
expect(messageValue).toBe(' 测试消息 ');
|
||||
});
|
||||
|
||||
test('应该能够输入换行符', async ({ contactPage }) => {
|
||||
const message = '第一行\n第二行\n第三行';
|
||||
await contactPage.messageInput.fill(message);
|
||||
const messageValue = await contactPage.getMessageInputValue();
|
||||
expect(messageValue).toBe(message);
|
||||
});
|
||||
|
||||
test('应该能够输入URL', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
formData.message = `请查看我的网站:${testDataGenerator.generateUrl()}`;
|
||||
await contactPage.fillContactForm(formData);
|
||||
const messageValue = await contactPage.getMessageInputValue();
|
||||
expect(messageValue).toContain('http');
|
||||
});
|
||||
|
||||
test('应该能够输入数字', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
formData.phone = testDataGenerator.generatePhone();
|
||||
await contactPage.fillContactForm(formData);
|
||||
const phoneValue = await contactPage.getPhoneInputValue();
|
||||
expect(phoneValue).toBe(formData.phone);
|
||||
});
|
||||
|
||||
test('应该能够输入电子邮件地址', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillContactForm(formData);
|
||||
const emailValue = await contactPage.getEmailInputValue();
|
||||
expect(emailValue).toContain('@');
|
||||
expect(emailValue).toContain('.');
|
||||
});
|
||||
|
||||
test('应该能够截取表单截图', async ({ contactPage }) => {
|
||||
await contactPage.scrollToForm();
|
||||
const isVisible = await contactPage.contactForm.isVisible();
|
||||
expect(isVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('应该能够截取成功消息截图', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillAndSubmitForm(formData);
|
||||
await contactPage.waitForFormSubmission();
|
||||
const isSuccessVisible = await contactPage.isSuccessMessageVisible();
|
||||
expect(isSuccessVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('应该正确处理表单重置', async ({ contactPage, testDataGenerator }) => {
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillContactForm(formData);
|
||||
await contactPage.page.reload();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
const nameValue = await contactPage.getNameInputValue();
|
||||
const emailValue = await contactPage.getEmailInputValue();
|
||||
const subjectValue = await contactPage.getSubjectInputValue();
|
||||
const messageValue = await contactPage.getMessageInputValue();
|
||||
|
||||
expect(nameValue).toBe('');
|
||||
expect(emailValue).toBe('');
|
||||
expect(subjectValue).toBe('');
|
||||
expect(messageValue).toBe('');
|
||||
});
|
||||
|
||||
test('应该没有JavaScript错误', async ({ contactPage, page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('pageerror', error => {
|
||||
errors.push(error.toString());
|
||||
});
|
||||
await contactPage.waitForPageLoad();
|
||||
const formData = { name: '测试', email: 'test@example.com', subject: '测试', message: '测试消息' };
|
||||
await contactPage.fillContactForm(formData);
|
||||
await contactPage.submitForm();
|
||||
await contactPage.waitForFormSubmission();
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
|
||||
test('应该正确处理网络请求', async ({ contactPage, page }) => {
|
||||
const failedRequests: string[] = [];
|
||||
page.on('requestfailed', request => {
|
||||
failedRequests.push(request.url());
|
||||
});
|
||||
await contactPage.waitForPageLoad();
|
||||
await contactPage.page.waitForTimeout(2000);
|
||||
expect(failedRequests.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
import { test, expect } from '../../fixtures/base.fixture';
|
||||
|
||||
test.describe('首页回归测试 @regression', () => {
|
||||
test.beforeEach(async ({ homePage }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
});
|
||||
|
||||
test('应该正确响应滚动事件', async ({ homePage }) => {
|
||||
const initialBg = await homePage.getHeaderBackgroundColor();
|
||||
await homePage.scrollToSection('services');
|
||||
await homePage.page.waitForTimeout(500);
|
||||
const scrolledBg = await homePage.getHeaderBackgroundColor();
|
||||
expect(scrolledBg).not.toBe(initialBg);
|
||||
});
|
||||
|
||||
test('应该显示粘性头部', async ({ homePage }) => {
|
||||
const isSticky = await homePage.isHeaderSticky();
|
||||
expect(isSticky).toBe(true);
|
||||
});
|
||||
|
||||
test('滚动后应该显示头部阴影', async ({ homePage }) => {
|
||||
await homePage.scrollToSection('services');
|
||||
await homePage.page.waitForTimeout(500);
|
||||
const hasShadow = await homePage.isHeaderScrolled();
|
||||
expect(hasShadow).toBe(true);
|
||||
});
|
||||
|
||||
test('应该能够通过导航跳转到各个区块', async ({ homePage }) => {
|
||||
const labels = await homePage.getAllNavigationLabels();
|
||||
for (let i = 0; i < Math.min(labels.length, 3); i++) {
|
||||
await homePage.clickNavigationItem(labels[i]);
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
const url = homePage.page.url();
|
||||
expect(url).toContain('#');
|
||||
}
|
||||
});
|
||||
|
||||
test('应该正确高亮当前区块的导航项', async ({ homePage }) => {
|
||||
await homePage.scrollToSection('services');
|
||||
await homePage.page.waitForTimeout(500);
|
||||
const activeItem = await homePage.getActiveNavigationItem();
|
||||
expect(activeItem).toBeTruthy();
|
||||
});
|
||||
|
||||
test('应该能够点击Logo返回首页', async ({ homePage }) => {
|
||||
await homePage.scrollToSection('services');
|
||||
await homePage.logo.click();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
const url = homePage.page.url();
|
||||
expect(url).toMatch(/\/$/);
|
||||
});
|
||||
|
||||
test('应该能够通过立即咨询按钮跳转到联系页面', async ({ homePage }) => {
|
||||
await homePage.clickContactButton();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
const url = homePage.page.url();
|
||||
expect(url).toContain('/contact');
|
||||
});
|
||||
|
||||
test('应该能够打开和关闭移动端菜单', async ({ homePage }) => {
|
||||
await homePage.openMobileMenu();
|
||||
await expect(homePage.mobileMenu).toBeVisible();
|
||||
|
||||
await homePage.closeMobileMenu();
|
||||
await expect(homePage.mobileMenu).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('移动端菜单应该包含所有导航项', async ({ homePage }) => {
|
||||
await homePage.openMobileMenu();
|
||||
const desktopNavItems = await homePage.getAllNavigationLabels();
|
||||
const mobileNavItems = homePage.mobileMenu.locator('a');
|
||||
const mobileCount = await mobileNavItems.count();
|
||||
expect(mobileCount).toBeGreaterThan(0);
|
||||
expect(mobileCount).toBe(desktopNavItems.length);
|
||||
});
|
||||
|
||||
test('应该能够通过移动端菜单导航', async ({ homePage }) => {
|
||||
await homePage.openMobileMenu();
|
||||
const firstLink = homePage.mobileMenu.locator('a').first();
|
||||
await firstLink.click();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
const isMenuVisible = await homePage.mobileMenu.isVisible();
|
||||
expect(isMenuVisible).toBe(false);
|
||||
});
|
||||
|
||||
test('应该能够平滑滚动到各个区块', async ({ homePage }) => {
|
||||
const sections = ['services', 'products', 'cases'];
|
||||
for (const sectionId of sections) {
|
||||
await homePage.scrollToSection(sectionId);
|
||||
const isVisible = await homePage.isSectionVisible(sectionId);
|
||||
expect(isVisible).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('应该正确显示所有区块标题', async ({ homePage }) => {
|
||||
const titles = [
|
||||
await homePage.getHeroSectionTitle(),
|
||||
await homePage.getServicesSectionTitle(),
|
||||
await homePage.getProductsSectionTitle(),
|
||||
await homePage.getCasesSectionTitle(),
|
||||
await homePage.getAboutSectionTitle(),
|
||||
await homePage.getNewsSectionTitle(),
|
||||
await homePage.getContactSectionTitle(),
|
||||
];
|
||||
titles.forEach(title => {
|
||||
expect(title).toBeTruthy();
|
||||
expect(title.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('应该能够滚动到页面底部并返回顶部', async ({ homePage }) => {
|
||||
await homePage.scrollToBottom();
|
||||
const bottomScroll = await homePage.page.evaluate(() => window.scrollY);
|
||||
expect(bottomScroll).toBeGreaterThan(0);
|
||||
|
||||
await homePage.scrollToTop();
|
||||
const topScroll = await homePage.page.evaluate(() => window.scrollY);
|
||||
expect(topScroll).toBe(0);
|
||||
});
|
||||
|
||||
test('应该正确处理快速滚动', async ({ homePage }) => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await homePage.page.evaluate(() => window.scrollBy(0, 500));
|
||||
await homePage.page.waitForTimeout(100);
|
||||
}
|
||||
const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
|
||||
expect(scrollPosition).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该能够截取各个区块的截图', async ({ homePage }) => {
|
||||
const sections = ['home', 'services'];
|
||||
for (const sectionId of sections) {
|
||||
await homePage.scrollToSection(sectionId);
|
||||
await homePage.page.waitForTimeout(500);
|
||||
const isVisible = await homePage.isSectionVisible(sectionId);
|
||||
expect(isVisible).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('应该正确响应窗口大小变化', async ({ homePage }) => {
|
||||
await homePage.page.setViewportSize({ width: 768, height: 1024 });
|
||||
await homePage.page.waitForTimeout(500);
|
||||
await expect(homePage.header).toBeVisible();
|
||||
|
||||
await homePage.page.setViewportSize({ width: 1280, height: 720 });
|
||||
await homePage.page.waitForTimeout(500);
|
||||
await expect(homePage.header).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能够通过键盘导航', async ({ homePage }) => {
|
||||
await homePage.page.keyboard.press('Tab');
|
||||
await homePage.page.waitForTimeout(100);
|
||||
const focusedElement = await homePage.page.evaluate(() => document.activeElement?.tagName);
|
||||
expect(focusedElement).toBeTruthy();
|
||||
});
|
||||
|
||||
test('应该正确显示页脚内容', async ({ homePage }) => {
|
||||
await homePage.scrollToBottom();
|
||||
const footerText = await homePage.getFooterText();
|
||||
expect(footerText).toBeTruthy();
|
||||
expect(footerText.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该能够重新加载页面', async ({ homePage }) => {
|
||||
await homePage.reload();
|
||||
await homePage.waitForPageLoad();
|
||||
await expect(homePage.header).toBeVisible();
|
||||
await expect(homePage.heroSection).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能够使用浏览器后退按钮', async ({ homePage }) => {
|
||||
await homePage.clickContactButton();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
await homePage.goBack();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
const url = homePage.page.url();
|
||||
expect(url).toMatch(/\/$/);
|
||||
});
|
||||
|
||||
test('应该能够使用浏览器前进按钮', async ({ homePage }) => {
|
||||
await homePage.clickContactButton();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
await homePage.goBack();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
await homePage.goForward();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
const url = homePage.page.url();
|
||||
expect(url).toContain('/contact');
|
||||
});
|
||||
|
||||
test('应该没有JavaScript错误', async ({ homePage, page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('pageerror', error => {
|
||||
errors.push(error.toString());
|
||||
});
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.scrollToSection('services');
|
||||
await homePage.scrollToSection('products');
|
||||
await homePage.scrollToSection('cases');
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
|
||||
test('应该正确处理网络请求', async ({ homePage, page }) => {
|
||||
const failedRequests: string[] = [];
|
||||
page.on('requestfailed', request => {
|
||||
failedRequests.push(request.url());
|
||||
});
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.page.waitForTimeout(2000);
|
||||
expect(failedRequests.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,356 @@
|
||||
import { test, expect } from '../../fixtures/base.fixture';
|
||||
import { devices, getMobileDevices } from '../../utils/devices';
|
||||
|
||||
test.describe('移动端交互测试 @responsive', () => {
|
||||
for (const device of getMobileDevices()) {
|
||||
test(`移动端 ${device.name} - 应该能够打开和关闭菜单`, async ({ homePage, page }) => {
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.mobileMenuButton).toBeVisible();
|
||||
|
||||
await homePage.openMobileMenu();
|
||||
await expect(homePage.mobileMenu).toBeVisible();
|
||||
|
||||
await homePage.closeMobileMenu();
|
||||
await expect(homePage.mobileMenu).not.toBeVisible();
|
||||
});
|
||||
}
|
||||
|
||||
test('移动端 - 应该能够通过菜单导航', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.openMobileMenu();
|
||||
const firstLink = homePage.mobileMenu.locator('a').first();
|
||||
await firstLink.click();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
|
||||
const isMenuVisible = await homePage.mobileMenu.isVisible();
|
||||
expect(isMenuVisible).toBe(false);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够滚动页面', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const initialScroll = await homePage.page.evaluate(() => window.scrollY);
|
||||
expect(initialScroll).toBe(0);
|
||||
|
||||
await homePage.scrollToSection('services');
|
||||
const afterScroll = await homePage.page.evaluate(() => window.scrollY);
|
||||
expect(afterScroll).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够点击Logo', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.logo.click();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
|
||||
const url = homePage.page.url();
|
||||
expect(url).toMatch(/\/$/);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够点击联系按钮', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.openMobileMenu();
|
||||
const contactButton = homePage.mobileMenu.locator('a:has-text("联系我们")').first();
|
||||
await contactButton.click();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
|
||||
const url = homePage.page.url();
|
||||
expect(url).toContain('/contact');
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够快速滚动', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const startTime = Date.now();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await homePage.page.evaluate(() => window.scrollBy(0, 200));
|
||||
await homePage.page.waitForTimeout(50);
|
||||
}
|
||||
const endTime = Date.now();
|
||||
|
||||
const scrollDuration = endTime - startTime;
|
||||
expect(scrollDuration).toBeLessThan(1500);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够触摸交互', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const heroSection = homePage.heroSection;
|
||||
await heroSection.tap();
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
const isVisible = await heroSection.isVisible();
|
||||
expect(isVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够滑动页面', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const heroSection = homePage.heroSection;
|
||||
await heroSection.tap();
|
||||
|
||||
const startX = await homePage.page.evaluate(() => window.scrollX);
|
||||
const startY = await homePage.page.evaluate(() => window.scrollY);
|
||||
|
||||
await homePage.page.touchscreen.tap(200, 500);
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
const endX = await homePage.page.evaluate(() => window.scrollX);
|
||||
const endY = await homePage.page.evaluate(() => window.scrollY);
|
||||
|
||||
expect(endY).toBeGreaterThan(startY);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够双击缩放', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const heroSection = homePage.heroSection;
|
||||
await heroSection.tap();
|
||||
await homePage.page.waitForTimeout(200);
|
||||
await heroSection.tap();
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
const isVisible = await heroSection.isVisible();
|
||||
expect(isVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够长按', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const heroSection = homePage.heroSection;
|
||||
await heroSection.tap();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
|
||||
const isVisible = await heroSection.isVisible();
|
||||
expect(isVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够使用键盘导航', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.page.keyboard.press('Tab');
|
||||
await homePage.page.waitForTimeout(100);
|
||||
|
||||
const focusedElement = await homePage.page.evaluate(() => document.activeElement?.tagName);
|
||||
expect(focusedElement).toBeTruthy();
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够输入表单', async ({ contactPage, page, testDataGenerator }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillContactForm(formData);
|
||||
|
||||
const nameValue = await contactPage.getNameInputValue();
|
||||
const emailValue = await contactPage.getEmailInputValue();
|
||||
|
||||
expect(nameValue).toBe(formData.name);
|
||||
expect(emailValue).toBe(formData.email);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够提交表单', async ({ contactPage, page, testDataGenerator }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillAndSubmitForm(formData);
|
||||
await contactPage.waitForFormSubmission();
|
||||
|
||||
const isSubmitted = await contactPage.isFormSubmitted();
|
||||
expect(isSubmitted).toBe(true);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够滚动到表单', async ({ contactPage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await contactPage.scrollToForm();
|
||||
const isVisible = await contactPage.contactForm.isVisible();
|
||||
expect(isVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够点击表单字段', async ({ contactPage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await contactPage.nameInput.tap();
|
||||
await homePage.page.waitForTimeout(100);
|
||||
|
||||
const isFocused = await contactPage.nameInput.evaluate(el => document.activeElement === el);
|
||||
expect(isFocused).toBe(true);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够使用返回按钮', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.clickContactButton();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
|
||||
await homePage.goBack();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
|
||||
const url = homePage.page.url();
|
||||
expect(url).toMatch(/\/$/);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够使用前进按钮', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.clickContactButton();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
await homePage.goBack();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
await homePage.goForward();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
|
||||
const url = homePage.page.url();
|
||||
expect(url).toContain('/contact');
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够刷新页面', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.reload();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.header).toBeVisible();
|
||||
await expect(homePage.heroSection).toBeVisible();
|
||||
});
|
||||
|
||||
test('移动端 - 应该没有JavaScript错误', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
|
||||
const errors: string[] = [];
|
||||
page.on('pageerror', error => {
|
||||
errors.push(error.toString());
|
||||
});
|
||||
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.scrollToSection('services');
|
||||
await homePage.scrollToSection('products');
|
||||
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够处理快速连续点击', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.openMobileMenu();
|
||||
|
||||
const startTime = Date.now();
|
||||
const links = homePage.mobileMenu.locator('a');
|
||||
const count = await links.count();
|
||||
|
||||
for (let i = 0; i < Math.min(count, 3); i++) {
|
||||
await links.nth(i).tap();
|
||||
await homePage.page.waitForTimeout(200);
|
||||
}
|
||||
const endTime = Date.now();
|
||||
|
||||
const rapidClickDuration = endTime - startTime;
|
||||
expect(rapidClickDuration).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够处理快速滚动', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const startTime = Date.now();
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await homePage.page.evaluate(() => window.scrollBy(0, 100));
|
||||
await homePage.page.waitForTimeout(30);
|
||||
}
|
||||
const endTime = Date.now();
|
||||
|
||||
const rapidScrollDuration = endTime - startTime;
|
||||
expect(rapidScrollDuration).toBeLessThan(1500);
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够处理方向变化', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await page.setViewportSize({ width: 667, height: 375 });
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
await expect(homePage.header).toBeVisible();
|
||||
await expect(homePage.heroSection).toBeVisible();
|
||||
});
|
||||
|
||||
test('移动端 - 应该能够处理键盘弹出', async ({ contactPage, page, testDataGenerator }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
await contactPage.nameInput.tap();
|
||||
await contactPage.nameInput.fill(formData.name);
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
const nameValue = await contactPage.getNameInputValue();
|
||||
expect(nameValue).toBe(formData.name);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,339 @@
|
||||
import { test, expect } from '../../fixtures/base.fixture';
|
||||
import { devices, getDesktopDevices, getMobileDevices, getTabletDevices } from '../../utils/devices';
|
||||
|
||||
test.describe('响应式测试 @responsive', () => {
|
||||
for (const device of getDesktopDevices()) {
|
||||
test(`桌面端 ${device.name} - 首页应该正常显示`, async ({ homePage, page }) => {
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.header).toBeVisible();
|
||||
await expect(homePage.heroSection).toBeVisible();
|
||||
await expect(homePage.footer).toBeVisible();
|
||||
await expect(homePage.navigation).toBeVisible();
|
||||
await expect(homePage.mobileMenuButton).not.toBeVisible();
|
||||
});
|
||||
}
|
||||
|
||||
for (const device of getMobileDevices()) {
|
||||
test(`移动端 ${device.name} - 首页应该正常显示`, async ({ homePage, page }) => {
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.header).toBeVisible();
|
||||
await expect(homePage.heroSection).toBeVisible();
|
||||
await expect(homePage.footer).toBeVisible();
|
||||
await expect(homePage.mobileMenuButton).toBeVisible();
|
||||
});
|
||||
}
|
||||
|
||||
for (const device of getTabletDevices()) {
|
||||
test(`平板端 ${device.name} - 首页应该正常显示`, async ({ homePage, page }) => {
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.header).toBeVisible();
|
||||
await expect(homePage.heroSection).toBeVisible();
|
||||
await expect(homePage.footer).toBeVisible();
|
||||
await expect(homePage.mobileMenuButton).toBeVisible();
|
||||
});
|
||||
}
|
||||
|
||||
test('桌面端 - 导航应该水平显示', async ({ homePage, page }) => {
|
||||
const device = devices['desktop-1280x720'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.navigation).toBeVisible();
|
||||
await expect(homePage.mobileMenuButton).not.toBeVisible();
|
||||
|
||||
const navItems = await homePage.getNavigationItemCount();
|
||||
expect(navItems).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('移动端 - 导航应该隐藏在汉堡菜单中', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.mobileMenuButton).toBeVisible();
|
||||
await expect(homePage.navigation).not.toBeVisible();
|
||||
|
||||
await homePage.openMobileMenu();
|
||||
await expect(homePage.mobileMenu).toBeVisible();
|
||||
});
|
||||
|
||||
test('平板端 - 导航应该隐藏在汉堡菜单中', async ({ homePage, page }) => {
|
||||
const device = devices['tablet-768x1024'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.mobileMenuButton).toBeVisible();
|
||||
await expect(homePage.navigation).not.toBeVisible();
|
||||
|
||||
await homePage.openMobileMenu();
|
||||
await expect(homePage.mobileMenu).toBeVisible();
|
||||
});
|
||||
|
||||
test('所有设备 - 页面应该能够滚动', async ({ homePage, page }) => {
|
||||
const testDevices = [
|
||||
devices['desktop-1280x720'],
|
||||
devices['mobile-375x667'],
|
||||
devices['tablet-768x1024'],
|
||||
];
|
||||
|
||||
for (const device of testDevices) {
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.scrollToSection('services');
|
||||
const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
|
||||
expect(scrollPosition).toBeGreaterThan(0);
|
||||
|
||||
await homePage.scrollToTop();
|
||||
const topPosition = await homePage.page.evaluate(() => window.scrollY);
|
||||
expect(topPosition).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('所有设备 - 所有区块应该可见', async ({ homePage, page }) => {
|
||||
const testDevices = [
|
||||
devices['desktop-1280x720'],
|
||||
devices['mobile-375x667'],
|
||||
devices['tablet-768x1024'],
|
||||
];
|
||||
|
||||
const sections = ['services', 'products', 'cases', 'about', 'news', 'contact'];
|
||||
|
||||
for (const device of testDevices) {
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
for (const sectionId of sections) {
|
||||
await homePage.scrollToSection(sectionId);
|
||||
const isVisible = await homePage.isSectionVisible(sectionId);
|
||||
expect(isVisible).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('移动端 - 移动菜单应该能够打开和关闭', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.openMobileMenu();
|
||||
await expect(homePage.mobileMenu).toBeVisible();
|
||||
|
||||
await homePage.closeMobileMenu();
|
||||
await expect(homePage.mobileMenu).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('桌面端 - Logo应该可见', async ({ homePage, page }) => {
|
||||
const device = devices['desktop-1280x720'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.logo).toBeVisible();
|
||||
const altText = await homePage.getLogoAltText();
|
||||
expect(altText).toBeTruthy();
|
||||
});
|
||||
|
||||
test('移动端 - Logo应该可见', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.logo).toBeVisible();
|
||||
const altText = await homePage.getLogoAltText();
|
||||
expect(altText).toBeTruthy();
|
||||
});
|
||||
|
||||
test('所有设备 - 页脚应该可见', async ({ homePage, page }) => {
|
||||
const testDevices = [
|
||||
devices['desktop-1280x720'],
|
||||
devices['mobile-375x667'],
|
||||
devices['tablet-768x1024'],
|
||||
];
|
||||
|
||||
for (const device of testDevices) {
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.scrollToBottom();
|
||||
await expect(homePage.footer).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('桌面端 - 立即咨询按钮应该可见', async ({ homePage, page }) => {
|
||||
const device = devices['desktop-1280x720'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const contactButton = homePage.page.locator('a:has-text("立即咨询")').first();
|
||||
await expect(contactButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('移动端 - 立即咨询按钮应该可见', async ({ homePage, page }) => {
|
||||
const device = devices['mobile-375x667'];
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.openMobileMenu();
|
||||
const contactButton = homePage.mobileMenu.locator('a:has-text("联系我们")').first();
|
||||
await expect(contactButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('所有设备 - 应该能够点击导航项', async ({ homePage, page }) => {
|
||||
const testDevices = [
|
||||
devices['desktop-1280x720'],
|
||||
devices['mobile-375x667'],
|
||||
devices['tablet-768x1024'],
|
||||
];
|
||||
|
||||
for (const device of testDevices) {
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
if (device.isMobile) {
|
||||
await homePage.openMobileMenu();
|
||||
}
|
||||
|
||||
const labels = await homePage.getAllNavigationLabels();
|
||||
if (labels.length > 0) {
|
||||
await homePage.clickNavigationItem(labels[0]);
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('所有设备 - 应该能够点击Logo返回首页', async ({ homePage, page }) => {
|
||||
const testDevices = [
|
||||
devices['desktop-1280x720'],
|
||||
devices['mobile-375x667'],
|
||||
devices['tablet-768x1024'],
|
||||
];
|
||||
|
||||
for (const device of testDevices) {
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.logo.click();
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
const url = homePage.page.url();
|
||||
expect(url).toMatch(/\/$/);
|
||||
}
|
||||
});
|
||||
|
||||
test('所有设备 - 应该没有水平滚动条', async ({ homePage, page }) => {
|
||||
const testDevices = [
|
||||
devices['desktop-1280x720'],
|
||||
devices['mobile-375x667'],
|
||||
devices['tablet-768x1024'],
|
||||
];
|
||||
|
||||
for (const device of testDevices) {
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const hasHorizontalScroll = await homePage.page.evaluate(() => {
|
||||
return document.body.scrollWidth > document.body.clientWidth;
|
||||
});
|
||||
|
||||
expect(hasHorizontalScroll).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test('所有设备 - 文字应该可读', async ({ homePage, page }) => {
|
||||
const testDevices = [
|
||||
devices['desktop-1280x720'],
|
||||
devices['mobile-375x667'],
|
||||
devices['tablet-768x1024'],
|
||||
];
|
||||
|
||||
for (const device of testDevices) {
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const heroTitle = await homePage.getHeroSectionTitle();
|
||||
expect(heroTitle).toBeTruthy();
|
||||
expect(heroTitle.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('所有设备 - 应该能够滚动到页面底部', async ({ homePage, page }) => {
|
||||
const testDevices = [
|
||||
devices['desktop-1280x720'],
|
||||
devices['mobile-375x667'],
|
||||
devices['tablet-768x1024'],
|
||||
];
|
||||
|
||||
for (const device of testDevices) {
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.scrollToBottom();
|
||||
const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
|
||||
expect(scrollPosition).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('所有设备 - 应该能够滚动到页面顶部', async ({ homePage, page }) => {
|
||||
const testDevices = [
|
||||
devices['desktop-1280x720'],
|
||||
devices['mobile-375x667'],
|
||||
devices['tablet-768x1024'],
|
||||
];
|
||||
|
||||
for (const device of testDevices) {
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await homePage.scrollToBottom();
|
||||
await homePage.scrollToTop();
|
||||
const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
|
||||
expect(scrollPosition).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('所有设备 - 页面应该没有JavaScript错误', async ({ homePage, page }) => {
|
||||
const testDevices = [
|
||||
devices['desktop-1280x720'],
|
||||
devices['mobile-375x667'],
|
||||
devices['tablet-768x1024'],
|
||||
];
|
||||
|
||||
for (const device of testDevices) {
|
||||
const errors: string[] = [];
|
||||
page.on('pageerror', error => {
|
||||
errors.push(error.toString());
|
||||
});
|
||||
|
||||
await page.setViewportSize(device.viewport);
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
expect(errors.length).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
import { test, expect } from '../../fixtures/base.fixture';
|
||||
|
||||
test.describe('联系页面冒烟测试 @smoke', () => {
|
||||
test.beforeEach(async ({ contactPage }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
});
|
||||
|
||||
test('应该成功加载联系页面', async ({ contactPage }) => {
|
||||
await expect(contactPage.page).toHaveURL(/\/contact/);
|
||||
await expect(contactPage.pageHeader).toBeVisible();
|
||||
await expect(contactPage.contactForm).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该显示正确的页面标题', async ({ contactPage }) => {
|
||||
const title = await contactPage.getPageTitle();
|
||||
expect(title).toBeTruthy();
|
||||
expect(title.length).toBeGreaterThan(0);
|
||||
expect(title).toContain('联系');
|
||||
});
|
||||
|
||||
test('应该显示页面描述', async ({ contactPage }) => {
|
||||
const description = await contactPage.getPageDescription();
|
||||
expect(description).toBeTruthy();
|
||||
expect(description.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该显示页面徽章', async ({ contactPage }) => {
|
||||
const badge = await contactPage.getBadgeText();
|
||||
expect(badge).toBeTruthy();
|
||||
expect(badge).toBe('联系我们');
|
||||
});
|
||||
|
||||
test('应该显示联系表单', async ({ contactPage }) => {
|
||||
await expect(contactPage.contactForm).toBeVisible();
|
||||
await expect(contactPage.nameInput).toBeVisible();
|
||||
await expect(contactPage.emailInput).toBeVisible();
|
||||
await expect(contactPage.subjectInput).toBeVisible();
|
||||
await expect(contactPage.messageInput).toBeVisible();
|
||||
await expect(contactPage.submitButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该显示所有表单字段', async ({ contactPage }) => {
|
||||
await expect(contactPage.nameInput).toBeVisible();
|
||||
await expect(contactPage.phoneInput).toBeVisible();
|
||||
await expect(contactPage.emailInput).toBeVisible();
|
||||
await expect(contactPage.subjectInput).toBeVisible();
|
||||
await expect(contactPage.messageInput).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该显示联系信息卡片', async ({ contactPage }) => {
|
||||
await expect(contactPage.contactInfoCard).toBeVisible();
|
||||
await expect(contactPage.addressInfo).toBeVisible();
|
||||
await expect(contactPage.phoneInfo).toBeVisible();
|
||||
await expect(contactPage.emailInfo).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该显示工作时间卡片', async ({ contactPage }) => {
|
||||
await expect(contactPage.workHoursCard).toBeVisible();
|
||||
const workHours = await contactPage.getWorkHours();
|
||||
expect(workHours.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该显示公司地址', async ({ contactPage }) => {
|
||||
const address = await contactPage.getAddress();
|
||||
expect(address).toBeTruthy();
|
||||
expect(address.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该显示联系电话', async ({ contactPage }) => {
|
||||
const phone = await contactPage.getPhone();
|
||||
expect(phone).toBeTruthy();
|
||||
expect(phone.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该显示电子邮箱', async ({ contactPage }) => {
|
||||
const email = await contactPage.getEmail();
|
||||
expect(email).toBeTruthy();
|
||||
expect(email.length).toBeGreaterThan(0);
|
||||
expect(email).toContain('@');
|
||||
});
|
||||
|
||||
test('应该显示提交按钮', async ({ contactPage }) => {
|
||||
await expect(contactPage.submitButton).toBeVisible();
|
||||
const buttonText = await contactPage.getSubmitButtonText();
|
||||
expect(buttonText).toContain('发送');
|
||||
});
|
||||
|
||||
test('应该能够输入姓名', async ({ contactPage }) => {
|
||||
const testName = '测试用户';
|
||||
await contactPage.nameInput.fill(testName);
|
||||
const value = await contactPage.getNameInputValue();
|
||||
expect(value).toBe(testName);
|
||||
});
|
||||
|
||||
test('应该能够输入电话', async ({ contactPage }) => {
|
||||
const testPhone = '13800138000';
|
||||
await contactPage.phoneInput.fill(testPhone);
|
||||
const value = await contactPage.getPhoneInputValue();
|
||||
expect(value).toBe(testPhone);
|
||||
});
|
||||
|
||||
test('应该能够输入邮箱', async ({ contactPage }) => {
|
||||
const testEmail = 'test@example.com';
|
||||
await contactPage.emailInput.fill(testEmail);
|
||||
const value = await contactPage.getEmailInputValue();
|
||||
expect(value).toBe(testEmail);
|
||||
});
|
||||
|
||||
test('应该能够输入主题', async ({ contactPage }) => {
|
||||
const testSubject = '测试主题';
|
||||
await contactPage.subjectInput.fill(testSubject);
|
||||
const value = await contactPage.getSubjectInputValue();
|
||||
expect(value).toBe(testSubject);
|
||||
});
|
||||
|
||||
test('应该能够输入消息内容', async ({ contactPage }) => {
|
||||
const testMessage = '这是一条测试消息';
|
||||
await contactPage.messageInput.fill(testMessage);
|
||||
const value = await contactPage.getMessageInputValue();
|
||||
expect(value).toBe(testMessage);
|
||||
});
|
||||
|
||||
test('应该显示必填字段标记', async ({ contactPage }) => {
|
||||
const isNameRequired = await contactPage.isFieldRequired('name');
|
||||
const isEmailRequired = await contactPage.isFieldRequired('email');
|
||||
const isSubjectRequired = await contactPage.isFieldRequired('subject');
|
||||
const isMessageRequired = await contactPage.isFieldRequired('message');
|
||||
|
||||
expect(isNameRequired).toBe(true);
|
||||
expect(isEmailRequired).toBe(true);
|
||||
expect(isSubjectRequired).toBe(true);
|
||||
expect(isMessageRequired).toBe(true);
|
||||
});
|
||||
|
||||
test('应该显示字段占位符', async ({ contactPage }) => {
|
||||
const namePlaceholder = await contactPage.getFieldPlaceholder('name');
|
||||
const emailPlaceholder = await contactPage.getFieldPlaceholder('email');
|
||||
const subjectPlaceholder = await contactPage.getFieldPlaceholder('subject');
|
||||
const messagePlaceholder = await contactPage.getFieldPlaceholder('message');
|
||||
|
||||
expect(namePlaceholder).toBeTruthy();
|
||||
expect(emailPlaceholder).toBeTruthy();
|
||||
expect(subjectPlaceholder).toBeTruthy();
|
||||
expect(messagePlaceholder).toBeTruthy();
|
||||
});
|
||||
|
||||
test('应该有正确的工作时间信息', async ({ contactPage }) => {
|
||||
const workHours = await contactPage.getWorkHours();
|
||||
expect(workHours.length).toBeGreaterThan(0);
|
||||
workHours.forEach(item => {
|
||||
expect(item.day).toBeTruthy();
|
||||
expect(item.hours).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('应该没有控制台错误', async ({ contactPage, page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
errors.push(msg.text());
|
||||
}
|
||||
});
|
||||
await contactPage.waitForPageLoad();
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
|
||||
test('应该能够滚动到表单', async ({ contactPage }) => {
|
||||
await contactPage.scrollToForm();
|
||||
const isVisible = await contactPage.contactForm.isVisible();
|
||||
expect(isVisible).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import { test, expect } from '../../fixtures/base.fixture';
|
||||
|
||||
test.describe('首页冒烟测试 @smoke', () => {
|
||||
test.beforeEach(async ({ homePage }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
});
|
||||
|
||||
test('应该成功加载首页', async ({ homePage }) => {
|
||||
await expect(homePage.page).toHaveURL(/\/$/);
|
||||
await expect(homePage.header).toBeVisible();
|
||||
await expect(homePage.heroSection).toBeVisible();
|
||||
await expect(homePage.footer).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该显示正确的页面标题', async ({ homePage }) => {
|
||||
const title = await homePage.getTitle();
|
||||
expect(title).toBeTruthy();
|
||||
expect(title.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该显示Logo', async ({ homePage }) => {
|
||||
await expect(homePage.logo).toBeVisible();
|
||||
const altText = await homePage.getLogoAltText();
|
||||
expect(altText).toBeTruthy();
|
||||
});
|
||||
|
||||
test('应该显示主导航菜单', async ({ homePage }) => {
|
||||
await expect(homePage.navigation).toBeVisible();
|
||||
const navItems = await homePage.getNavigationItemCount();
|
||||
expect(navItems).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该显示所有主要区块', async ({ homePage }) => {
|
||||
await expect(homePage.heroSection).toBeVisible();
|
||||
await homePage.scrollToSection('services');
|
||||
await expect(homePage.servicesSection).toBeVisible();
|
||||
await homePage.scrollToSection('products');
|
||||
await expect(homePage.productsSection).toBeVisible();
|
||||
await homePage.scrollToSection('cases');
|
||||
await expect(homePage.casesSection).toBeVisible();
|
||||
await homePage.scrollToSection('about');
|
||||
await expect(homePage.aboutSection).toBeVisible();
|
||||
await homePage.scrollToSection('news');
|
||||
await expect(homePage.newsSection).toBeVisible();
|
||||
await homePage.scrollToSection('contact');
|
||||
await expect(homePage.contactSection).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该显示页脚', async ({ homePage }) => {
|
||||
await homePage.scrollToBottom();
|
||||
await expect(homePage.footer).toBeVisible();
|
||||
const footerText = await homePage.getFooterText();
|
||||
expect(footerText.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该能够滚动到页面底部', async ({ homePage }) => {
|
||||
await homePage.scrollToBottom();
|
||||
const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
|
||||
expect(scrollPosition).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该能够滚动到页面顶部', async ({ homePage }) => {
|
||||
await homePage.scrollToBottom();
|
||||
await homePage.scrollToTop();
|
||||
const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
|
||||
expect(scrollPosition).toBe(0);
|
||||
});
|
||||
|
||||
test('应该显示Hero区块标题', async ({ homePage }) => {
|
||||
const title = await homePage.getHeroSectionTitle();
|
||||
expect(title).toBeTruthy();
|
||||
expect(title.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该显示Services区块标题', async ({ homePage }) => {
|
||||
await homePage.waitForServicesSection();
|
||||
const title = await homePage.getServicesSectionTitle();
|
||||
expect(title).toBeTruthy();
|
||||
expect(title.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该显示Products区块标题', async ({ homePage }) => {
|
||||
await homePage.waitForProductsSection();
|
||||
const title = await homePage.getProductsSectionTitle();
|
||||
expect(title).toBeTruthy();
|
||||
expect(title.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该显示Cases区块标题', async ({ homePage }) => {
|
||||
await homePage.waitForCasesSection();
|
||||
const title = await homePage.getCasesSectionTitle();
|
||||
expect(title).toBeTruthy();
|
||||
expect(title.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该显示About区块标题', async ({ homePage }) => {
|
||||
await homePage.waitForAboutSection();
|
||||
const title = await homePage.getAboutSectionTitle();
|
||||
expect(title).toBeTruthy();
|
||||
expect(title.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该显示News区块标题', async ({ homePage }) => {
|
||||
await homePage.waitForNewsSection();
|
||||
const title = await homePage.getNewsSectionTitle();
|
||||
expect(title).toBeTruthy();
|
||||
expect(title.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该显示Contact区块标题', async ({ homePage }) => {
|
||||
await homePage.waitForContactSection();
|
||||
const title = await homePage.getContactSectionTitle();
|
||||
expect(title).toBeTruthy();
|
||||
expect(title.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该有正确的导航标签', async ({ homePage }) => {
|
||||
const labels = await homePage.getAllNavigationLabels();
|
||||
expect(labels.length).toBeGreaterThan(0);
|
||||
labels.forEach(label => {
|
||||
expect(label).toBeTruthy();
|
||||
expect(label.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('应该没有控制台错误', async ({ homePage, page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
errors.push(msg.text());
|
||||
}
|
||||
});
|
||||
await homePage.waitForPageLoad();
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import { test, expect } from '../../fixtures/base.fixture';
|
||||
|
||||
test.describe('导航冒烟测试 @smoke', () => {
|
||||
test.beforeEach(async ({ homePage }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
});
|
||||
|
||||
test('应该显示主导航菜单', async ({ homePage }) => {
|
||||
await expect(homePage.navigation).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该显示Logo链接', async ({ homePage }) => {
|
||||
await expect(homePage.logo).toBeVisible();
|
||||
const altText = await homePage.getLogoAltText();
|
||||
expect(altText).toBeTruthy();
|
||||
});
|
||||
|
||||
test('应该有导航项', async ({ homePage }) => {
|
||||
const navItems = await homePage.getNavigationItemCount();
|
||||
expect(navItems).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该能够点击导航项', async ({ homePage }) => {
|
||||
const labels = await homePage.getAllNavigationLabels();
|
||||
if (labels.length > 0) {
|
||||
await homePage.clickNavigationItem(labels[0]);
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
}
|
||||
});
|
||||
|
||||
test('应该显示立即咨询按钮', async ({ homePage }) => {
|
||||
const contactButton = homePage.page.locator('a:has-text("立即咨询")').first();
|
||||
await expect(contactButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能够点击立即咨询按钮', async ({ homePage }) => {
|
||||
const contactButton = homePage.page.locator('a:has-text("立即咨询")').first();
|
||||
await contactButton.click();
|
||||
await homePage.page.waitForTimeout(2000);
|
||||
const url = homePage.page.url();
|
||||
console.log('点击立即咨询后的URL:', url);
|
||||
expect(url).toContain('/contact');
|
||||
});
|
||||
|
||||
test('应该显示移动端菜单按钮', async ({ homePage }) => {
|
||||
await expect(homePage.mobileMenuButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能够打开移动端菜单', async ({ homePage }) => {
|
||||
await homePage.openMobileMenu();
|
||||
await expect(homePage.mobileMenu).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能够关闭移动端菜单', async ({ homePage }) => {
|
||||
await homePage.openMobileMenu();
|
||||
await homePage.closeMobileMenu();
|
||||
await expect(homePage.mobileMenu).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('应该有正确的导航标签', async ({ homePage }) => {
|
||||
const labels = await homePage.getAllNavigationLabels();
|
||||
expect(labels.length).toBeGreaterThan(0);
|
||||
labels.forEach(label => {
|
||||
expect(label).toBeTruthy();
|
||||
expect(label.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('应该能够滚动到各个区块', async ({ homePage }) => {
|
||||
const sections = ['services', 'products', 'cases', 'about', 'news', 'contact'];
|
||||
for (const sectionId of sections) {
|
||||
await homePage.scrollToSection(sectionId);
|
||||
const isVisible = await homePage.isSectionVisible(sectionId);
|
||||
expect(isVisible).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('应该能够滚动到页面顶部', async ({ homePage }) => {
|
||||
await homePage.scrollToBottom();
|
||||
await homePage.scrollToTop();
|
||||
const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
|
||||
expect(scrollPosition).toBe(0);
|
||||
});
|
||||
|
||||
test('应该能够滚动到页面底部', async ({ homePage }) => {
|
||||
await homePage.scrollToBottom();
|
||||
const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
|
||||
expect(scrollPosition).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该显示所有区块', async ({ homePage }) => {
|
||||
const sectionIds = await homePage.getAllSectionIds();
|
||||
expect(sectionIds.length).toBeGreaterThan(0);
|
||||
sectionIds.forEach(sectionId => {
|
||||
expect(sectionId).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('应该能够通过导航跳转到区块', async ({ homePage }) => {
|
||||
const labels = await homePage.getAllNavigationLabels();
|
||||
if (labels.length > 1) {
|
||||
await homePage.clickNavigationItem(labels[1]);
|
||||
await homePage.page.waitForTimeout(1000);
|
||||
const url = homePage.page.url();
|
||||
expect(url).toContain('#');
|
||||
}
|
||||
});
|
||||
|
||||
test('应该显示页脚', async ({ homePage }) => {
|
||||
await homePage.scrollToBottom();
|
||||
await expect(homePage.footer).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该有正确的页面标题', async ({ homePage }) => {
|
||||
const title = await homePage.getTitle();
|
||||
expect(title).toBeTruthy();
|
||||
expect(title.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('应该没有控制台错误', async ({ homePage, page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
errors.push(msg.text());
|
||||
}
|
||||
});
|
||||
await homePage.waitForPageLoad();
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,298 @@
|
||||
import { test, expect } from '../../fixtures/base.fixture';
|
||||
|
||||
test.describe('联系页面视觉回归测试 @visual', () => {
|
||||
test('联系页面 - 页面头部应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(contactPage.pageHeader).toHaveScreenshot('contact-page-header.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 联系表单应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(contactPage.contactForm).toHaveScreenshot('contact-form.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 联系信息卡片应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(contactPage.contactInfoCard).toHaveScreenshot('contact-info-card.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 工作时间卡片应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(contactPage.workHoursCard).toHaveScreenshot('work-hours-card.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 表单字段应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(contactPage.nameInput).toHaveScreenshot('name-input.png', {
|
||||
maxDiffPixels: 20,
|
||||
threshold: 0.1,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 邮箱字段应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(contactPage.emailInput).toHaveScreenshot('email-input.png', {
|
||||
maxDiffPixels: 20,
|
||||
threshold: 0.1,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 主题字段应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(contactPage.subjectInput).toHaveScreenshot('subject-input.png', {
|
||||
maxDiffPixels: 20,
|
||||
threshold: 0.1,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 消息字段应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(contactPage.messageInput).toHaveScreenshot('message-input.png', {
|
||||
maxDiffPixels: 50,
|
||||
threshold: 0.15,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 提交按钮应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(contactPage.submitButton).toHaveScreenshot('submit-button.png', {
|
||||
maxDiffPixels: 30,
|
||||
threshold: 0.15,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 完整页面应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(page).toHaveScreenshot('contact-full-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
threshold: 0.3,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 滚动后页面应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
await contactPage.scrollToForm();
|
||||
await contactPage.page.waitForTimeout(500);
|
||||
|
||||
await expect(page).toHaveScreenshot('contact-scrolled-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
threshold: 0.3,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 移动端视图应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(page).toHaveScreenshot('contact-mobile-view.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
threshold: 0.3,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 平板端视图应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(page).toHaveScreenshot('contact-tablet-view.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
threshold: 0.3,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 桌面端视图应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(page).toHaveScreenshot('contact-desktop-view.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
threshold: 0.3,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 填写表单后应该与基线匹配', async ({ contactPage, page, testDataGenerator }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillContactForm(formData);
|
||||
await contactPage.page.waitForTimeout(500);
|
||||
|
||||
await expect(contactPage.contactForm).toHaveScreenshot('contact-form-filled.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 提交成功后应该与基线匹配', async ({ contactPage, page, testDataGenerator }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
const formData = testDataGenerator.generateContactFormData();
|
||||
await contactPage.fillAndSubmitForm(formData);
|
||||
await contactPage.waitForFormSubmission();
|
||||
|
||||
await expect(contactPage.successMessage).toHaveScreenshot('contact-success.png', {
|
||||
maxDiffPixels: 50,
|
||||
threshold: 0.15,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 应该能够捕获视觉差异', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
const screenshot = await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/screenshots/contact-page-visual.png',
|
||||
});
|
||||
|
||||
expect(screenshot).toBeTruthy();
|
||||
});
|
||||
|
||||
test('联系页面 - 应该能够比较视觉差异', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(contactPage.contactForm).toHaveScreenshot('contact-comparison.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
animations: 'disabled',
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 应该能够禁用动画进行截图', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(page).toHaveScreenshot('contact-no-animations.png', {
|
||||
fullPage: true,
|
||||
animations: 'disabled',
|
||||
maxDiffPixels: 200,
|
||||
threshold: 0.3,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 应该能够捕获高DPI截图', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
const screenshot = await page.screenshot({
|
||||
fullPage: true,
|
||||
scale: 'device',
|
||||
path: 'test-results/screenshots/contact-high-dpi.png',
|
||||
});
|
||||
|
||||
expect(screenshot).toBeTruthy();
|
||||
});
|
||||
|
||||
test('联系页面 - 表单字段焦点状态应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await contactPage.nameInput.focus();
|
||||
await contactPage.page.waitForTimeout(300);
|
||||
|
||||
await expect(contactPage.nameInput).toHaveScreenshot('name-input-focused.png', {
|
||||
maxDiffPixels: 30,
|
||||
threshold: 0.15,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 表单字段悬停状态应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await contactPage.nameInput.hover();
|
||||
await contactPage.page.waitForTimeout(300);
|
||||
|
||||
await expect(contactPage.nameInput).toHaveScreenshot('name-input-hover.png', {
|
||||
maxDiffPixels: 30,
|
||||
threshold: 0.15,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 提交按钮悬停状态应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await contactPage.submitButton.hover();
|
||||
await contactPage.page.waitForTimeout(300);
|
||||
|
||||
await expect(contactPage.submitButton).toHaveScreenshot('submit-button-hover.png', {
|
||||
maxDiffPixels: 30,
|
||||
threshold: 0.15,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 所有表单字段应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(contactPage.contactForm).toHaveScreenshot('all-form-fields.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 联系信息应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(contactPage.contactInfoCard).toHaveScreenshot('contact-info.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('联系页面 - 工作时间应该与基线匹配', async ({ contactPage, page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.waitForPageLoad();
|
||||
|
||||
await expect(contactPage.workHoursCard).toHaveScreenshot('work-hours.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,298 @@
|
||||
import { test, expect } from '../../fixtures/base.fixture';
|
||||
|
||||
test.describe('首页视觉回归测试 @visual', () => {
|
||||
test('首页 - Hero区块应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.heroSection).toHaveScreenshot('hero-section.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - Services区块应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.scrollToSection('services');
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
await expect(homePage.servicesSection).toHaveScreenshot('services-section.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - Products区块应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.scrollToSection('products');
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
await expect(homePage.productsSection).toHaveScreenshot('products-section.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - Cases区块应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.scrollToSection('cases');
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
await expect(homePage.casesSection).toHaveScreenshot('cases-section.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - About区块应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.scrollToSection('about');
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
await expect(homePage.aboutSection).toHaveScreenshot('about-section.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - News区块应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.scrollToSection('news');
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
await expect(homePage.newsSection).toHaveScreenshot('news-section.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - Contact区块应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.scrollToSection('contact');
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
await expect(homePage.contactSection).toHaveScreenshot('contact-section.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - Header应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.header).toHaveScreenshot('header.png', {
|
||||
maxDiffPixels: 50,
|
||||
threshold: 0.1,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - Footer应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.scrollToBottom();
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
await expect(homePage.footer).toHaveScreenshot('footer.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - Logo应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.logo).toHaveScreenshot('logo.png', {
|
||||
maxDiffPixels: 10,
|
||||
threshold: 0.05,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - 导航应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.navigation).toHaveScreenshot('navigation.png', {
|
||||
maxDiffPixels: 50,
|
||||
threshold: 0.1,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - 完整页面应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(page).toHaveScreenshot('full-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
threshold: 0.3,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - 滚动后页面应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.scrollToSection('services');
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
await expect(page).toHaveScreenshot('scrolled-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
threshold: 0.3,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - Hero区块标题应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const title = homePage.heroSection.locator('h1, h2').first();
|
||||
await expect(title).toHaveScreenshot('hero-title.png', {
|
||||
maxDiffPixels: 20,
|
||||
threshold: 0.1,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - 悬停状态应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const firstNavItem = homePage.navigation.locator('a').first();
|
||||
await firstNavItem.hover();
|
||||
await homePage.page.waitForTimeout(300);
|
||||
|
||||
await expect(homePage.navigation).toHaveScreenshot('nav-hover.png', {
|
||||
maxDiffPixels: 50,
|
||||
threshold: 0.1,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - 移动端视图应该与基线匹配', async ({ homePage, page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(page).toHaveScreenshot('mobile-view.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
threshold: 0.3,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - 平板端视图应该与基线匹配', async ({ homePage, page }) => {
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(page).toHaveScreenshot('tablet-view.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
threshold: 0.3,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - 桌面端视图应该与基线匹配', async ({ homePage, page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(page).toHaveScreenshot('desktop-view.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
threshold: 0.3,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - 移动端菜单应该与基线匹配', async ({ homePage, page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.openMobileMenu();
|
||||
await homePage.page.waitForTimeout(300);
|
||||
|
||||
await expect(homePage.mobileMenu).toHaveScreenshot('mobile-menu.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - 滚动后Header应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.scrollToSection('services');
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
await expect(homePage.header).toHaveScreenshot('header-scrolled.png', {
|
||||
maxDiffPixels: 50,
|
||||
threshold: 0.1,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - 所有区块组合应该与基线匹配', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
await homePage.scrollToBottom();
|
||||
await homePage.page.waitForTimeout(500);
|
||||
|
||||
await expect(page).toHaveScreenshot('all-sections.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 300,
|
||||
threshold: 0.4,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - 应该能够捕获视觉差异', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const screenshot = await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/screenshots/homepage-visual.png',
|
||||
});
|
||||
|
||||
expect(screenshot).toBeTruthy();
|
||||
});
|
||||
|
||||
test('首页 - 应该能够比较视觉差异', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(homePage.heroSection).toHaveScreenshot('hero-comparison.png', {
|
||||
maxDiffPixels: 100,
|
||||
threshold: 0.2,
|
||||
animations: 'disabled',
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - 应该能够禁用动画进行截图', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
await expect(page).toHaveScreenshot('no-animations.png', {
|
||||
fullPage: true,
|
||||
animations: 'disabled',
|
||||
maxDiffPixels: 200,
|
||||
threshold: 0.3,
|
||||
});
|
||||
});
|
||||
|
||||
test('首页 - 应该能够捕获高DPI截图', async ({ homePage, page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.waitForPageLoad();
|
||||
|
||||
const screenshot = await page.screenshot({
|
||||
fullPage: true,
|
||||
scale: 'device',
|
||||
path: 'test-results/screenshots/homepage-high-dpi.png',
|
||||
});
|
||||
|
||||
expect(screenshot).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
export interface TestData {
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
company?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ContactFormData extends TestData {
|
||||
subject?: string;
|
||||
}
|
||||
|
||||
export interface NavigationItem {
|
||||
label: string;
|
||||
href: string;
|
||||
expectedUrl?: string;
|
||||
}
|
||||
|
||||
export interface PerformanceMetrics {
|
||||
loadTime: number;
|
||||
firstContentfulPaint: number;
|
||||
largestContentfulPaint: number;
|
||||
timeToInteractive: number;
|
||||
cumulativeLayoutShift: number;
|
||||
firstInputDelay: number;
|
||||
}
|
||||
|
||||
export interface PerformanceThresholds {
|
||||
loadTime: number;
|
||||
firstContentfulPaint: number;
|
||||
largestContentfulPaint: number;
|
||||
timeToInteractive: number;
|
||||
cumulativeLayoutShift: number;
|
||||
firstInputDelay: number;
|
||||
}
|
||||
|
||||
export interface DeviceConfig {
|
||||
name: string;
|
||||
viewport: { width: number; height: number };
|
||||
userAgent?: string;
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
export interface TestResult {
|
||||
test: string;
|
||||
status: 'passed' | 'failed' | 'skipped';
|
||||
duration: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface AccessibilityViolation {
|
||||
id: string;
|
||||
impact: 'critical' | 'serious' | 'moderate' | 'minor';
|
||||
description: string;
|
||||
help: string;
|
||||
helpUrl: string;
|
||||
nodes: any[];
|
||||
}
|
||||
|
||||
export interface VisualRegressionResult {
|
||||
baseline: string;
|
||||
current: string;
|
||||
diff?: string;
|
||||
mismatchedPixels: number;
|
||||
passed: boolean;
|
||||
}
|
||||
|
||||
export interface PageElement {
|
||||
selector: string;
|
||||
text?: string;
|
||||
visible?: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface TestConfig {
|
||||
baseURL: string;
|
||||
timeout: number;
|
||||
retries: number;
|
||||
headless: boolean;
|
||||
screenshotOnFailure: boolean;
|
||||
traceOnFailure: boolean;
|
||||
videoOnFailure: boolean;
|
||||
}
|
||||
|
||||
export interface TestCase {
|
||||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
priority: 'critical' | 'high' | 'medium' | 'low';
|
||||
dependencies?: string[];
|
||||
}
|
||||
|
||||
export interface TestSuite {
|
||||
name: string;
|
||||
description: string;
|
||||
tests: TestCase[];
|
||||
}
|
||||
|
||||
export interface ApiResponse {
|
||||
status: number;
|
||||
data: any;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ErrorDetails {
|
||||
message: string;
|
||||
stack?: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export interface TestEnvironment {
|
||||
name: string;
|
||||
url: string;
|
||||
isProduction: boolean;
|
||||
browser: string;
|
||||
version: string;
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { PerformanceMetrics, PerformanceThresholds } from '../types';
|
||||
|
||||
export class PerformanceMonitor {
|
||||
private page: Page;
|
||||
private metrics: PerformanceMetrics;
|
||||
private startTime: number;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.metrics = {
|
||||
loadTime: 0,
|
||||
firstContentfulPaint: 0,
|
||||
largestContentfulPaint: 0,
|
||||
timeToInteractive: 0,
|
||||
cumulativeLayoutShift: 0,
|
||||
firstInputDelay: 0,
|
||||
};
|
||||
this.startTime = 0;
|
||||
}
|
||||
|
||||
async startMonitoring(): Promise<void> {
|
||||
this.startTime = Date.now();
|
||||
|
||||
await this.page.evaluate(() => {
|
||||
window.performance.clearResourceTimings();
|
||||
});
|
||||
|
||||
await this.page.evaluate(() => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
entries.forEach((entry) => {
|
||||
if (entry.entryType === 'layout-shift' && !(entry as any).hadRecentInput) {
|
||||
(window as any).cumulativeLayoutShift = ((window as any).cumulativeLayoutShift || 0) + (entry as any).value;
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe({ entryTypes: ['layout-shift'] });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async collectMetrics(): Promise<PerformanceMetrics> {
|
||||
const navigationTiming = await this.page.evaluate(() => {
|
||||
const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
||||
return {
|
||||
loadTime: timing.loadEventEnd - timing.fetchStart,
|
||||
domContentLoaded: timing.domContentLoadedEventEnd - timing.fetchStart,
|
||||
firstPaint: timing.responseStart - timing.fetchStart,
|
||||
};
|
||||
});
|
||||
|
||||
const paintTiming = await this.page.evaluate(() => {
|
||||
const paints = performance.getEntriesByType('paint');
|
||||
const fcp = paints.find((p) => p.name === 'first-contentful-paint');
|
||||
return {
|
||||
firstContentfulPaint: fcp ? fcp.startTime : 0,
|
||||
};
|
||||
});
|
||||
|
||||
const lcp = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const lastEntry = entries[entries.length - 1];
|
||||
resolve(lastEntry ? lastEntry.startTime : 0);
|
||||
});
|
||||
observer.observe({ entryTypes: ['largest-contentful-paint'] });
|
||||
} else {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const cls = await this.page.evaluate(() => {
|
||||
return (window as any).cumulativeLayoutShift || 0;
|
||||
});
|
||||
|
||||
const tti = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const longTasks = entries.filter((e) => e.duration > 50);
|
||||
if (longTasks.length > 0) {
|
||||
resolve(longTasks[0].startTime);
|
||||
}
|
||||
});
|
||||
observer.observe({ entryTypes: ['longtask'] });
|
||||
} else {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const fid = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
if (entries.length > 0) {
|
||||
resolve(entries[0].processingStart - entries[0].startTime);
|
||||
}
|
||||
});
|
||||
observer.observe({ entryTypes: ['first-input'] });
|
||||
} else {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.metrics = {
|
||||
loadTime: navigationTiming.loadTime,
|
||||
firstContentfulPaint: paintTiming.firstContentfulPaint,
|
||||
largestContentfulPaint: lcp,
|
||||
timeToInteractive: tti,
|
||||
cumulativeLayoutShift: cls,
|
||||
firstInputDelay: fid,
|
||||
};
|
||||
|
||||
return this.metrics;
|
||||
}
|
||||
|
||||
async measurePageLoad(): Promise<number> {
|
||||
const startTime = Date.now();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
const endTime = Date.now();
|
||||
return endTime - startTime;
|
||||
}
|
||||
|
||||
async measureFirstContentfulPaint(): Promise<number> {
|
||||
const fcp = await this.page.evaluate(() => {
|
||||
const paints = performance.getEntriesByType('paint');
|
||||
const fcpEntry = paints.find((p) => p.name === 'first-contentful-paint');
|
||||
return fcpEntry ? fcpEntry.startTime : 0;
|
||||
});
|
||||
return fcp;
|
||||
}
|
||||
|
||||
async measureLargestContentfulPaint(): Promise<number> {
|
||||
const lcp = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const lastEntry = entries[entries.length - 1];
|
||||
resolve(lastEntry ? lastEntry.startTime : 0);
|
||||
});
|
||||
observer.observe({ entryTypes: ['largest-contentful-paint'] });
|
||||
setTimeout(() => resolve(0), 5000);
|
||||
} else {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
return lcp;
|
||||
}
|
||||
|
||||
async measureCumulativeLayoutShift(): Promise<number> {
|
||||
const cls = await this.page.evaluate(() => {
|
||||
return (window as any).cumulativeLayoutShift || 0;
|
||||
});
|
||||
return cls;
|
||||
}
|
||||
|
||||
async measureTimeToInteractive(): Promise<number> {
|
||||
const tti = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const longTasks = entries.filter((e) => e.duration > 50);
|
||||
if (longTasks.length > 0) {
|
||||
resolve(longTasks[0].startTime);
|
||||
}
|
||||
});
|
||||
observer.observe({ entryTypes: ['longtask'] });
|
||||
setTimeout(() => resolve(0), 10000);
|
||||
} else {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
return tti;
|
||||
}
|
||||
|
||||
async measureFirstInputDelay(): Promise<number> {
|
||||
const fid = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
if (entries.length > 0) {
|
||||
resolve(entries[0].processingStart - entries[0].startTime);
|
||||
}
|
||||
});
|
||||
observer.observe({ entryTypes: ['first-input'] });
|
||||
setTimeout(() => resolve(0), 5000);
|
||||
} else {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
return fid;
|
||||
}
|
||||
|
||||
async measureResourceTiming(): Promise<any[]> {
|
||||
const resources = await this.page.evaluate(() => {
|
||||
return performance.getEntriesByType('resource').map((r) => ({
|
||||
name: r.name,
|
||||
duration: r.duration,
|
||||
size: (r as any).transferSize,
|
||||
type: r.initiatorType,
|
||||
}));
|
||||
});
|
||||
return resources;
|
||||
}
|
||||
|
||||
async measureMemoryUsage(): Promise<number> {
|
||||
const memory = await this.page.evaluate(() => {
|
||||
return (performance as any).memory?.usedJSHeapSize || 0;
|
||||
});
|
||||
return memory;
|
||||
}
|
||||
|
||||
async measureFrameRate(): Promise<number> {
|
||||
const frameRate = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
let frames = 0;
|
||||
const startTime = performance.now();
|
||||
|
||||
function countFrames() {
|
||||
frames++;
|
||||
if (performance.now() - startTime >= 1000) {
|
||||
resolve(frames);
|
||||
} else {
|
||||
requestAnimationFrame(countFrames);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(countFrames);
|
||||
});
|
||||
});
|
||||
return frameRate;
|
||||
}
|
||||
|
||||
async measureDomContentLoaded(): Promise<number> {
|
||||
const dcl = await this.page.evaluate(() => {
|
||||
const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
||||
return timing.domContentLoadedEventEnd - timing.fetchStart;
|
||||
});
|
||||
return dcl;
|
||||
}
|
||||
|
||||
validateMetrics(thresholds: PerformanceThresholds): { passed: boolean; violations: string[] } {
|
||||
const violations: string[] = [];
|
||||
|
||||
if (this.metrics.loadTime > thresholds.loadTime) {
|
||||
violations.push(`页面加载时间 ${this.metrics.loadTime}ms 超过阈值 ${thresholds.loadTime}ms`);
|
||||
}
|
||||
if (this.metrics.firstContentfulPaint > thresholds.firstContentfulPaint) {
|
||||
violations.push(`首次内容绘制 ${this.metrics.firstContentfulPaint}ms 超过阈值 ${thresholds.firstContentfulPaint}ms`);
|
||||
}
|
||||
if (this.metrics.largestContentfulPaint > thresholds.largestContentfulPaint) {
|
||||
violations.push(`最大内容绘制 ${this.metrics.largestContentfulPaint}ms 超过阈值 ${thresholds.largestContentfulPaint}ms`);
|
||||
}
|
||||
if (this.metrics.timeToInteractive > thresholds.timeToInteractive) {
|
||||
violations.push(`可交互时间 ${this.metrics.timeToInteractive}ms 超过阈值 ${thresholds.timeToInteractive}ms`);
|
||||
}
|
||||
if (this.metrics.cumulativeLayoutShift > thresholds.cumulativeLayoutShift) {
|
||||
violations.push(`累积布局偏移 ${this.metrics.cumulativeLayoutShift} 超过阈值 ${thresholds.cumulativeLayoutShift}`);
|
||||
}
|
||||
if (this.metrics.firstInputDelay > thresholds.firstInputDelay) {
|
||||
violations.push(`首次输入延迟 ${this.metrics.firstInputDelay}ms 超过阈值 ${thresholds.firstInputDelay}ms`);
|
||||
}
|
||||
|
||||
return {
|
||||
passed: violations.length === 0,
|
||||
violations,
|
||||
};
|
||||
}
|
||||
|
||||
getMetrics(): PerformanceMetrics {
|
||||
return this.metrics;
|
||||
}
|
||||
|
||||
async generateReport(): Promise<string> {
|
||||
const metrics = await this.collectMetrics();
|
||||
const resources = await this.measureResourceTiming();
|
||||
|
||||
let report = '=== 性能测试报告 ===\n\n';
|
||||
report += '核心指标:\n';
|
||||
report += `- 页面加载时间: ${metrics.loadTime.toFixed(2)}ms\n`;
|
||||
report += `- 首次内容绘制: ${metrics.firstContentfulPaint.toFixed(2)}ms\n`;
|
||||
report += `- 最大内容绘制: ${metrics.largestContentfulPaint.toFixed(2)}ms\n`;
|
||||
report += `- 可交互时间: ${metrics.timeToInteractive.toFixed(2)}ms\n`;
|
||||
report += `- 累积布局偏移: ${metrics.cumulativeLayoutShift.toFixed(4)}\n`;
|
||||
report += `- 首次输入延迟: ${metrics.firstInputDelay.toFixed(2)}ms\n\n`;
|
||||
|
||||
report += '资源加载:\n';
|
||||
const totalResources = resources.length;
|
||||
const totalSize = resources.reduce((sum, r) => sum + (r.size || 0), 0);
|
||||
const avgDuration = resources.reduce((sum, r) => sum + r.duration, 0) / totalResources;
|
||||
|
||||
report += `- 总资源数: ${totalResources}\n`;
|
||||
report += `- 总大小: ${(totalSize / 1024).toFixed(2)}KB\n`;
|
||||
report += `- 平均加载时间: ${avgDuration.toFixed(2)}ms\n`;
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import { ContactFormData, TestData } from '../types';
|
||||
|
||||
export class TestDataGenerator {
|
||||
private static readonly FIRST_NAMES = ['张', '李', '王', '刘', '陈', '杨', '赵', '黄', '周', '吴'];
|
||||
private static readonly LAST_NAMES = ['伟', '芳', '娜', '秀英', '敏', '静', '丽', '强', '磊', '军'];
|
||||
private static readonly COMPANIES = ['科技有限公司', '信息技术有限公司', '网络技术有限公司', '数据科技有限公司', '智能科技有限公司'];
|
||||
private static readonly SUBJECTS = ['产品咨询', '技术支持', '商务合作', '其他', '意见反馈'];
|
||||
|
||||
static generateName(): string {
|
||||
const first = this.FIRST_NAMES[Math.floor(Math.random() * this.FIRST_NAMES.length)];
|
||||
const last = this.LAST_NAMES[Math.floor(Math.random() * this.LAST_NAMES.length)];
|
||||
return `${first}${last}`;
|
||||
}
|
||||
|
||||
static generateEmail(name?: string): string {
|
||||
const username = name || this.generateName();
|
||||
const domains = ['example.com', 'test.com', 'demo.com'];
|
||||
const domain = domains[Math.floor(Math.random() * domains.length)];
|
||||
return `${username}@${domain}`;
|
||||
}
|
||||
|
||||
static generatePhone(): string {
|
||||
const prefix = ['138', '139', '136', '137', '158', '159'][Math.floor(Math.random() * 6)];
|
||||
const middle = Math.floor(Math.random() * 9000 + 1000);
|
||||
const suffix = Math.floor(Math.random() * 9000 + 1000);
|
||||
return `${prefix}${middle}${suffix}`;
|
||||
}
|
||||
|
||||
static generateCompany(): string {
|
||||
const prefix = ['创新', '未来', '智慧', '科技', '数字'][Math.floor(Math.random() * 5)];
|
||||
const suffix = this.COMPANIES[Math.floor(Math.random() * this.COMPANIES.length)];
|
||||
return `${prefix}${suffix}`;
|
||||
}
|
||||
|
||||
static generateMessage(minLength: number = 10, maxLength: number = 100): string {
|
||||
const messages = [
|
||||
'您好,我对贵公司的产品很感兴趣,希望能了解更多信息。',
|
||||
'请问贵公司是否有相关的技术支持服务?',
|
||||
'我们正在寻找合作伙伴,希望能与贵公司建立联系。',
|
||||
'贵公司的产品功能很强大,希望能安排一次演示。',
|
||||
'我对贵公司的服务有些建议和想法,希望能与您交流。',
|
||||
'请问贵公司的产品价格如何?是否有优惠政策?',
|
||||
'我们公司正在评估相关技术方案,希望能了解贵公司的解决方案。',
|
||||
'您好,我想咨询一下贵公司的产品定制服务。',
|
||||
];
|
||||
return messages[Math.floor(Math.random() * messages.length)];
|
||||
}
|
||||
|
||||
static generateSubject(): string {
|
||||
return this.SUBJECTS[Math.floor(Math.random() * this.SUBJECTS.length)];
|
||||
}
|
||||
|
||||
static generateContactFormData(): ContactFormData {
|
||||
return {
|
||||
name: this.generateName(),
|
||||
email: this.generateEmail(),
|
||||
phone: this.generatePhone(),
|
||||
company: this.generateCompany(),
|
||||
message: this.generateMessage(),
|
||||
subject: this.generateSubject(),
|
||||
};
|
||||
}
|
||||
|
||||
static generateTestData(): TestData {
|
||||
return {
|
||||
name: this.generateName(),
|
||||
email: this.generateEmail(),
|
||||
phone: this.generatePhone(),
|
||||
company: this.generateCompany(),
|
||||
message: this.generateMessage(),
|
||||
};
|
||||
}
|
||||
|
||||
static generateInvalidEmail(): string {
|
||||
const invalidEmails = [
|
||||
'invalid-email',
|
||||
'@example.com',
|
||||
'user@',
|
||||
'user@domain',
|
||||
'user domain.com',
|
||||
];
|
||||
return invalidEmails[Math.floor(Math.random() * invalidEmails.length)];
|
||||
}
|
||||
|
||||
static generateInvalidPhone(): string {
|
||||
const invalidPhones = [
|
||||
'123',
|
||||
'1234567890123',
|
||||
'abcdefghijk',
|
||||
'123-456-7890',
|
||||
];
|
||||
return invalidPhones[Math.floor(Math.random() * invalidPhones.length)];
|
||||
}
|
||||
|
||||
static generateShortMessage(): string {
|
||||
return '测试';
|
||||
}
|
||||
|
||||
static generateLongMessage(): string {
|
||||
return 'A'.repeat(1001);
|
||||
}
|
||||
|
||||
static generateSpecialCharacters(): string {
|
||||
return '!@#$%^&*()_+-=[]{}|;:,.<>?/~`';
|
||||
}
|
||||
|
||||
static generateChineseCharacters(): string {
|
||||
return '这是一段中文测试文本,包含了一些特殊字符:!@#¥%……&*()——+';
|
||||
}
|
||||
|
||||
static generateMixedContent(): string {
|
||||
return 'Hello 世界!This is a mixed content test 测试。';
|
||||
}
|
||||
|
||||
static generateNumberString(length: number): string {
|
||||
return Math.random().toString().slice(2, 2 + length);
|
||||
}
|
||||
|
||||
static generateAlphanumeric(length: number): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static generateWhitespace(): string {
|
||||
return ' \t\n\r ';
|
||||
}
|
||||
|
||||
static generateUrl(): string {
|
||||
const urls = [
|
||||
'https://example.com',
|
||||
'http://test.com',
|
||||
'https://demo.com/path',
|
||||
'http://example.com/page?param=value',
|
||||
];
|
||||
return urls[Math.floor(Math.random() * urls.length)];
|
||||
}
|
||||
|
||||
static generateDate(): string {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + Math.floor(Math.random() * 30));
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
static generateTime(): string {
|
||||
const hours = Math.floor(Math.random() * 24);
|
||||
const minutes = Math.floor(Math.random() * 60);
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
static generateBoolean(): boolean {
|
||||
return Math.random() < 0.5;
|
||||
}
|
||||
|
||||
static generateNumber(min: number = 0, max: number = 100): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
static generateFloat(min: number = 0, max: number = 100, decimals: number = 2): number {
|
||||
const num = Math.random() * (max - min) + min;
|
||||
return parseFloat(num.toFixed(decimals));
|
||||
}
|
||||
|
||||
static generateArray<T>(generator: () => T, length: number = 5): T[] {
|
||||
return Array.from({ length }, generator);
|
||||
}
|
||||
|
||||
static generateObject(): Record<string, any> {
|
||||
return {
|
||||
name: this.generateName(),
|
||||
email: this.generateEmail(),
|
||||
age: this.generateNumber(18, 65),
|
||||
active: this.generateBoolean(),
|
||||
score: this.generateFloat(0, 100),
|
||||
};
|
||||
}
|
||||
|
||||
static generateUuid(): string {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
static generateIPv4(): string {
|
||||
return `${this.generateNumber(0, 255)}.${this.generateNumber(0, 255)}.${this.generateNumber(0, 255)}.${this.generateNumber(0, 255)}`;
|
||||
}
|
||||
|
||||
static generateMacAddress(): string {
|
||||
const hex = '0123456789ABCDEF';
|
||||
let mac = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (i > 0) mac += ':';
|
||||
mac += hex[Math.floor(Math.random() * 16)];
|
||||
mac += hex[Math.floor(Math.random() * 16)];
|
||||
}
|
||||
return mac;
|
||||
}
|
||||
|
||||
static generateColor(): string {
|
||||
return `#${this.generateAlphanumeric(6)}`;
|
||||
}
|
||||
|
||||
static generateJson(): string {
|
||||
const obj = this.generateObject();
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
|
||||
static generateXml(): string {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<name>${this.generateName()}</name>
|
||||
<email>${this.generateEmail()}</email>
|
||||
<age>${this.generateNumber(18, 65)}</age>
|
||||
</root>`;
|
||||
}
|
||||
|
||||
static generateCsv(): string {
|
||||
const headers = ['Name,Email,Phone,Company'];
|
||||
const rows = this.generateArray(() =>
|
||||
`${this.generateName()},${this.generateEmail()},${this.generatePhone()},${this.generateCompany()}`,
|
||||
5
|
||||
);
|
||||
return [...headers, ...rows].join('\n');
|
||||
}
|
||||
|
||||
static generateHtml(): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${this.generateName()}</h1>
|
||||
<p>${this.generateMessage()}</p>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
static generateMarkdown(): string {
|
||||
return `# ${this.generateName()}
|
||||
|
||||
## Contact Information
|
||||
- Email: ${this.generateEmail()}
|
||||
- Phone: ${this.generatePhone()}
|
||||
|
||||
## Message
|
||||
${this.generateMessage()}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { DeviceConfig } from '../types';
|
||||
|
||||
export const devices: Record<string, DeviceConfig> = {
|
||||
'desktop-1920x1080': {
|
||||
name: 'Desktop 1920x1080',
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
isMobile: false,
|
||||
},
|
||||
'desktop-1366x768': {
|
||||
name: 'Desktop 1366x768',
|
||||
viewport: { width: 1366, height: 768 },
|
||||
isMobile: false,
|
||||
},
|
||||
'desktop-1280x720': {
|
||||
name: 'Desktop 1280x720',
|
||||
viewport: { width: 1280, height: 720 },
|
||||
isMobile: false,
|
||||
},
|
||||
'laptop-1440x900': {
|
||||
name: 'Laptop 1440x900',
|
||||
viewport: { width: 1440, height: 900 },
|
||||
isMobile: false,
|
||||
},
|
||||
'laptop-1024x768': {
|
||||
name: 'Laptop 1024x768',
|
||||
viewport: { width: 1024, height: 768 },
|
||||
isMobile: false,
|
||||
},
|
||||
'tablet-768x1024': {
|
||||
name: 'Tablet 768x1024',
|
||||
viewport: { width: 768, height: 1024 },
|
||||
isMobile: true,
|
||||
},
|
||||
'tablet-834x1194': {
|
||||
name: 'Tablet 834x1194 (iPad Pro)',
|
||||
viewport: { width: 834, height: 1194 },
|
||||
isMobile: true,
|
||||
},
|
||||
'mobile-375x667': {
|
||||
name: 'Mobile 375x667 (iPhone SE)',
|
||||
viewport: { width: 375, height: 667 },
|
||||
isMobile: true,
|
||||
},
|
||||
'mobile-390x844': {
|
||||
name: 'Mobile 390x844 (iPhone 12)',
|
||||
viewport: { width: 390, height: 844 },
|
||||
isMobile: true,
|
||||
},
|
||||
'mobile-414x896': {
|
||||
name: 'Mobile 414x896 (iPhone 11)',
|
||||
viewport: { width: 414, height: 896 },
|
||||
isMobile: true,
|
||||
},
|
||||
'mobile-360x640': {
|
||||
name: 'Mobile 360x640 (Android)',
|
||||
viewport: { width: 360, height: 640 },
|
||||
isMobile: true,
|
||||
},
|
||||
'mobile-412x915': {
|
||||
name: 'Mobile 412x915 (Pixel 5)',
|
||||
viewport: { width: 412, height: 915 },
|
||||
isMobile: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const desktopDevices = Object.entries(devices)
|
||||
.filter(([_, config]) => !config.isMobile)
|
||||
.map(([key, config]) => ({ key, ...config }));
|
||||
|
||||
export const mobileDevices = Object.entries(devices)
|
||||
.filter(([_, config]) => config.isMobile)
|
||||
.map(([key, config]) => ({ key, ...config }));
|
||||
|
||||
export const tabletDevices = Object.entries(devices)
|
||||
.filter(([_, config]) => config.isMobile && config.viewport.width >= 768)
|
||||
.map(([key, config]) => ({ key, ...config }));
|
||||
|
||||
export const getDevice = (key: string): DeviceConfig => {
|
||||
return devices[key] || devices['desktop-1280x720'];
|
||||
};
|
||||
|
||||
export const getAllDevices = (): DeviceConfig[] => {
|
||||
return Object.values(devices);
|
||||
};
|
||||
|
||||
export const getDesktopDevices = (): DeviceConfig[] => {
|
||||
return desktopDevices.map(d => devices[d.key]);
|
||||
};
|
||||
|
||||
export const getMobileDevices = (): DeviceConfig[] => {
|
||||
return mobileDevices.map(d => devices[d.key]);
|
||||
};
|
||||
|
||||
export const getTabletDevices = (): DeviceConfig[] => {
|
||||
return tabletDevices.map(d => devices[d.key]);
|
||||
};
|
||||
|
||||
export const getBreakpoints = () => {
|
||||
return {
|
||||
xs: 0,
|
||||
sm: 640,
|
||||
md: 768,
|
||||
lg: 1024,
|
||||
xl: 1280,
|
||||
'2xl': 1536,
|
||||
};
|
||||
};
|
||||
|
||||
export const isMobile = (width: number): boolean => {
|
||||
const breakpoints = getBreakpoints();
|
||||
return width < breakpoints.lg;
|
||||
};
|
||||
|
||||
export const isTablet = (width: number): boolean => {
|
||||
const breakpoints = getBreakpoints();
|
||||
return width >= breakpoints.md && width < breakpoints.lg;
|
||||
};
|
||||
|
||||
export const isDesktop = (width: number): boolean => {
|
||||
const breakpoints = getBreakpoints();
|
||||
return width >= breakpoints.lg;
|
||||
};
|
||||
Reference in New Issue
Block a user