Skip to content

Commit d0b89fa

Browse files
oidacranicobytes
andauthored
feat(dotcms-ui-e2e): Make run E2E inside Nx (#33266)
- Updated Jest configuration to include coverage reporters and JUnit output. - Introduced Playwright configuration for different environments (dev, ci, local) with headless options. - Added new components and locators for better test organization. - Implemented login functionality and various test cases for login scenarios, including valid and invalid credentials. - Created environment configuration for managing base URLs and server reuse. - Added utility functions for waiting on locators and generating base64 credentials. This update improves the structure and reliability of end-to-end tests for the dotCMS UI. #32525 --------- Co-authored-by: Nicolas Molina Monroy <hi@nicobytes.com>
1 parent 7e2d0c8 commit d0b89fa

56 files changed

Lines changed: 2112 additions & 275 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/rules/e2e-rules.mdc

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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="..."]')`

.github/test-matrix.yml

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,6 @@ test_types:
146146
needs_node: true
147147
base_maven_args: "verify -De2e.test.skip=false"
148148
suites:
149-
- name: "E2E Tests - JVM Suite"
150-
maven_args: >-
151-
-Dci=true -Dit.test=E2eTestSuite
152-
-De2e.test.forkCount=1 -pl :dotcms-e2e-java
153-
stage_name: "E2E JVM E2E Suite"
154-
- name: "E2E Tests - Node Suite"
155-
maven_args: "-De2e.test.env=ci -pl :dotcms-e2e-node"
156-
stage_name: "E2E Node E2E Suite"
149+
- name: "E2E Tests - Nx Playwright"
150+
maven_args: "-De2e.test.env=ci -pl :dotcms-ui-e2e"
151+
stage_name: "E2E Nx Playwright Suite"

.github/workflows/cicd_1-pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ jobs:
8383
karate: ${{ fromJSON(needs.initialize.outputs.filters).backend == 'true' }}
8484
frontend: ${{ fromJSON(needs.initialize.outputs.filters).frontend == 'true' }}
8585
cli: ${{ fromJSON(needs.initialize.outputs.filters).cli == 'true' || fromJSON(needs.initialize.outputs.filters).backend == 'true' }}
86-
e2e: false
86+
e2e: ${{ fromJSON(needs.initialize.outputs.filters).build == 'true' }}
8787
secrets:
8888
DOTCMS_LICENSE: ${{ secrets.DOTCMS_LICENSE }}
8989

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ dotsecure
142142
/core-web/out*/
143143
dist-docs/
144144
**/test-reports/
145+
/core-web/apps/dotcms-ui-e2e/playwright-report
146+
/core-web/apps/dotcms-ui-e2e/test-results
147+
/core-web/apps/dotcms-ui-e2e/test-results/junit.xml
148+
/core-web/apps/dotcms-ui-e2e/target
145149

146150
#env
147151
/core-web/**/.env

core-web/.cursor/mcp.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
{
22
"mcpServers": {
33
"primeng": {
4-
"type": "stdio",
54
"command": "npx",
65
"args": ["-y", "@primeng/mcp"]
6+
},
7+
"nx-mcp": {
8+
"type": "stdio",
9+
"command": "npx",
10+
"args": ["nx", "mcp"]
711
}
812
}
913
}

core-web/apps/dotcdn/jest.config.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ export default {
44
preset: '../../jest.preset.js',
55
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
66
globals: {},
7-
coverageReporters: [['lcovonly', { file: 'TEST-dotcdn.lcov' }]],
87
reporters: [
9-
'default',
10-
['github-actions', { silent: false }],
118
[
129
'jest-junit',
1310
{
Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,10 @@
11
{
22
"extends": ["plugin:playwright/recommended", "../../.eslintrc.base.json"],
3-
"ignorePatterns": ["!**/*"],
3+
"ignorePatterns": ["playwright-report/**", "test-results/**", "target/**"],
44
"overrides": [
5-
{
6-
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7-
"rules": {}
8-
},
9-
{
10-
"files": ["*.ts", "*.tsx"],
11-
"rules": {}
12-
},
13-
{
14-
"files": ["*.js", "*.jsx"],
15-
"rules": {}
16-
},
17-
{
18-
"files": ["src/**/*.{ts,js,tsx,jsx}"],
19-
"rules": {}
20-
}
5+
{ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "rules": {} },
6+
{ "files": ["*.ts", "*.tsx"], "rules": {} },
7+
{ "files": ["*.js", "*.jsx"], "rules": {} },
8+
{ "files": ["src/**/*.{ts,js,tsx,jsx}"], "rules": {} }
219
]
2210
}

0 commit comments

Comments
 (0)