|
| 1 | +--- |
| 2 | +globs: core-web/apps/dotcms-ui-e2e/**/*.spec.ts |
| 3 | +alwaysApply: false |
| 4 | +--- |
| 5 | + |
| 6 | +# Page Object Model (POM) Conventions for dotCMS E2E Tests |
| 7 | + |
| 8 | +## Overview |
| 9 | + |
| 10 | +This project follows the **Page Object Model (POM)** pattern for all E2E tests. This ensures maintainability, reusability, and clear separation of concerns. |
| 11 | + |
| 12 | +## Directory Structure |
| 13 | + |
| 14 | +``` |
| 15 | +src/ |
| 16 | +├── pages/ # Page Objects (one class per page) |
| 17 | +│ ├── login.page.ts |
| 18 | +│ ├── dashboard.page.ts |
| 19 | +│ └── content.page.ts |
| 20 | +├── components/ # Reusable UI components |
| 21 | +│ ├── sideMenu.component.ts |
| 22 | +│ ├── header.component.ts |
| 23 | +│ └── modal.component.ts |
| 24 | +├── tests/ # Test files that use Page Objects |
| 25 | +│ ├── login/ |
| 26 | +│ ├── content/ |
| 27 | +│ └── navigation/ |
| 28 | +├── utils/ # Shared utilities and helpers |
| 29 | +│ └── utils.ts |
| 30 | +└── config/ # Configuration files |
| 31 | + └── environments.ts |
| 32 | +``` |
| 33 | + |
| 34 | +## POM Rules |
| 35 | + |
| 36 | +### 1. Page Objects |
| 37 | + |
| 38 | +- **One class per page** - Each page has its own Page Object class |
| 39 | +- **Encapsulate all page interactions** - All `page.fill()`, `page.click()`, etc. should be in Page Objects |
| 40 | +- **Return meaningful data** - Methods should return relevant information when needed |
| 41 | +- **Handle environment differences** - Page Objects should adapt to different environments (dev/ci) |
| 42 | +- **ALWAYS use data-testid selectors** - Use `page.getByTestId()` instead of `page.locator()` with CSS selectors |
| 43 | + |
| 44 | +### 2. Components |
| 45 | + |
| 46 | +- **Reusable UI elements** - Components that appear across multiple pages |
| 47 | +- **Self-contained logic** - Each component manages its own state and interactions |
| 48 | +- **Composable** - Components can be used within Page Objects |
| 49 | +- **ALWAYS use data-testid selectors** - Use `page.getByTestId()` for all element interactions |
| 50 | + |
| 51 | +### 3. Tests |
| 52 | + |
| 53 | +- **Use Page Objects only** - Never interact directly with the DOM in tests |
| 54 | +- **Descriptive test names** - Clear, readable test descriptions |
| 55 | +- **One test per scenario** - Each test should verify one specific behavior |
| 56 | +- **Use test data files** - Centralize test data in separate files |
| 57 | + |
| 58 | +## Selector Rules |
| 59 | + |
| 60 | +### ✅ ALWAYS Use data-testid |
| 61 | + |
| 62 | +```typescript |
| 63 | +// CORRECT - Use data-testid selectors |
| 64 | +await this.page.getByTestId("userNameInput").click(); |
| 65 | +await this.page.getByTestId("userNameInput").fill(username); |
| 66 | +await this.page.getByTestId("password").fill(password); |
| 67 | +await this.page.getByTestId("submitButton").click(); |
| 68 | +``` |
| 69 | + |
| 70 | +### ❌ NEVER Use CSS selectors |
| 71 | + |
| 72 | +```typescript |
| 73 | +// WRONG - Don't use CSS selectors |
| 74 | +await this.page.locator('input[id="userId"]').fill(username); |
| 75 | +await this.page.locator('button[id="loginButton"]').click(); |
| 76 | +await this.page.locator(".login-form input").fill(username); |
| 77 | +``` |
| 78 | + |
| 79 | +### Why data-testid? |
| 80 | + |
| 81 | +1. **More stable** - Not affected by CSS class changes or styling updates |
| 82 | +2. **More specific** - Designed specifically for testing |
| 83 | +3. **Better performance** - Playwright's `getByTestId()` is optimized |
| 84 | +4. **Clearer intent** - Makes it obvious the element is for testing |
| 85 | + |
| 86 | +## Code Examples |
| 87 | + |
| 88 | +### ✅ Correct POM Implementation |
| 89 | + |
| 90 | +```typescript |
| 91 | +// pages/login.page.ts |
| 92 | +export class LoginPage { |
| 93 | + constructor(private page: Page) {} |
| 94 | + |
| 95 | + async login(username: string, password: string): Promise<void> { |
| 96 | + const currentEnv = process.env["CURRENT_ENV"] || "dev"; |
| 97 | + const loginUrl = |
| 98 | + currentEnv === "ci" ? "/login/" : "/dotAdmin/#/public/login"; |
| 99 | + |
| 100 | + await this.page.goto(loginUrl); |
| 101 | + await this.page.waitForLoadState(); |
| 102 | + |
| 103 | + // Use data-testid selectors |
| 104 | + await this.page.getByTestId("userNameInput").click(); |
| 105 | + await this.page.getByTestId("userNameInput").fill(username); |
| 106 | + await this.page.getByTestId("userNameInput").press("Tab"); |
| 107 | + await this.page.getByTestId("password").fill(password); |
| 108 | + await this.page.getByTestId("submitButton").click(); |
| 109 | + } |
| 110 | + |
| 111 | + async isLoggedIn(): Promise<boolean> { |
| 112 | + const currentUrl = this.page.url(); |
| 113 | + return ( |
| 114 | + !currentUrl.includes("/login/") && !currentUrl.includes("/public/login") |
| 115 | + ); |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +// tests/login/login.spec.ts |
| 120 | +test("User can login with valid credentials", async ({ page }) => { |
| 121 | + const loginPage = new LoginPage(page); |
| 122 | + |
| 123 | + await loginPage.login("admin@dotcms.com", "admin"); |
| 124 | + |
| 125 | + expect(await loginPage.isLoggedIn()).toBe(true); |
| 126 | +}); |
| 127 | +``` |
| 128 | + |
| 129 | +### ❌ Incorrect Implementation |
| 130 | + |
| 131 | +```typescript |
| 132 | +// DON'T DO THIS - Direct DOM interaction in tests |
| 133 | +test("User can login", async ({ page }) => { |
| 134 | + await page.goto("/login/"); |
| 135 | + await page.fill('input[id="userId"]', "admin@dotcms.com"); |
| 136 | + await page.fill('input[id="password"]', "admin"); |
| 137 | + await page.click('button[id="loginButton"]'); |
| 138 | +}); |
| 139 | + |
| 140 | +// DON'T DO THIS - Using CSS selectors in Page Objects |
| 141 | +export class LoginPage { |
| 142 | + async login(username: string, password: string) { |
| 143 | + await this.page.locator('input[id="userId"]').fill(username); |
| 144 | + await this.page.locator('input[id="password"]').fill(password); |
| 145 | + await this.page.locator('button[id="loginButton"]').click(); |
| 146 | + } |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +## Environment Handling |
| 151 | + |
| 152 | +### Page Objects should handle environment differences: |
| 153 | + |
| 154 | +```typescript |
| 155 | +export class LoginPage { |
| 156 | + private getLoginUrl(): string { |
| 157 | + const currentEnv = process.env["CURRENT_ENV"] || "dev"; |
| 158 | + return currentEnv === "ci" ? "/login/" : "/dotAdmin/#/public/login"; |
| 159 | + } |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +## Test Data Management |
| 164 | + |
| 165 | +### Centralize test data: |
| 166 | + |
| 167 | +```typescript |
| 168 | +// tests/login/credentialsData.ts |
| 169 | +export const validCredentials = [ |
| 170 | + { username: "admin@dotcms.com", password: "admin" }, |
| 171 | + { username: "test@dotcms.com", password: "test" }, |
| 172 | +]; |
| 173 | + |
| 174 | +export const invalidCredentials = [ |
| 175 | + { username: "wrong@dotcms.com", password: "wrong" }, |
| 176 | +]; |
| 177 | +``` |
| 178 | + |
| 179 | +## Naming Conventions |
| 180 | + |
| 181 | +- **Page Objects**: `[PageName].page.ts` (e.g., `login.page.ts`) |
| 182 | +- **Components**: `[ComponentName].component.ts` (e.g., `sideMenu.component.ts`) |
| 183 | +- **Test Files**: `[FeatureName].spec.ts` (e.g., `login.spec.ts`) |
| 184 | +- **Test Data**: `[FeatureName]Data.ts` (e.g., `credentialsData.ts`) |
| 185 | + |
| 186 | +## Best Practices |
| 187 | + |
| 188 | +1. **Keep Page Objects focused** - Each Page Object should handle one page only |
| 189 | +2. **Use meaningful method names** - `login()`, `navigateToContent()`, `verifyUserIsLoggedIn()` |
| 190 | +3. **Handle waits properly** - Use appropriate waits in Page Objects |
| 191 | +4. **Return useful data** - Methods should return information that tests need |
| 192 | +5. **Keep tests simple** - Tests should be easy to read and understand |
| 193 | +6. **Use TypeScript** - Leverage type safety for better maintainability |
| 194 | +7. **ALWAYS use data-testid** - Never use CSS selectors, always use `page.getByTestId()` |
| 195 | + |
| 196 | +## Migration Guidelines |
| 197 | + |
| 198 | +When migrating tests from Maven E2E: |
| 199 | + |
| 200 | +1. **Identify pages** - Create Page Objects for each page |
| 201 | +2. **Extract components** - Identify reusable UI components |
| 202 | +3. **Centralize data** - Move test data to dedicated files |
| 203 | +4. **Update tests** - Refactor tests to use Page Objects |
| 204 | +5. **Handle environments** - Ensure Page Objects work in both dev and ci modes |
| 205 | +6. **Convert selectors** - Replace all CSS selectors with data-testid selectors |
| 206 | + |
| 207 | +## Using Playwright Codegen |
| 208 | + |
| 209 | +When using Playwright's codegen to generate tests: |
| 210 | + |
| 211 | +1. **Run codegen**: `npx playwright codegen http://localhost:8080/dotAdmin/#/public/login` |
| 212 | +2. **Copy the generated selectors** - Use the `getByTestId()` calls from codegen |
| 213 | +3. **Update Page Objects** - Replace old selectors with the new data-testid selectors |
| 214 | +4. **Test the changes** - Verify the new selectors work correctly |
| 215 | + |
| 216 | +--- |
| 217 | + |
| 218 | +**Remember**: |
| 219 | + |
| 220 | +- Always use Page Objects for E2E tests |
| 221 | +- Never interact directly with the DOM in test files |
| 222 | +- ALWAYS use `data-testid` selectors with `page.getByTestId()` |
| 223 | +- Never use CSS selectors like `page.locator('input[id="..."]')` |
0 commit comments