550 lines
17 KiB
TypeScript
550 lines
17 KiB
TypeScript
import { Page, Locator } 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 emailInfo: Locator;
|
|
readonly phoneInfo: Locator;
|
|
readonly addressInfo: Locator;
|
|
readonly emailLink: Locator;
|
|
readonly phoneLink: Locator;
|
|
readonly addressText: Locator;
|
|
readonly pageBadge: Locator;
|
|
readonly pageDescription: Locator;
|
|
|
|
readonly successMessage: Locator;
|
|
|
|
readonly nameError: Locator;
|
|
readonly emailError: Locator;
|
|
readonly phoneError: Locator;
|
|
readonly messageError: Locator;
|
|
|
|
constructor(page: Page) {
|
|
super(page);
|
|
this.url = '/contact';
|
|
|
|
this.pageHeader = page.locator('h1');
|
|
this.contactForm = page.locator('form');
|
|
this.nameInput = page.locator('[data-testid="name-input"]');
|
|
this.phoneInput = page.locator('[data-testid="phone-input"]');
|
|
this.emailInput = page.locator('[data-testid="email-input"]');
|
|
this.subjectInput = page.locator('[data-testid="subject-input"]');
|
|
this.messageInput = page.locator('[data-testid="message-input"]');
|
|
this.submitButton = page.locator('[data-testid="submit-button"]');
|
|
|
|
this.contactInfoCard = page.locator('[data-testid="contact-info"]');
|
|
this.workHoursCard = page.locator('[data-testid="work-hours-card"]');
|
|
this.emailInfo = page.locator('[data-testid="email-info"]');
|
|
this.phoneInfo = page.locator('[data-testid="phone-info"]');
|
|
this.addressInfo = page.locator('[data-testid="address-info"]');
|
|
this.emailLink = page.locator('[data-testid="email-link"]');
|
|
this.phoneLink = page.locator('[data-testid="phone-link"]');
|
|
this.addressText = page.locator('[data-testid="address-text"]');
|
|
this.pageBadge = page.locator('[data-testid="page-badge"]');
|
|
this.pageDescription = page.locator('[data-testid="page-description"]');
|
|
|
|
this.successMessage = page.locator('text=消息已发送');
|
|
|
|
this.nameError = page.locator('[data-testid="name-input"] + .error-message, [data-testid="name-input"] ~ .text-destructive').first();
|
|
this.emailError = page.locator('[data-testid="email-input"] + .error-message, [data-testid="email-input"] ~ .text-destructive').first();
|
|
this.phoneError = page.locator('[data-testid="phone-input"] + .error-message, [data-testid="phone-input"] ~ .text-destructive').first();
|
|
this.messageError = page.locator('[data-testid="message-input"] + .error-message, [data-testid="message-input"] ~ .text-destructive').first();
|
|
}
|
|
|
|
get breadcrumb(): Locator {
|
|
return this.page.locator('nav[aria-label="breadcrumb"]');
|
|
}
|
|
|
|
async navigateToContact(): Promise<void> {
|
|
await this.navigate(this.url);
|
|
}
|
|
|
|
async verifyBreadcrumb(): Promise<boolean> {
|
|
return await this.breadcrumb.isVisible();
|
|
}
|
|
|
|
async verifyPageHeader(): Promise<boolean> {
|
|
const header = await this.pageHeader.textContent();
|
|
return header?.includes('与我们取得联系') || false;
|
|
}
|
|
|
|
async verifyContactForm(): Promise<boolean> {
|
|
return await this.contactForm.isVisible();
|
|
}
|
|
|
|
async verifyContactInfo(): Promise<boolean> {
|
|
return await this.contactInfoCard.isVisible();
|
|
}
|
|
|
|
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> {
|
|
console.log('Filling form with data:', data);
|
|
await this.fillContactForm(data);
|
|
console.log('Form filled, clicking submit button');
|
|
await this.submitForm();
|
|
console.log('Submit button clicked');
|
|
}
|
|
|
|
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> {
|
|
return await this.addressText.textContent() || '';
|
|
}
|
|
|
|
async getPhone(): Promise<string> {
|
|
return await this.phoneLink.textContent() || '';
|
|
}
|
|
|
|
async getEmail(): Promise<string> {
|
|
return await this.emailLink.textContent() || '';
|
|
}
|
|
|
|
async getPageTitle(): Promise<string> {
|
|
return await this.pageHeader.textContent() || '';
|
|
}
|
|
|
|
async getPageDescription(): Promise<string> {
|
|
return await this.pageDescription.textContent() || '';
|
|
}
|
|
|
|
async getBadgeText(): Promise<string> {
|
|
return await this.pageBadge.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(3000);
|
|
await this.page.waitForLoadState('networkidle');
|
|
await this.page.waitForTimeout(2000);
|
|
}
|
|
|
|
async isFormSubmitted(): Promise<boolean> {
|
|
const isSuccessVisible = await this.isSuccessMessageVisible();
|
|
console.log('Success message visible:', isSuccessVisible);
|
|
return isSuccessVisible;
|
|
}
|
|
|
|
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(`[data-testid="${fieldName}-input"]`);
|
|
await input.focus();
|
|
}
|
|
|
|
async blurField(fieldName: string): Promise<void> {
|
|
const input = this.page.locator(`[data-testid="${fieldName}-input"]`);
|
|
await input.blur();
|
|
}
|
|
|
|
async typeInField(fieldName: string, text: string, options?: { delay?: number }): Promise<void> {
|
|
const input = this.page.locator(`[data-testid="${fieldName}-input"]`);
|
|
await input.type(text, options);
|
|
}
|
|
|
|
async clearField(fieldName: string): Promise<void> {
|
|
const input = this.page.locator(`[data-testid="${fieldName}-input"]`);
|
|
await input.fill('');
|
|
}
|
|
|
|
async isFieldVisible(fieldName: string): Promise<boolean> {
|
|
const input = this.page.locator(`[data-testid="${fieldName}-input"]`);
|
|
return await input.isVisible();
|
|
}
|
|
|
|
async isFieldEnabled(fieldName: string): Promise<boolean> {
|
|
const input = this.page.locator(`[data-testid="${fieldName}-input"]`);
|
|
return await input.isEnabled();
|
|
}
|
|
|
|
async getFieldAttribute(fieldName: string, attribute: string): Promise<string | null> {
|
|
const input = this.page.locator(`[data-testid="${fieldName}-input"]`);
|
|
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;
|
|
}
|
|
|
|
async getNameError(): Promise<string> {
|
|
return await this.nameError.textContent() || '';
|
|
}
|
|
|
|
async getEmailError(): Promise<string> {
|
|
return await this.emailError.textContent() || '';
|
|
}
|
|
|
|
async getPhoneError(): Promise<string> {
|
|
return await this.phoneError.textContent() || '';
|
|
}
|
|
|
|
async getMessageError(): Promise<string> {
|
|
return await this.messageError.textContent() || '';
|
|
}
|
|
|
|
async isNameErrorVisible(): Promise<boolean> {
|
|
return await this.nameError.isVisible();
|
|
}
|
|
|
|
async isEmailErrorVisible(): Promise<boolean> {
|
|
return await this.emailError.isVisible();
|
|
}
|
|
|
|
async isPhoneErrorVisible(): Promise<boolean> {
|
|
return await this.phoneError.isVisible();
|
|
}
|
|
|
|
async isMessageErrorVisible(): Promise<boolean> {
|
|
return await this.messageError.isVisible();
|
|
}
|
|
|
|
async testXSSInjection(payload: string): Promise<void> {
|
|
await this.fillContactForm({
|
|
name: payload,
|
|
email: 'test@example.com',
|
|
phone: '13800138000',
|
|
message: payload,
|
|
});
|
|
await this.submitForm();
|
|
}
|
|
|
|
async testSQLInjection(payload: string): Promise<void> {
|
|
await this.fillContactForm({
|
|
name: payload,
|
|
email: payload,
|
|
phone: payload,
|
|
message: payload,
|
|
});
|
|
await this.submitForm();
|
|
}
|
|
|
|
async testPathTraversal(payload: string): Promise<void> {
|
|
await this.fillContactForm({
|
|
name: payload,
|
|
email: 'test@example.com',
|
|
phone: '13800138000',
|
|
message: payload,
|
|
});
|
|
await this.submitForm();
|
|
}
|
|
|
|
async verifyFormResponsiveLayout(viewport: { width: number; height: number }): Promise<{
|
|
isFormVisible: boolean;
|
|
isSubmitButtonVisible: boolean;
|
|
isContactInfoVisible: boolean;
|
|
}> {
|
|
await this.page.setViewportSize(viewport);
|
|
await this.waitForTimeout(500);
|
|
|
|
return {
|
|
isFormVisible: await this.isFormVisible(),
|
|
isSubmitButtonVisible: await this.isVisible(this.submitButton),
|
|
isContactInfoVisible: await this.isContactInfoCardVisible(),
|
|
};
|
|
}
|
|
|
|
async measureFormSubmissionPerformance(): Promise<{
|
|
fillTime: number;
|
|
submitTime: number;
|
|
totalTime: number;
|
|
}> {
|
|
const startTime = Date.now();
|
|
|
|
const data = {
|
|
name: '测试用户',
|
|
email: 'test@example.com',
|
|
phone: '13800138000',
|
|
message: '这是一条测试消息',
|
|
};
|
|
|
|
const fillStartTime = Date.now();
|
|
await this.fillContactForm(data);
|
|
const fillTime = Date.now() - fillStartTime;
|
|
|
|
const submitStartTime = Date.now();
|
|
await this.submitForm();
|
|
await this.waitForFormSubmission();
|
|
const submitTime = Date.now() - submitStartTime;
|
|
|
|
const totalTime = Date.now() - startTime;
|
|
|
|
return {
|
|
fillTime,
|
|
submitTime,
|
|
totalTime,
|
|
};
|
|
}
|
|
|
|
async getFormAccessibilityAttributes(): Promise<{
|
|
nameAriaLabel: string | null;
|
|
emailAriaLabel: string | null;
|
|
phoneAriaLabel: string | null;
|
|
messageAriaLabel: string | null;
|
|
submitAriaLabel: string | null;
|
|
}> {
|
|
return {
|
|
nameAriaLabel: await this.nameInput.getAttribute('aria-label'),
|
|
emailAriaLabel: await this.emailInput.getAttribute('aria-label'),
|
|
phoneAriaLabel: await this.phoneInput.getAttribute('aria-label'),
|
|
messageAriaLabel: await this.messageInput.getAttribute('aria-label'),
|
|
submitAriaLabel: await this.submitButton.getAttribute('aria-label'),
|
|
};
|
|
}
|
|
|
|
async verifyFormLabels(): Promise<{
|
|
nameLabel: string | null;
|
|
emailLabel: string | null;
|
|
phoneLabel: string | null;
|
|
messageLabel: string | null;
|
|
}> {
|
|
return {
|
|
nameLabel: await this.page.locator('label[for="name"]').textContent(),
|
|
emailLabel: await this.page.locator('label[for="email"]').textContent(),
|
|
phoneLabel: await this.page.locator('label[for="phone"]').textContent(),
|
|
messageLabel: await this.page.locator('label[for="message"]').textContent(),
|
|
};
|
|
}
|
|
|
|
async getFormInputTypes(): Promise<{
|
|
nameType: string | null;
|
|
emailType: string | null;
|
|
phoneType: string | null;
|
|
subjectType: string | null;
|
|
}> {
|
|
return {
|
|
nameType: await this.nameInput.getAttribute('type'),
|
|
emailType: await this.emailInput.getAttribute('type'),
|
|
phoneType: await this.phoneInput.getAttribute('type'),
|
|
subjectType: await this.subjectInput.getAttribute('type'),
|
|
};
|
|
}
|
|
|
|
async verifyRequiredFields(): Promise<{
|
|
nameRequired: boolean;
|
|
emailRequired: boolean;
|
|
phoneRequired: boolean;
|
|
messageRequired: boolean;
|
|
}> {
|
|
return {
|
|
nameRequired: await this.nameInput.getAttribute('required') !== null,
|
|
emailRequired: await this.emailInput.getAttribute('required') !== null,
|
|
phoneRequired: await this.phoneInput.getAttribute('required') !== null,
|
|
messageRequired: await this.messageInput.getAttribute('required') !== null,
|
|
};
|
|
}
|
|
|
|
async getFormAutocompleteAttributes(): Promise<{
|
|
nameAutocomplete: string | null;
|
|
emailAutocomplete: string | null;
|
|
phoneAutocomplete: string | null;
|
|
}> {
|
|
return {
|
|
nameAutocomplete: await this.nameInput.getAttribute('autocomplete'),
|
|
emailAutocomplete: await this.emailInput.getAttribute('autocomplete'),
|
|
phoneAutocomplete: await this.phoneInput.getAttribute('autocomplete'),
|
|
};
|
|
}
|
|
|
|
async verifyKeyboardNavigation(): Promise<void> {
|
|
await this.nameInput.focus();
|
|
await this.pressKey('Tab');
|
|
await this.pressKey('Tab');
|
|
await this.pressKey('Tab');
|
|
await this.pressKey('Tab');
|
|
await this.pressKey('Tab');
|
|
}
|
|
|
|
async isFormKeyboardAccessible(): Promise<boolean> {
|
|
try {
|
|
await this.verifyKeyboardNavigation();
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
}
|