First Init
Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { type Page, type Locator, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Page Object Model for the Contact Page
|
||||
* Encapsulates all selectors and interactions for the contact form
|
||||
*/
|
||||
export class ContactPage {
|
||||
readonly page: Page;
|
||||
readonly form: Locator;
|
||||
readonly nameInput: Locator;
|
||||
readonly emailInput: Locator;
|
||||
readonly phoneInput: Locator;
|
||||
readonly subjectSelect: Locator;
|
||||
readonly messageTextarea: Locator;
|
||||
readonly submitButton: Locator;
|
||||
readonly honeypotInput: Locator;
|
||||
readonly toastContainer: Locator;
|
||||
|
||||
// Error elements
|
||||
readonly nameError: Locator;
|
||||
readonly emailError: Locator;
|
||||
readonly phoneError: Locator;
|
||||
readonly subjectError: Locator;
|
||||
readonly messageError: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.form = page.locator('#contact-form');
|
||||
this.nameInput = page.locator('input[name="name"]');
|
||||
this.emailInput = page.locator('input[name="email"]');
|
||||
this.phoneInput = page.locator('input[name="phone"]');
|
||||
this.subjectSelect = page.locator('select[name="subject"]');
|
||||
this.messageTextarea = page.locator('textarea[name="message"]');
|
||||
this.submitButton = page.locator('button[type="submit"]');
|
||||
this.honeypotInput = page.locator('input[name="website"]');
|
||||
this.toastContainer = page.locator('#toast-container');
|
||||
|
||||
this.nameError = page.locator('[data-error="name"]');
|
||||
this.emailError = page.locator('[data-error="email"]');
|
||||
this.phoneError = page.locator('[data-error="phone"]');
|
||||
this.subjectError = page.locator('[data-error="subject"]');
|
||||
this.messageError = page.locator('[data-error="message"]');
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/contact');
|
||||
await this.page.waitForLoadState('domcontentloaded');
|
||||
}
|
||||
|
||||
async fillValidForm(overrides: Partial<{
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
}> = {}) {
|
||||
const data = {
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
phone: '+1 555-123-4567',
|
||||
subject: 'web-development',
|
||||
message: 'I am interested in building a custom web application for my business.',
|
||||
...overrides,
|
||||
};
|
||||
|
||||
await this.nameInput.fill(data.name);
|
||||
await this.emailInput.fill(data.email);
|
||||
if (data.phone) await this.phoneInput.fill(data.phone);
|
||||
await this.subjectSelect.selectOption(data.subject);
|
||||
await this.messageTextarea.fill(data.message);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
await this.submitButton.click();
|
||||
}
|
||||
|
||||
async fillAndSubmit(overrides: Parameters<typeof this.fillValidForm>[0] = {}) {
|
||||
await this.fillValidForm(overrides);
|
||||
await this.submit();
|
||||
}
|
||||
|
||||
async waitForSuccessToast() {
|
||||
await expect(this.page.locator('#toast-container')).toContainText(/sent successfully/i, { timeout: 10000 });
|
||||
}
|
||||
|
||||
async waitForErrorToast() {
|
||||
await expect(this.page.locator('#toast-container')).toContainText(/error|wrong|fix/i, { timeout: 5000 });
|
||||
}
|
||||
|
||||
async isSubmitButtonDisabled() {
|
||||
return this.submitButton.isDisabled();
|
||||
}
|
||||
|
||||
async getFieldError(field: 'name' | 'email' | 'phone' | 'subject' | 'message') {
|
||||
const errorLocator = this.page.locator(`[data-error="${field}"]`);
|
||||
const isHidden = await errorLocator.evaluate((el) => el.classList.contains('hidden'));
|
||||
return !isHidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { type Page, type Locator, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Page Object Model for Navigation
|
||||
* Encapsulates header nav, footer nav, and mobile menu interactions
|
||||
*/
|
||||
export class NavigationPage {
|
||||
readonly page: Page;
|
||||
readonly header: Locator;
|
||||
readonly footer: Locator;
|
||||
readonly logo: Locator;
|
||||
readonly mobileMenuButton: Locator;
|
||||
|
||||
readonly navLinks = {
|
||||
home: '/href="/"',
|
||||
about: '/about',
|
||||
services: '/services',
|
||||
portfolio: '/portfolio',
|
||||
blog: '/blog',
|
||||
contact: '/contact',
|
||||
};
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.header = page.locator('header');
|
||||
this.footer = page.locator('footer');
|
||||
this.logo = page.locator('header a').first();
|
||||
this.mobileMenuButton = page.locator('button[aria-label*="menu"], button[aria-expanded], [data-menu-toggle]').first();
|
||||
}
|
||||
|
||||
async clickNavLink(path: string) {
|
||||
await this.page.locator(`header a[href="${path}"]`).first().click();
|
||||
await expect(this.page).toHaveURL(path);
|
||||
}
|
||||
|
||||
async openMobileMenu() {
|
||||
if (await this.mobileMenuButton.isVisible()) {
|
||||
await this.mobileMenuButton.click();
|
||||
await this.page.waitForTimeout(300); // Wait for animation
|
||||
}
|
||||
}
|
||||
|
||||
async closeMobileMenu() {
|
||||
if (await this.mobileMenuButton.isVisible()) {
|
||||
const isOpen = await this.mobileMenuButton.getAttribute('aria-expanded');
|
||||
if (isOpen === 'true') {
|
||||
await this.mobileMenuButton.click();
|
||||
await this.page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async scrollToFooter() {
|
||||
await this.footer.scrollIntoViewIfNeeded();
|
||||
await this.page.waitForTimeout(200);
|
||||
}
|
||||
|
||||
async clickFooterLink(path: string) {
|
||||
const link = this.footer.locator(`a[href="${path}"]`).first();
|
||||
await link.click();
|
||||
await expect(this.page).toHaveURL(path);
|
||||
}
|
||||
|
||||
async verifyNoHorizontalOverflow(tolerance = 20) {
|
||||
const bodyWidth = await this.page.evaluate(() => document.body.scrollWidth);
|
||||
const viewportWidth = this.page.viewportSize()?.width ?? 1280;
|
||||
expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + tolerance);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user