e2e-rules.mdc in dotCMS/core runs 888 words across 22 headings.
The Visual Headless Content Management System for Enterprises
Covers
9 of the 20 section tags
In the order a file is read inHeadings
22 headings, in the order the file writes them
01Page Object Model (POM) Conventions for dotCMS E2E Tests
02Overview
03Directory Structure
04POM Rules
051. Page Objects
062. Components
073. Tests
08Selector Rules
09✅ ALWAYS Use data-testid
10❌ NEVER Use CSS selectors
11Why data-testid?
12Code Examples
13✅ Correct POM Implementation
14❌ Incorrect Implementation
15Environment Handling
16Page Objects should handle environment differences:
17Test Data Management
18Centralize test data:
19Naming Conventions
20Best Practices
21Migration Guidelines
22Using Playwright Codegen
Commands
1 commands this file writes down
Extracted from the file, verbatimnpx playwright codegen http://localhost:8080/dotAdmin/#/public/login
The file
.cursor/rules/e2e-rules.mdc
First 160 of 224 lines1---
2globs: core-web/apps/dotcms-ui-e2e/**/*.spec.ts
3alwaysApply: false
4---
5
6# Page Object Model (POM) Conventions for dotCMS E2E Tests
7
8## Overview
9
10This 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```
15src/
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
64await this.page.getByTestId("userNameInput").click();
65await this.page.getByTestId("userNameInput").fill(username);
66await this.page.getByTestId("password").fill(password);
67await this.page.getByTestId("submitButton").click();
68```
69
70### ❌ NEVER Use CSS selectors
71
72```typescript
73// WRONG - Don't use CSS selectors
74await this.page.locator('input[id="userId"]').fill(username);
75await this.page.locator('button[id="loginButton"]').click();
76await this.page.locator(".login-form input").fill(username);
77```
78
79### Why data-testid?
80
811. **More stable** - Not affected by CSS class changes or styling updates
822. **More specific** - Designed specifically for testing
833. **Better performance** - Playwright's `getByTestId()` is optimized
844. **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
92export 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
120test("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
133test("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
141export 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
155export class LoginPage {
156 private getLoginUrl(): string {
157 const currentEnv = process.env["CURRENT_ENV"] || "dev";
158 return currentEnv === "ci" ? "/login/" : "/dotAdmin/#/public/login";
159 }
160}
64 more lines are in the file. Read the raw file.
The rest of the repository
dotCMS/core ships 20 other instruction files
.cursor/rules/java-context.mdccore-web/libs/sdk/client/CLAUDE.md.github/instructions/frontend.instructions.mdcore-web/CLAUDE.mdcore-web/apps/dotcms-ui/AGENTS.mdcore-web/apps/mcp-server/CLAUDE.mddotCMS/src/main/java/com/dotcms/rest/CLAUDE.mdtest-jmeter/CLAUDE.mdcore-web/apps/dotcms-ui-e2e/AGENTS.md.cursor/rules/doc-updates.mdc.cursor/rules/frontend-context.mdccore-web/libs/sdk/react/CLAUDE.mdcore-web/AGENTS.md.github/copilot-instructions.md.cursor/rules/dotcms-guide.mdc.cursor/rules/test-context.mdcCLAUDE.mdcore-web/libs/block-editor/CLAUDE.mdcore-web/libs/new-block-editor/CLAUDE.mdcore-web/libs/portlets/CLAUDE.md
A row that is not a link is a file this repository ships that this app did not freeze a sheet for. It is listed because the corpus knows it exists, and it is not linked because there is nothing here to open.
This listing
Whoever runs dotCMS/core can claim it
This is yours? Claim this config and we will write to you when the measurement moves. The check is one token placed where only you can place it, and there is no account and no password.