Fix: legit logins only

This commit is contained in:
Zakaria
2026-05-16 13:57:02 -04:00
parent 677cae0125
commit 69ed1c78ad
78 changed files with 27211 additions and 8 deletions
@@ -0,0 +1,315 @@
# Page Object Model (POM)
## Table of Contents
1. [Overview](#overview)
2. [Basic Structure](#basic-structure)
3. [Component Objects](#component-objects)
4. [Composition Patterns](#composition-patterns)
5. [Factory Functions](#factory-functions)
6. [Best Practices](#best-practices)
## Overview
Page Object Model encapsulates page structure and interactions, providing:
- **Maintainability**: Change selectors in one place
- **Reusability**: Share page interactions across tests
- **Readability**: Tests express intent, not implementation
## Basic Structure
### Page Class
```typescript
// pages/login.page.ts
import { Page, Locator, expect } from "@playwright/test";
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
this.submitButton = page.getByRole("button", { name: "Sign in" });
this.errorMessage = page.getByRole("alert");
}
async goto() {
await this.page.goto("/login");
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}
```
### Usage in Tests
```typescript
// tests/login.spec.ts
import { test, expect } from "@playwright/test";
import { LoginPage } from "../pages/login.page";
test.describe("Login", () => {
test("successful login redirects to dashboard", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("user@example.com", "password123");
await expect(page).toHaveURL("/dashboard");
});
test("shows error for invalid credentials", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("invalid@example.com", "wrong");
await loginPage.expectError("Invalid credentials");
});
});
```
## Component Objects
For reusable UI components:
```typescript
// components/navbar.component.ts
import { Page, Locator } from "@playwright/test";
export class NavbarComponent {
readonly container: Locator;
readonly logo: Locator;
readonly searchInput: Locator;
readonly userMenu: Locator;
constructor(page: Page) {
this.container = page.getByRole("navigation");
this.logo = this.container.getByRole("link", { name: "Home" });
this.searchInput = this.container.getByRole("searchbox");
this.userMenu = this.container.getByRole("button", { name: /user menu/i });
}
async search(query: string) {
await this.searchInput.fill(query);
await this.searchInput.press("Enter");
}
async openUserMenu() {
await this.userMenu.click();
}
}
```
```typescript
// components/modal.component.ts
import { Locator, expect } from "@playwright/test";
export class ModalComponent {
readonly container: Locator;
readonly title: Locator;
readonly closeButton: Locator;
readonly confirmButton: Locator;
constructor(container: Locator) {
this.container = container;
this.title = container.getByRole("heading");
this.closeButton = container.getByRole("button", { name: "Close" });
this.confirmButton = container.getByRole("button", { name: "Confirm" });
}
async expectTitle(title: string) {
await expect(this.title).toHaveText(title);
}
async close() {
await this.closeButton.click();
}
async confirm() {
await this.confirmButton.click();
}
}
```
## Composition Patterns
### Page with Components
```typescript
// pages/dashboard.page.ts
import { Page, Locator } from "@playwright/test";
import { NavbarComponent } from "../components/navbar.component";
import { ModalComponent } from "../components/modal.component";
export class DashboardPage {
readonly page: Page;
readonly navbar: NavbarComponent;
readonly newProjectButton: Locator;
constructor(page: Page) {
this.page = page;
this.navbar = new NavbarComponent(page);
this.newProjectButton = page.getByRole("button", { name: "New Project" });
}
async goto() {
await this.page.goto("/dashboard");
}
async createProject() {
await this.newProjectButton.click();
return new ModalComponent(this.page.getByRole("dialog"));
}
}
```
### Page Navigation
```typescript
// pages/base.page.ts
import { Page } from "@playwright/test";
export abstract class BasePage {
constructor(readonly page: Page) {}
abstract goto(): Promise<void>;
async getTitle(): Promise<string> {
return this.page.title();
}
}
```
```typescript
// Return new page object on navigation
export class LoginPage extends BasePage {
async login(email: string, password: string): Promise<DashboardPage> {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
return new DashboardPage(this.page);
}
}
// Usage
const loginPage = new LoginPage(page);
await loginPage.goto();
const dashboardPage = await loginPage.login("user@example.com", "pass");
await dashboardPage.expectWelcomeMessage();
```
## Factory Functions
Alternative to classes for simpler pages:
```typescript
// pages/login.page.ts
import { Page } from "@playwright/test";
export function createLoginPage(page: Page) {
const emailInput = page.getByLabel("Email");
const passwordInput = page.getByLabel("Password");
const submitButton = page.getByRole("button", { name: "Sign in" });
return {
goto: () => page.goto("/login"),
login: async (email: string, password: string) => {
await emailInput.fill(email);
await passwordInput.fill(password);
await submitButton.click();
},
emailInput,
passwordInput,
submitButton,
};
}
// Usage
const loginPage = createLoginPage(page);
await loginPage.goto();
await loginPage.login("user@example.com", "password");
```
## Best Practices
### Do
- **Keep locators in page objects** - Single source of truth
- **Return new page objects** when navigation occurs
- **Expose elements** for custom assertions in tests
- **Use descriptive method names** - `submitOrder()` not `clickButton()`
- **Keep methods focused** - One action per method
### Don't
- **Don't include assertions in page methods** (usually) - Keep in tests
- **Don't expose implementation details** - Hide complex interactions
- **Don't make page objects too large** - Split into components
- **Don't share state** between page object instances
### Directory Structure
```
tests/
├── pages/
│ ├── base.page.ts
│ ├── login.page.ts
│ ├── dashboard.page.ts
│ └── settings.page.ts
├── components/
│ ├── navbar.component.ts
│ ├── modal.component.ts
│ └── table.component.ts
├── fixtures/
│ └── pages.fixture.ts
└── specs/
├── login.spec.ts
└── dashboard.spec.ts
```
### Using with Fixtures
```typescript
// fixtures/pages.fixture.ts
import { test as base } from "@playwright/test";
import { LoginPage } from "../pages/login.page";
import { DashboardPage } from "../pages/dashboard.page";
type Pages = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
};
export const test = base.extend<Pages>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
});
// Usage in tests
test("can login", async ({ loginPage }) => {
await loginPage.goto();
await loginPage.login("user@example.com", "password");
});
```
## Related References
- **Locator strategies**: See [locators.md](locators.md) for selecting elements
- **Fixtures**: See [fixtures-hooks.md](fixtures-hooks.md) for advanced fixture patterns
- **Test organization**: See [test-suite-structure.md](test-suite-structure.md) for structuring test suites