Initial commit
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* HomePage Page Object Model
|
||||
*
|
||||
* Example POM for a React + Vite application homepage
|
||||
* Demonstrates best practices for locator selection
|
||||
*/
|
||||
|
||||
export class HomePage {
|
||||
readonly page: Page;
|
||||
|
||||
// Locators - Using semantic selectors (priority: getByRole > getByLabel > getByText > getByTestId)
|
||||
readonly welcomeMessage: Locator;
|
||||
readonly aboutLink: Locator;
|
||||
readonly contactLink: Locator;
|
||||
readonly navbar: Locator;
|
||||
readonly heroSection: Locator;
|
||||
readonly ctaButton: Locator;
|
||||
readonly featureCards: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
// Initialize locators with semantic selectors
|
||||
this.navbar = page.getByRole('navigation');
|
||||
this.welcomeMessage = page.getByRole('heading', { name: /welcome/i });
|
||||
this.aboutLink = page.getByRole('link', { name: /about/i });
|
||||
this.contactLink = page.getByRole('link', { name: /contact/i });
|
||||
this.heroSection = page.getByRole('banner');
|
||||
this.ctaButton = page.getByRole('button', { name: /get started/i });
|
||||
this.featureCards = page.getByRole('article');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to homepage
|
||||
*/
|
||||
async goto() {
|
||||
await this.page.goto('/');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for page to be fully loaded and ready
|
||||
*/
|
||||
async waitForReady() {
|
||||
await this.welcomeMessage.waitFor({ state: 'visible' });
|
||||
await this.navbar.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to About page
|
||||
*/
|
||||
async goToAbout() {
|
||||
await this.aboutLink.click();
|
||||
await this.page.waitForURL('**/about');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to Contact page
|
||||
*/
|
||||
async goToContact() {
|
||||
await this.contactLink.click();
|
||||
await this.page.waitForURL('**/contact');
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the main CTA button
|
||||
*/
|
||||
async clickCTA() {
|
||||
await this.ctaButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of feature cards
|
||||
*/
|
||||
async getFeatureCardCount(): Promise<number> {
|
||||
return await this.featureCards.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Take screenshot of homepage
|
||||
*/
|
||||
async screenshot(name: string = 'homepage') {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
await this.page.screenshot({
|
||||
path: `screenshots/current/${name}-${timestamp}.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { HomePage } from '../pages/home.page';
|
||||
import { captureWithContext } from '../utils/screenshot-helper';
|
||||
|
||||
/**
|
||||
* Example Playwright Test for React + Vite Application
|
||||
*
|
||||
* This demonstrates best practices for e2e testing with screenshot capture
|
||||
*/
|
||||
|
||||
test.describe('Homepage', () => {
|
||||
let homePage: HomePage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
homePage = new HomePage(page);
|
||||
await homePage.goto();
|
||||
|
||||
// Capture initial page load
|
||||
await captureWithContext(page, 'homepage-initial-load', 'Homepage loaded successfully');
|
||||
});
|
||||
|
||||
test('should display welcome message', async ({ page }) => {
|
||||
// Arrange: Page is already loaded in beforeEach
|
||||
|
||||
// Act: No action needed, just checking initial state
|
||||
await captureWithContext(page, 'homepage-welcome-check', 'Checking for welcome message');
|
||||
|
||||
// Assert: Welcome message is visible
|
||||
await expect(homePage.welcomeMessage).toBeVisible();
|
||||
await expect(homePage.welcomeMessage).toContainText('Welcome');
|
||||
});
|
||||
|
||||
test('should navigate to about page when clicking About link', async ({ page }) => {
|
||||
// Arrange: Page loaded
|
||||
await captureWithContext(page, 'homepage-before-nav', 'Before clicking About link');
|
||||
|
||||
// Act: Click About link
|
||||
await homePage.aboutLink.click();
|
||||
|
||||
// Capture after navigation
|
||||
await page.waitForURL('**/about');
|
||||
await captureWithContext(page, 'about-page-loaded', 'About page after navigation');
|
||||
|
||||
// Assert: URL changed and about page content visible
|
||||
expect(page.url()).toContain('/about');
|
||||
await expect(page.getByRole('heading', { name: 'About' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should submit contact form successfully', async ({ page }) => {
|
||||
// Arrange: Navigate to contact page
|
||||
await page.goto('/contact');
|
||||
await captureWithContext(page, 'contact-form-initial', 'Contact form initial state');
|
||||
|
||||
// Act: Fill out form
|
||||
await page.getByLabel('Name').fill('John Doe');
|
||||
await page.getByLabel('Email').fill('john@example.com');
|
||||
await page.getByLabel('Message').fill('This is a test message');
|
||||
|
||||
await captureWithContext(page, 'contact-form-filled', 'Form filled before submission');
|
||||
|
||||
await page.getByRole('button', { name: 'Send Message' }).click();
|
||||
|
||||
// Wait for success message
|
||||
await page.waitForSelector('[data-testid="success-message"]', { state: 'visible' });
|
||||
|
||||
await captureWithContext(page, 'contact-form-success', 'Success message displayed');
|
||||
|
||||
// Assert: Success message appears
|
||||
await expect(page.getByTestId('success-message')).toBeVisible();
|
||||
await expect(page.getByTestId('success-message')).toContainText('Message sent successfully');
|
||||
});
|
||||
|
||||
test('should validate required fields', async ({ page }) => {
|
||||
// Arrange: Navigate to contact page
|
||||
await page.goto('/contact');
|
||||
await captureWithContext(page, 'contact-form-validation-init', 'Before validation check');
|
||||
|
||||
// Act: Try to submit empty form
|
||||
await page.getByRole('button', { name: 'Send Message' }).click();
|
||||
|
||||
await captureWithContext(page, 'contact-form-validation-errors', 'Validation errors displayed');
|
||||
|
||||
// Assert: Error messages appear
|
||||
await expect(page.getByText('Name is required')).toBeVisible();
|
||||
await expect(page.getByText('Email is required')).toBeVisible();
|
||||
await expect(page.getByText('Message is required')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not have accessibility violations', async ({ page }) => {
|
||||
const AxeBuilder = (await import('@axe-core/playwright')).default;
|
||||
|
||||
const accessibilityScanResults = await new AxeBuilder({ page })
|
||||
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
||||
.analyze();
|
||||
|
||||
await captureWithContext(
|
||||
page,
|
||||
'homepage-accessibility-check',
|
||||
`Found ${accessibilityScanResults.violations.length} accessibility violations`
|
||||
);
|
||||
|
||||
// Log violations for review
|
||||
if (accessibilityScanResults.violations.length > 0) {
|
||||
console.log('\n⚠️ Accessibility Violations:');
|
||||
accessibilityScanResults.violations.forEach((violation) => {
|
||||
console.log(`\n- ${violation.id}: ${violation.description}`);
|
||||
console.log(` Impact: ${violation.impact}`);
|
||||
console.log(` Nodes: ${violation.nodes.length}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Fail on critical violations only (for this example)
|
||||
const criticalViolations = accessibilityScanResults.violations.filter(
|
||||
(v) => v.impact === 'critical' || v.impact === 'serious'
|
||||
);
|
||||
|
||||
expect(criticalViolations).toEqual([]);
|
||||
});
|
||||
|
||||
test('should display correctly across viewports', async ({ page }) => {
|
||||
const viewports = [
|
||||
{ name: 'desktop', width: 1280, height: 720 },
|
||||
{ name: 'tablet', width: 768, height: 1024 },
|
||||
{ name: 'mobile', width: 375, height: 667 },
|
||||
];
|
||||
|
||||
for (const viewport of viewports) {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.waitForTimeout(500); // Let responsive changes settle
|
||||
|
||||
await captureWithContext(
|
||||
page,
|
||||
`homepage-responsive-${viewport.name}`,
|
||||
`${viewport.width}x${viewport.height} viewport`
|
||||
);
|
||||
|
||||
// Verify no horizontal scroll on mobile/tablet
|
||||
if (viewport.name !== 'desktop') {
|
||||
const scrollWidth = await page.evaluate(() => document.body.scrollWidth);
|
||||
const clientWidth = await page.evaluate(() => document.body.clientWidth);
|
||||
expect(scrollWidth).toBeLessThanOrEqual(clientWidth + 1); // Allow 1px tolerance
|
||||
}
|
||||
|
||||
// Verify main navigation is accessible
|
||||
const nav = page.getByRole('navigation');
|
||||
await expect(nav).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user