test: add E2E tests for contact form security

This commit is contained in:
张翔
2026-03-24 11:41:20 +08:00
parent d6862b0754
commit 89726da2c7
4 changed files with 627 additions and 9 deletions
+12 -9
View File
@@ -4,29 +4,32 @@ export default defineConfig({
testDir: './src/tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: 2,
workers: 2,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['list'],
['html', { open: 'never' }],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/junit.xml' }],
['line'],
['list'],
],
timeout: 90000,
timeout: 60000,
expect: {
timeout: 30000,
timeout: 10000,
},
use: {
baseURL: 'http://localhost:3000',
trace: 'retain-on-failure',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
headless: true,
viewport: { width: 1280, height: 720 },
actionTimeout: 30000,
navigationTimeout: 60000,
actionTimeout: 10000,
navigationTimeout: 30000,
},
projects: [
{
name: 'chromium-no-auth',
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
+138
View File
@@ -0,0 +1,138 @@
import { Page, Locator } from '@playwright/test';
export class ContactFormPage {
readonly page: Page;
readonly nameInput: Locator;
readonly phoneInput: Locator;
readonly emailInput: Locator;
readonly messageInput: Locator;
readonly captchaQuestion: Locator;
readonly captchaInput: Locator;
readonly refreshCaptchaButton: Locator;
readonly submitButton: Locator;
readonly successMessage: Locator;
readonly errorMessage: Locator;
readonly captchaErrorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.nameInput = page.getByTestId('name-input');
this.phoneInput = page.getByTestId('phone-input');
this.emailInput = page.getByTestId('email-input');
this.messageInput = page.getByTestId('message-input');
this.captchaQuestion = page.getByTestId('captcha-question');
this.captchaInput = page.getByTestId('captcha-input');
this.refreshCaptchaButton = page.getByTestId('refresh-captcha');
this.submitButton = page.getByRole('button', { name: /发送消息/ });
this.successMessage = page.getByText('消息已成功发送');
this.errorMessage = page.getByRole('alert');
this.captchaErrorMessage = page.getByTestId('captcha-error');
}
async goto() {
await this.page.goto('/contact');
}
async fillForm(data: {
name: string;
phone: string;
email: string;
message: string;
}) {
await this.nameInput.fill(data.name);
await this.phoneInput.fill(data.phone);
await this.emailInput.fill(data.email);
await this.messageInput.fill(data.message);
}
async solveCaptcha() {
const questionText = await this.captchaQuestion.textContent();
if (!questionText) throw new Error('Captcha question not found');
const match = questionText.match(/(\d+)\s*([+\-×÷])\s*(\d+)\s*=/);
if (!match) throw new Error('Invalid captcha format');
const [, num1, operator, num2] = match;
const n1 = parseInt(num1);
const n2 = parseInt(num2);
let answer: number;
switch (operator) {
case '+':
answer = n1 + n2;
break;
case '-':
answer = n1 - n2;
break;
case '×':
answer = n1 * n2;
break;
case '÷':
answer = n1 / n2;
break;
default:
throw new Error(`Unknown operator: ${operator}`);
}
await this.captchaInput.fill(answer.toString());
}
async submit() {
await this.submitButton.click();
}
async submitForm(data: {
name: string;
phone: string;
email: string;
message: string;
}) {
await this.fillForm(data);
await this.solveCaptcha();
await this.submit();
}
async refreshCaptcha() {
await this.refreshCaptchaButton.click();
}
async getCaptchaQuestion(): Promise<string> {
return (await this.captchaQuestion.textContent()) || '';
}
async getSuccessMessage(): Promise<string | null> {
try {
return await this.successMessage.textContent();
} catch {
return null;
}
}
async getErrorMessage(): Promise<string | null> {
try {
return await this.errorMessage.textContent();
} catch {
return null;
}
}
async getCaptchaErrorMessage(): Promise<string | null> {
try {
return await this.captchaErrorMessage.textContent();
} catch {
return null;
}
}
async waitForSuccessMessage() {
await this.successMessage.waitFor({ state: 'visible', timeout: 5000 });
}
async waitForErrorMessage() {
await this.errorMessage.waitFor({ state: 'visible', timeout: 5000 });
}
async isSubmitButtonEnabled(): Promise<boolean> {
return await this.submitButton.isEnabled();
}
}
+254
View File
@@ -0,0 +1,254 @@
import { test, expect } from '@playwright/test';
import { ContactFormPage } from '../pages/ContactFormPage';
test.describe('Contact Form Security E2E Tests', () => {
let contactPage: ContactFormPage;
test.beforeEach(async ({ page }) => {
contactPage = new ContactFormPage(page);
await contactPage.goto();
});
test.describe('Captcha Functionality', () => {
test('should display captcha question', async () => {
const question = await contactPage.getCaptchaQuestion();
expect(question).toMatch(/\d+\s*[+\-×÷]\s*\d+\s*=/);
});
test('should refresh captcha when refresh button is clicked', async () => {
const firstQuestion = await contactPage.getCaptchaQuestion();
await contactPage.refreshCaptcha();
const secondQuestion = await contactPage.getCaptchaQuestion();
expect(secondQuestion).toBeTruthy();
});
test('should submit form with correct captcha', async ({ page }) => {
const formData = {
name: '张三',
phone: '13800138000',
email: 'test@example.com',
message: '这是一条测试留言内容',
};
await contactPage.submitForm(formData);
await expect(page).toHaveURL(/\/contact/);
const successMessage = await contactPage.getSuccessMessage();
expect(successMessage).toContain('成功');
});
test('should show error for incorrect captcha', async ({ page }) => {
const formData = {
name: '张三',
phone: '13800138000',
email: 'test@example.com',
message: '这是一条测试留言内容',
};
await contactPage.fillForm(formData);
await contactPage.captchaInput.fill('999');
await contactPage.submit();
const captchaError = await contactPage.getCaptchaErrorMessage();
expect(captchaError).toContain('验证码错误');
});
});
test.describe('Form Validation', () => {
test('should validate name field', async ({ page }) => {
await contactPage.nameInput.fill('');
await contactPage.nameInput.blur();
const errorMessage = await contactPage.getErrorMessage();
expect(errorMessage).toBeTruthy();
});
test('should validate phone field', async ({ page }) => {
await contactPage.phoneInput.fill('123');
await contactPage.phoneInput.blur();
const errorMessage = await contactPage.getErrorMessage();
expect(errorMessage).toBeTruthy();
});
test('should validate email field', async ({ page }) => {
await contactPage.emailInput.fill('invalid-email');
await contactPage.emailInput.blur();
const errorMessage = await contactPage.getErrorMessage();
expect(errorMessage).toBeTruthy();
});
test('should validate message field', async ({ page }) => {
await contactPage.messageInput.fill('太短');
await contactPage.messageInput.blur();
const errorMessage = await contactPage.getErrorMessage();
expect(errorMessage).toBeTruthy();
});
});
test.describe('Security Features', () => {
test('should prevent XSS attacks in form fields', async ({ page }) => {
const xssPayload = '<script>alert("XSS")</script>';
await contactPage.nameInput.fill(xssPayload);
await contactPage.messageInput.fill(xssPayload);
await contactPage.solveCaptcha();
await contactPage.submit();
await expect(page.locator('script')).not.toBeAttached();
});
test('should handle SQL injection attempts', async ({ page }) => {
const sqlPayload = "'; DROP TABLE users; --";
await contactPage.nameInput.fill(sqlPayload);
await contactPage.messageInput.fill(sqlPayload);
await contactPage.solveCaptcha();
await contactPage.submit();
const successMessage = await contactPage.getSuccessMessage();
expect(successMessage).toBeNull();
});
test('should sanitize malicious content', async ({ page }) => {
const maliciousContent = '<img src=x onerror=alert(1)>';
await contactPage.messageInput.fill(maliciousContent);
await contactPage.solveCaptcha();
await contactPage.submit();
await expect(page.locator('img[onerror]')).not.toBeAttached();
});
});
test.describe('Rate Limiting', () => {
test('should enforce rate limiting on rapid submissions', async ({ page }) => {
const formData = {
name: '张三',
phone: '13800138000',
email: 'test@example.com',
message: '这是一条测试留言内容',
};
let submissionCount = 0;
let rateLimited = false;
for (let i = 0; i < 15; i++) {
await contactPage.goto();
await contactPage.fillForm(formData);
await contactPage.solveCaptcha();
await contactPage.submit();
const errorMessage = await contactPage.getErrorMessage();
if (errorMessage && errorMessage.includes('过于频繁')) {
rateLimited = true;
break;
}
submissionCount++;
await page.waitForTimeout(100);
}
expect(rateLimited).toBe(true);
});
test('should allow submissions after rate limit window', async ({ page }) => {
const formData = {
name: '张三',
phone: '13800138000',
email: 'test@example.com',
message: '这是一条测试留言内容',
};
await contactPage.submitForm(formData);
await page.waitForTimeout(61000);
await contactPage.goto();
await contactPage.submitForm(formData);
const successMessage = await contactPage.getSuccessMessage();
expect(successMessage).toContain('成功');
});
});
test.describe('Accessibility', () => {
test('should have proper form labels', async () => {
await expect(contactPage.nameInput).toBeVisible();
await expect(contactPage.phoneInput).toBeVisible();
await expect(contactPage.emailInput).toBeVisible();
await expect(contactPage.messageInput).toBeVisible();
await expect(contactPage.captchaInput).toBeVisible();
});
test('should be keyboard navigable', async ({ page }) => {
await contactPage.nameInput.focus();
await page.keyboard.press('Tab');
await expect(contactPage.phoneInput).toBeFocused();
await page.keyboard.press('Tab');
await expect(contactPage.emailInput).toBeFocused();
await page.keyboard.press('Tab');
await expect(contactPage.messageInput).toBeFocused();
await page.keyboard.press('Tab');
await expect(contactPage.captchaInput).toBeFocused();
await page.keyboard.press('Tab');
await expect(contactPage.submitButton).toBeFocused();
});
});
test.describe('Responsive Design', () => {
test('should work on mobile devices', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await contactPage.goto();
await expect(contactPage.nameInput).toBeVisible();
await expect(contactPage.submitButton).toBeVisible();
});
test('should work on tablet devices', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await contactPage.goto();
await expect(contactPage.nameInput).toBeVisible();
await expect(contactPage.submitButton).toBeVisible();
});
});
test.describe('User Flow', () => {
test('should complete full contact form submission flow', async ({ page }) => {
await test.step('Navigate to contact page', async () => {
await contactPage.goto();
await expect(page).toHaveURL(/\/contact/);
});
await test.step('Fill in all required fields', async () => {
const formData = {
name: '李四',
phone: '13900139000',
email: 'lisi@example.com',
message: '我想咨询贵公司的服务详情,请尽快联系我。',
};
await contactPage.fillForm(formData);
});
await test.step('Solve captcha', async () => {
await contactPage.solveCaptcha();
});
await test.step('Submit form', async () => {
await contactPage.submit();
});
await test.step('Verify success message', async () => {
await contactPage.waitForSuccessMessage();
const successMessage = await contactPage.getSuccessMessage();
expect(successMessage).toContain('成功');
});
});
});
});
+223
View File
@@ -0,0 +1,223 @@
import { test, expect } from '@playwright/test';
test.describe('Contact Form E2E Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/contact');
});
test.describe('Form Rendering', () => {
test('should display contact form', async ({ page }) => {
await expect(page.getByTestId('name-input')).toBeVisible();
await expect(page.getByTestId('phone-input')).toBeVisible();
await expect(page.getByTestId('email-input')).toBeVisible();
await expect(page.getByTestId('subject-input')).toBeVisible();
await expect(page.getByTestId('message-input')).toBeVisible();
await expect(page.getByTestId('submit-button')).toBeVisible();
});
test.skip('should display contact information', async ({ page }) => {
await expect(page.getByTestId('contact-info')).toBeVisible();
await expect(page.getByTestId('email-link')).toBeVisible();
await expect(page.getByTestId('phone-link')).toBeVisible();
await expect(page.getByTestId('address-text')).toBeVisible();
});
test('should display work hours', async ({ page }) => {
await expect(page.getByTestId('work-hours-card')).toBeVisible();
await expect(page.getByText('9:00 - 18:00')).toBeVisible();
});
});
test.describe('Form Validation', () => {
test('should validate name field', async ({ page }) => {
await page.getByTestId('name-input').fill('');
await page.getByTestId('name-input').blur();
await page.getByTestId('submit-button').click();
await expect(page.getByText('姓名至少需要2个字符')).toBeVisible();
});
test('should validate phone field', async ({ page }) => {
await page.getByTestId('phone-input').fill('123');
await page.getByTestId('phone-input').blur();
await page.getByTestId('submit-button').click();
await expect(page.getByText('请输入有效的手机号码')).toBeVisible();
});
test('should validate email field', async ({ page }) => {
await page.getByTestId('email-input').fill('invalid-email');
await page.getByTestId('email-input').blur();
await page.getByTestId('submit-button').click();
await expect(page.getByText('请输入有效的邮箱地址')).toBeVisible();
});
test('should validate subject field', async ({ page }) => {
await page.getByTestId('subject-input').fill('');
await page.getByTestId('subject-input').blur();
await page.getByTestId('submit-button').click();
await expect(page.getByText('主题至少需要2个字符')).toBeVisible();
});
test('should validate message field', async ({ page }) => {
await page.getByTestId('message-input').fill('太短');
await page.getByTestId('message-input').blur();
await page.getByTestId('submit-button').click();
await expect(page.getByText('留言内容至少需要10个字符')).toBeVisible();
});
});
test.describe('Form Submission', () => {
test.skip('should submit form with valid data', async ({ page }) => {
await page.getByTestId('name-input').fill('张三');
await page.getByTestId('phone-input').fill('13800138000');
await page.getByTestId('email-input').fill('test@example.com');
await page.getByTestId('subject-input').fill('咨询业务');
await page.getByTestId('message-input').fill('我想咨询贵公司的服务详情,请尽快联系我。');
await page.getByTestId('submit-button').click();
await expect(page.getByText('消息已发送')).toBeVisible();
});
test.skip('should show loading state during submission', async ({ page }) => {
await page.getByTestId('name-input').fill('张三');
await page.getByTestId('phone-input').fill('13800138000');
await page.getByTestId('email-input').fill('test@example.com');
await page.getByTestId('subject-input').fill('咨询业务');
await page.getByTestId('message-input').fill('我想咨询贵公司的服务详情,请尽快联系我。');
await page.getByTestId('submit-button').click();
await expect(page.getByText('发送中...')).toBeVisible();
});
test.skip('should reset form after successful submission', async ({ page }) => {
await page.getByTestId('name-input').fill('张三');
await page.getByTestId('phone-input').fill('13800138000');
await page.getByTestId('email-input').fill('test@example.com');
await page.getByTestId('subject-input').fill('咨询业务');
await page.getByTestId('message-input').fill('我想咨询贵公司的服务详情,请尽快联系我。');
await page.getByTestId('submit-button').click();
await expect(page.getByText('消息已发送')).toBeVisible();
});
});
test.describe('Security Features', () => {
test.skip('should have CSRF token', async ({ page }) => {
const csrfToken = await page.locator('input[name="_csrf"]').inputValue();
expect(csrfToken).toBeTruthy();
expect(csrfToken.length).toBeGreaterThan(0);
});
test('should have honeypot field', async ({ page }) => {
const honeypot = page.locator('input[name="website"]');
await expect(honeypot).toHaveAttribute('style', /display:\s*none/);
await expect(honeypot).toHaveAttribute('tabIndex', '-1');
});
test('should sanitize input on change', async ({ page }) => {
const xssPayload = '<script>alert("XSS")</script>';
await page.getByTestId('name-input').fill(xssPayload);
const nameValue = await page.getByTestId('name-input').inputValue();
expect(nameValue).not.toContain('<script>');
});
});
test.describe('Accessibility', () => {
test('should have proper form labels', async ({ page }) => {
await expect(page.getByLabel('姓名')).toBeVisible();
await expect(page.getByLabel('电话')).toBeVisible();
await expect(page.getByLabel('邮箱')).toBeVisible();
await expect(page.getByLabel('主题')).toBeVisible();
await expect(page.getByLabel('留言内容')).toBeVisible();
});
test('should be keyboard navigable', async ({ page }) => {
await page.getByTestId('name-input').focus();
await page.keyboard.press('Tab');
await expect(page.getByTestId('phone-input')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByTestId('email-input')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByTestId('subject-input')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByTestId('message-input')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByTestId('submit-button')).toBeFocused();
});
test('should have proper ARIA attributes', async ({ page }) => {
await expect(page.getByRole('button', { name: '发送消息' })).toBeVisible();
await expect(page.getByRole('textbox', { name: '姓名' })).toBeVisible();
await expect(page.getByRole('textbox', { name: '留言内容' })).toBeVisible();
});
});
test.describe('Responsive Design', () => {
test('should work on mobile devices', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/contact');
await expect(page.getByTestId('name-input')).toBeVisible();
await expect(page.getByTestId('submit-button')).toBeVisible();
});
test('should work on tablet devices', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await page.goto('/contact');
await expect(page.getByTestId('name-input')).toBeVisible();
await expect(page.getByTestId('submit-button')).toBeVisible();
});
test('should work on desktop devices', async ({ page }) => {
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto('/contact');
await expect(page.getByTestId('name-input')).toBeVisible();
await expect(page.getByTestId('submit-button')).toBeVisible();
});
});
test.describe('User Flow', () => {
test.skip('should complete full contact form submission flow', async ({ page }) => {
await test.step('Navigate to contact page', async () => {
await page.goto('/contact');
await expect(page).toHaveURL(/\/contact/);
});
await test.step('Fill in all required fields', async () => {
await page.getByTestId('name-input').fill('李四');
await page.getByTestId('phone-input').fill('13900139000');
await page.getByTestId('email-input').fill('lisi@example.com');
await page.getByTestId('subject-input').fill('产品咨询');
await page.getByTestId('message-input').fill('我想了解贵公司的产品详情,请尽快联系我。');
});
await test.step('Submit form', async () => {
await page.getByTestId('submit-button').click();
});
await test.step('Verify success message', async () => {
await expect(page.getByText('消息已发送')).toBeVisible();
});
});
});
});