| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 1 | 2 | 43 | 2% |
| Commands | 0 | 0 | 0 | — |
| Section tags | 1 | 2 | 6 | 11% |
What each file covers
Sections
1 shared · 2 only in A · 43 only in B- − Services
- − CI/CD - build pipleline
- + Testing Guidelines for NOWCRM
- + Overview
- + Test Structure
- + Directory Organization
- + File Naming Conventions
- + Page Object Model (POM) Pattern
- + Structure
- + Locator Best Practices
- + Action Methods
- + Assertion Methods
- + Test File Structure
- + Basic Template
- + Test Organization
- + Test Data Management
- + Using Faker for Test Data
- + Unique Identifiers
- + Test Credentials
- + Authentication and Setup
- + Global Setup
- + Login Helper
- + Test Execution Patterns
- + Waiting Strategies
- + Error Handling
- + Test Isolation
- + Assertions
- + Best Practices
- + Helper Functions
- + Reusable Test Helpers
- + External Service Helpers
- + Test Configuration
- + Playwright Config
- + Environment Variables
- + Test Maintenance
- + Handling Flaky Tests
- + Skipping Tests
- + Comments
- + Naming Conventions
- + Common Patterns
- + Row Operations
- + Modal/Dialog Operations
- + Mass Actions
- + Best Practices Summary
- + Anti-Patterns to Avoid
- Code Style
Commands
neither file has anySection tags
1 shared · 2 only in A · 6 only in B- − build
- − deployment
- + setup
- + test
- + architecture
- + security
- + do-not
- + docs
- code-style
Line diff
nowtec/nowCRM · CLAUDE.md
@@ −1 @@
1# Code Style
2Read coding guidelines completed
3.cursor/rules
4
5
6# Services
7See all the services under **apps** directory
8
9
10# CI/CD - build pipleline
11.github/workflows/main.yaml - creates release, builds services, pushed to Github registry, sends notification to Telegram
nowtec/nowCRM · .cursor/rules/testing-guidelines.mdc
@@ +1 @@
1---
2description: Testing guidelines for NOWCRM
3globs: ["**/*.spec.ts", "**/*.test.ts", "**/tests/**", "**/playwright.config.ts"]
4alwaysApply: false
5---
6
7# Testing Guidelines for NOWCRM
8
9## Overview
10
11NOWCRM uses **Playwright** for end-to-end (E2E) testing. All tests follow the **Page Object Model (POM)** pattern for maintainability and reusability.
12
13## Test Structure
14
15### Directory Organization
16
17```
18apps/nowcrm/tests/
19├── *.spec.ts # Test specification files (numbered for execution order)
20├── pages/ # Page Object Models (POMs)
21│ ├── CommonPage.ts
22│ ├── ContactsListPage.ts
23│ └── ...
24├── utils/ # Test utilities and helpers
25│ ├── authHelper.ts
26│ └── data.ts
27├── setup/ # Setup and teardown scripts
28│ ├── global-setup.ts
29│ ├── create-users.ts
30│ └── delete-users.ts
31└── files/ # Test fixtures and data files
32```
33
34### File Naming Conventions
35
36- **Test files**: Use numbered prefixes for execution order (e.g., `01Authentication.spec.ts`, `02Contacts.spec.ts`)
37- **Page Objects**: Use descriptive names ending with `Page` or `Modal` (e.g., `ContactsListPage.ts`, `ContactCreateModal.ts`)
38- **Utilities**: Use descriptive names (e.g., `authHelper.ts`, `data.ts`)
39
40## Page Object Model (POM) Pattern
41
42### Structure
43
44Every Page Object should follow this structure:
45
46```typescript
47import { type Locator, type Page, expect } from '@playwright/test';
48
49export class PageName {
50 readonly page: Page;
51
52 // Locators - declare as readonly
53 readonly elementName: Locator;
54
55 constructor(page: Page) {
56 this.page = page;
57 // Initialize locators
58 this.elementName = page.getByRole('button', { name: 'Button Name' });
59 }
60
61 // Actions - async methods that perform interactions
62 async performAction() {
63 await expect(this.elementName).toBeVisible();
64 await this.elementName.click();
65 }
66
67 // Assertions - async methods that verify state
68 async expectSomethingVisible(timeout: number = 5000) {
69 await expect(this.elementName, 'Descriptive message').toBeVisible({ timeout });
70 }
71}
72```
73
74### Locator Best Practices
75
761. **Prefer role-based selectors**:
77```typescript
78// ✅ Good - accessible and stable
79this.createButton = page.getByRole('button', { name: 'Create' });
80this.emailInput = page.getByRole('textbox', { name: 'Email' });
81
82// ❌ Avoid - fragile CSS selectors
83this.createButton = page.locator('.btn-primary');
84```
85
862. **Scope locators within dialogs/modals**:
87```typescript
88constructor(page: Page) {
89 this.dialog = page.getByRole('dialog', { name: /Create Contact/i });
90 // Scope inputs within dialog
91 this.firstNameInput = this.dialog.getByRole('textbox', { name: 'First name' });
92}
93```
94
953. **Use descriptive locator names**:
96```typescript
97// ✅ Good
98readonly userMenuTrigger: Locator;
99readonly deleteMassActionMenuItem: Locator;
100
101// ❌ Avoid
102readonly btn1: Locator;
103readonly menuItem: Locator;
104```
105
106### Action Methods
107
108- **Naming**: Use verb phrases (e.g., `clickCreateButton`, `fillAndSubmit`, `openUserMenu`)
109- **Wait for visibility**: Always wait for elements before interacting
110- **Return values**: Return relevant data when needed (e.g., created entity ID)
111
112```typescript
113async clickCreateButton() {
114 await expect(this.createButton, 'Create button should be visible').toBeVisible({ timeout: 20000 });
115 await this.createButton.click();
116}
117
118async fillAndSubmit(data: ContactData) {
119 await this.firstNameInput.fill(data.firstName);
120 await this.lastNameInput.fill(data.lastName);
121 await this.emailInput.fill(data.email);
122 await this.createButton.click();
123}
124```
125
126### Assertion Methods
127
128- **Naming**: Prefix with `expect` (e.g., `expectDashboardVisible`, `expectStatusMessage`)
129- **Descriptive messages**: Always include meaningful error messages
130- **Configurable timeouts**: Accept timeout parameters with sensible defaults
131
132```typescript
133async expectStatusMessage(message: string, timeout: number = 20000) {
134 const messageLocator = this.page.getByText(message, { exact: true });
135 await expect(messageLocator, `Status message "${message}" should be visible`)
136 .toBeVisible({ timeout });
137}
138
139async expectDashboardVisible(timeout: number = 10000) {
140 await expect(this.page, 'URL should indicate CRM dashboard')
141 .toHaveURL(/\/crm$/, { timeout });
142}
143```
144
145## Test File Structure
146
147### Basic Template
148
149```typescript
150import { test, expect } from '@playwright/test';
151import { faker } from '@faker-js/faker';
152
153// Import Page Object Models
154import { ContactsListPage } from './pages/ContactsListPage';
155import { ContactCreateModal } from './pages/ContactCreateModal';
156
157// Import utilities
158import { loginUser } from './utils/authHelper';
159
160test.describe('Feature Name', () => {
161 let pageObject1: ContactsListPage;
162 let pageObject2: ContactCreateModal;
163
164 test.beforeEach(async ({ page }) => {
165 // Initialize POMs
166 pageObject1 = new ContactsListPage(page);
167 pageObject2 = new ContactCreateModal(page);
168
169 // Common setup (e.g., login)
170 await loginUser(page);
171 await pageObject1.goto();
172 });
173
174 test('User can perform action', async () => {
175 // Arrange - set up test data
176 const testData = {
177 firstName: faker.person.firstName(),
178 email: faker.internet.email()
179 };
180
181 // Act - perform actions
182 await pageObject1.clickCreateButton();
183 await pageObject2.fillAndSubmit(testData);
184
185 // Assert - verify results
186 await pageObject2.expectCreationStatusMessage(testData.firstName);
187 await expect(pageObject1.getRowLocator(testData.email))
188 .toBeVisible({ timeout: 10000 });
189 });
190});
191```
192
193### Test Organization
194
1951. **Use `test.describe` blocks** to group related tests
1962. **Initialize POMs in `beforeEach`** for consistency
1973. **Number test files** for execution order (e.g., `01Authentication.spec.ts`)
1984. **One feature per describe block** (e.g., 'Contact Management', 'Authentication Flow')
199
200## Test Data Management
201
202### Using Faker for Test Data
203
204```typescript
205import { faker } from '@faker-js/faker';
206
207// Generate unique test data
208const contact = {
209 firstName: faker.person.firstName(),
210 lastName: faker.person.lastName(),
211 email: faker.internet.email({ provider: `test.${faker.string.alphanumeric(5)}.pw` }),
212 address: faker.location.streetAddress(),
213};
214```
215
216### Unique Identifiers
217
218- **Use timestamps or random strings** to ensure uniqueness:
219```typescript
220const uniqueEmail = `testuser+${Date.now()}@example.com`;
221const uniqueListName = `List_${faker.string.alphanumeric(6)}`;
222```
223
224### Test Credentials
225
226- **Store in environment variables** via `utils/data.ts`:
227```typescript
228export const testCredentials = {
229 email: process.env.TEST_USER_EMAIL || 'testuser@example.com',
230 password: process.env.TEST_USER_PASSWORD || 'StrongPassword123!',
231};
232```
233
234## Authentication and Setup
235
236### Global Setup
237
238- **Use `global-setup.ts`** for authentication state management
239- **Save storage state** to avoid repeated logins:
240```typescript
241await page.context().storageState({ path: STORAGE_STATE_PATH });
242```
243
244### Login Helper
245
246- **Create reusable login function** in `utils/authHelper.ts`:
247```typescript
248export async function loginUser(
249 page: Page,
250 postLoginUrlRegex: RegExp = /\/crm$/
251): Promise<void> {
252 await page.goto('/en/auth');
253 await page.getByRole('textbox', { name: 'Email' }).fill(testCredentials.email);
254 await page.getByRole('textbox', { name: 'Password' }).fill(testCredentials.password);
255 await page.getByRole('button', { name: 'Sign in' }).click();
256 await expect(page).toHaveURL(postLoginUrlRegex, { timeout: 15000 });
257}
258```
259
260## Test Execution Patterns
261
262### Waiting Strategies
263
2641. **Use Playwright's auto-waiting**:
265```typescript
266// ✅ Good - Playwright waits automatically
267await button.click();
268
269// ❌ Avoid - unnecessary manual waits
270await page.waitForTimeout(1000);
271await button.click();
272```
273
2742. **Use explicit waits for async operations**:
275```typescript
276// ✅ Good - wait for specific condition
277await expect(element).toBeVisible({ timeout: 10000 });
278
279// ✅ Good - wait for URL change
280await expect(page).toHaveURL(/\/contacts\/\d+\/details/);
281```
282
2833. **Use `waitForTimeout` sparingly** (only when necessary):
284```typescript
285// Only when waiting for async operations that can't be detected
286await page.waitForTimeout(300); // Wait for dropdown to render
287```
288
289### Error Handling
290
291- **Use try/finally blocks** for cleanup:
292```typescript
293test('User can perform action', async ({ page, request }) => {
294 const uniqueEmail = `test+${Date.now()}@example.com`;
295
296 try {
297 // Test logic
298 await createTestUser(request, { email: uniqueEmail });
299 // ... test steps ...
300 } finally {
301 // Cleanup
302 await deleteUserFromStrapi(request, uniqueEmail);
303 await request.delete('http://localhost:8025/api/v1/messages');
304 }
305});
306```
307
308### Test Isolation
309
310- **Each test should be independent** - don't rely on test execution order
311- **Clean up test data** after each test
312- **Use unique identifiers** to avoid conflicts
313
314## Assertions
315
316### Best Practices
317
3181. **Always include descriptive messages**:
319```typescript
320// ✅ Good
321await expect(contactRow, 'Contact row should contain correct email')
322 .toContainText(contact.email);
323
324// ❌ Avoid
325await expect(contactRow).toContainText(contact.email);
326```
327
3282. **Use appropriate matchers**:
329```typescript
330await expect(element).toBeVisible({ timeout: 10000 });
331await expect(element).toHaveText('Expected Text');
332await expect(element).toContainText('Partial Text');
333await expect(page).toHaveURL(/\/crm$/);
334await expect(locator).toHaveCount(1);
335```
336
3373. **Set reasonable timeouts**:
338```typescript
339// Default timeout: 5000ms
340await expect(element).toBeVisible();
341
342// Custom timeout for slow operations
343await expect(element).toBeVisible({ timeout: 20000 });
344```
345
346## Helper Functions
347
348### Reusable Test Helpers
349
350Create helper functions for common operations:
351
352```typescript
353// In test file or utils
354async function createContactViaUI(data: ContactData) {
355 await contactsListPage.clickCreateButton();
356 await contactCreateModal.waitForDialogVisible();
357 await contactCreateModal.fillAndSubmit(data);
358 await contactCreateModal.expectCreationStatusMessage(data.firstName);
359 await contactsListPage.goto();
360 await expect(contactsListPage.getRowLocator(data.email))
361 .toBeVisible({ timeout: 10000 });
362}
363```
364
365### External Service Helpers
366
367For services like Mailpit, create helper classes:
368
369```typescript
370export class MailpitHelper {
371 readonly request: APIRequestContext;
372
373 constructor(request: APIRequestContext) {
374 this.request = request;
375 }
376
377 async waitForEmails(recipient: string, subject: string, expectedCount = 2) {
378 // Implementation
379 }
380}
381```
382
383## Test Configuration
384
385### Playwright Config
386
387Key configuration patterns:
388
389```typescript
390export default defineConfig({
391 testDir: './tests',
392 timeout: TIMEOUT, // Default: 30000
393 globalSetup: require.resolve('./tests/setup/global-setup'),
394 expect: {
395 timeout: EXPECT_TIMEOUT, // Default: 5000
396 },
397 fullyParallel: false, // Set to false for sequential execution
398 retries: CI ? 1 : 0,
399 workers: CI ? 1 : WORKERS,
400 use: {
401 baseURL: CRM_BASE_URL,
402 trace: 'on-first-retry',
403 screenshot: 'only-on-failure',
404 video: 'on-first-retry',
405 },
406});
407```
408
409### Environment Variables
410
411Required environment variables:
412
413- `CRM_BASE_URL` - Base URL for the application
414- `TEST_USER_EMAIL` - Test user email
415- `TEST_USER_PASSWORD` - Test user password
416- `STRAPI_TEST_ADMIN_EMAIL` - Strapi admin email
417- `STRAPI_TEST_ADMIN_PASSWORD` - Strapi admin password
418- `PLAYWRIGHT_WORKERS` - Number of workers (optional)
419- `PLAYWRIGHT_RETRIES` - Number of retries (optional)
420- `PLAYWRIGHT_TIMEOUT` - Test timeout (optional)
421
422## Test Maintenance
423
424### Handling Flaky Tests
425
4261. **Increase timeouts** for slow operations
4272. **Add explicit waits** for async operations
4283. **Use more stable locators** (role-based over CSS)
4294. **Retry logic** for known flaky operations:
430```typescript
431let langSelected = false;
432for (let i = 0; i < 3; i++) {
433 try {
434 await langOption.click();
435 langSelected = true;
436 break;
437 } catch (err) {
438 if (i === 2) throw err;
439 await this.page.waitForTimeout(100);
440 }
441}
442```
443
444### Skipping Tests
445
446- **Use `test.skip()`** for temporarily disabled tests:
447```typescript
448test.skip('should allow creating a journey with drag-and-drop', async () => {
449 // Test implementation
450});
451```
452
453- **Use `test.fail()`** for tests that are expected to fail (document why):
454```typescript
455// This test is marked as expected to fail due to a known application bug.
456test.fail('User can edit a list name (expected failure due to edit bug)', async () => {
457 // Test implementation
458});
459```
460
461## Code Style
462
463### Comments
464
465- **Add comments** explaining complex test logic
466- **Document test steps** in multi-step tests:
467```typescript
468// Step 1: Navigate to the login page
469await loginPage.goto();
470
471// Step 2: Fill in credentials
472await loginPage.fillCredentials(email, password);
473
474// Step 3: Submit and verify
475await loginPage.clickSignIn();
476await commonPage.expectDashboardVisible();
477```
478
479### Naming Conventions
480
481- **Test descriptions**: Use "User can..." or "should..." format
482- **Helper functions**: Use descriptive verb phrases
483- **Variables**: Use camelCase with descriptive names
484
485## Common Patterns
486
487### Row Operations
488
489```typescript
490// Get row locator
491const row = contactsListPage.getRowLocator(uniqueEmail);
492
493// Get row-specific elements
494const checkbox = contactsListPage.getCheckboxForRow(row);
495const link = contactsListPage.getLinkForRow(row, firstName);
496const deleteButton = contactsListPage.getDeleteButtonForRow(row);
497
498// Interact with row
499await checkbox.check();
500await link.click();
501await deleteButton.click();
502```
503
504### Modal/Dialog Operations
505
506```typescript
507// Wait for modal
508await modal.waitForDialogVisible();
509
510// Fill form
511await modal.fillAndSubmit(data);
512
513// Verify success
514await modal.expectCreationStatusMessage(data.name);
515```
516
517### Mass Actions
518
519```typescript
520// Select items
521await contactsListPage.getCheckboxForRow(row).check();
522
523// Open mass actions menu
524await contactsListPage.openMassActionsMenu();
525
526// Perform action
527await contactsListPage.clickDeleteMassAction();
528await contactsListPage.clickDeleteConfirmMassAction();
529```
530
531## Best Practices Summary
532
5331. ✅ **Use Page Object Model** for all page interactions
5342. ✅ **Prefer role-based locators** over CSS selectors
5353. ✅ **Use descriptive test names** and assertion messages
5364. ✅ **Generate unique test data** using Faker
5375. ✅ **Clean up test data** in finally blocks
5386. ✅ **Use helper functions** for common operations
5397. ✅ **Set appropriate timeouts** for async operations
5408. ✅ **Keep tests independent** and isolated
5419. ✅ **Document complex test logic** with comments
54210. ✅ **Handle errors gracefully** with try/finally blocks
543
544## Anti-Patterns to Avoid
545
5461. ❌ **Hardcoded test data** - Use Faker or environment variables
5472. ❌ **CSS selectors** - Prefer role-based or accessible selectors
5483. ❌ **Unnecessary waits** - Use Playwright's auto-waiting
5494. ❌ **Test dependencies** - Keep tests independent
5505. ❌ **Missing cleanup** - Always clean up test data
5516. ❌ **Vague assertions** - Include descriptive error messages
5527. ❌ **Duplicate code** - Extract to helper functions or POMs
5538. ❌ **Fragile locators** - Use stable, accessible selectors
554
@@ −1 +1 @@
1−# Code Style
2−Read coding guidelines completed
3−.cursor/rules
1+---
2+description: Testing guidelines for NOWCRM
3+globs: ["**/*.spec.ts", "**/*.test.ts", "**/tests/**", "**/playwright.config.ts"]
4+alwaysApply: false
5+---
46
7+# Testing Guidelines for NOWCRM
58
6−# Services
7−See all the services under **apps** directory
9+## Overview
810
11+NOWCRM uses **Playwright** for end-to-end (E2E) testing. All tests follow the **Page Object Model (POM)** pattern for maintainability and reusability.
912
10−# CI/CD - build pipleline
11−.github/workflows/main.yaml - creates release, builds services, pushed to Github registry, sends notification to Telegram
13+## Test Structure
14+
15+### Directory Organization
16+
17+```
18+apps/nowcrm/tests/
19+├── *.spec.ts # Test specification files (numbered for execution order)
20+├── pages/ # Page Object Models (POMs)
21+│ ├── CommonPage.ts
22+│ ├── ContactsListPage.ts
23+│ └── ...
24+├── utils/ # Test utilities and helpers
25+│ ├── authHelper.ts
26+│ └── data.ts
27+├── setup/ # Setup and teardown scripts
28+│ ├── global-setup.ts
29+│ ├── create-users.ts
30+│ └── delete-users.ts
31+└── files/ # Test fixtures and data files
32+```
33+
34+### File Naming Conventions
35+
36+- **Test files**: Use numbered prefixes for execution order (e.g., `01Authentication.spec.ts`, `02Contacts.spec.ts`)
37+- **Page Objects**: Use descriptive names ending with `Page` or `Modal` (e.g., `ContactsListPage.ts`, `ContactCreateModal.ts`)
38+- **Utilities**: Use descriptive names (e.g., `authHelper.ts`, `data.ts`)
39+
40+## Page Object Model (POM) Pattern
41+
42+### Structure
43+
44+Every Page Object should follow this structure:
45+
46+```typescript
47+import { type Locator, type Page, expect } from '@playwright/test';
48+
49+export class PageName {
50+ readonly page: Page;
51+
52+ // Locators - declare as readonly
53+ readonly elementName: Locator;
54+
55+ constructor(page: Page) {
56+ this.page = page;
57+ // Initialize locators
58+ this.elementName = page.getByRole('button', { name: 'Button Name' });
59+ }
60+
61+ // Actions - async methods that perform interactions
62+ async performAction() {
63+ await expect(this.elementName).toBeVisible();
64+ await this.elementName.click();
65+ }
66+
67+ // Assertions - async methods that verify state
68+ async expectSomethingVisible(timeout: number = 5000) {
69+ await expect(this.elementName, 'Descriptive message').toBeVisible({ timeout });
70+ }
71+}
72+```
73+
74+### Locator Best Practices
75+
76+1. **Prefer role-based selectors**:
77+```typescript
78+// ✅ Good - accessible and stable
79+this.createButton = page.getByRole('button', { name: 'Create' });
80+this.emailInput = page.getByRole('textbox', { name: 'Email' });
81+
82+// ❌ Avoid - fragile CSS selectors
83+this.createButton = page.locator('.btn-primary');
84+```
85+
86+2. **Scope locators within dialogs/modals**:
87+```typescript
88+constructor(page: Page) {
89+ this.dialog = page.getByRole('dialog', { name: /Create Contact/i });
90+ // Scope inputs within dialog
91+ this.firstNameInput = this.dialog.getByRole('textbox', { name: 'First name' });
92+}
93+```
94+
95+3. **Use descriptive locator names**:
96+```typescript
97+// ✅ Good
98+readonly userMenuTrigger: Locator;
99+readonly deleteMassActionMenuItem: Locator;
100+
101+// ❌ Avoid
102+readonly btn1: Locator;
103+readonly menuItem: Locator;
104+```
105+
106+### Action Methods
107+
108+- **Naming**: Use verb phrases (e.g., `clickCreateButton`, `fillAndSubmit`, `openUserMenu`)
109+- **Wait for visibility**: Always wait for elements before interacting
110+- **Return values**: Return relevant data when needed (e.g., created entity ID)
111+
112+```typescript
113+async clickCreateButton() {
114+ await expect(this.createButton, 'Create button should be visible').toBeVisible({ timeout: 20000 });
115+ await this.createButton.click();
116+}
117+
118+async fillAndSubmit(data: ContactData) {
119+ await this.firstNameInput.fill(data.firstName);
120+ await this.lastNameInput.fill(data.lastName);
121+ await this.emailInput.fill(data.email);
122+ await this.createButton.click();
123+}
124+```
125+
126+### Assertion Methods
127+
128+- **Naming**: Prefix with `expect` (e.g., `expectDashboardVisible`, `expectStatusMessage`)
129+- **Descriptive messages**: Always include meaningful error messages
130+- **Configurable timeouts**: Accept timeout parameters with sensible defaults
131+
132+```typescript
133+async expectStatusMessage(message: string, timeout: number = 20000) {
134+ const messageLocator = this.page.getByText(message, { exact: true });
135+ await expect(messageLocator, `Status message "${message}" should be visible`)
136+ .toBeVisible({ timeout });
137+}
138+
139+async expectDashboardVisible(timeout: number = 10000) {
140+ await expect(this.page, 'URL should indicate CRM dashboard')
141+ .toHaveURL(/\/crm$/, { timeout });
142+}
143+```
144+
145+## Test File Structure
146+
147+### Basic Template
148+
149+```typescript
150+import { test, expect } from '@playwright/test';
151+import { faker } from '@faker-js/faker';
152+
153+// Import Page Object Models
154+import { ContactsListPage } from './pages/ContactsListPage';
155+import { ContactCreateModal } from './pages/ContactCreateModal';
156+
157+// Import utilities
158+import { loginUser } from './utils/authHelper';
159+
160+test.describe('Feature Name', () => {
161+ let pageObject1: ContactsListPage;
162+ let pageObject2: ContactCreateModal;
163+
164+ test.beforeEach(async ({ page }) => {
165+ // Initialize POMs
166+ pageObject1 = new ContactsListPage(page);
167+ pageObject2 = new ContactCreateModal(page);
168+
169+ // Common setup (e.g., login)
170+ await loginUser(page);
171+ await pageObject1.goto();
172+ });
173+
174+ test('User can perform action', async () => {
175+ // Arrange - set up test data
176+ const testData = {
177+ firstName: faker.person.firstName(),
178+ email: faker.internet.email()
179+ };
180+
181+ // Act - perform actions
182+ await pageObject1.clickCreateButton();
183+ await pageObject2.fillAndSubmit(testData);
184+
185+ // Assert - verify results
186+ await pageObject2.expectCreationStatusMessage(testData.firstName);
187+ await expect(pageObject1.getRowLocator(testData.email))
188+ .toBeVisible({ timeout: 10000 });
189+ });
190+});
191+```
192+
193+### Test Organization
194+
195+1. **Use `test.describe` blocks** to group related tests
196+2. **Initialize POMs in `beforeEach`** for consistency
197+3. **Number test files** for execution order (e.g., `01Authentication.spec.ts`)
198+4. **One feature per describe block** (e.g., 'Contact Management', 'Authentication Flow')
199+
200+## Test Data Management
201+
202+### Using Faker for Test Data
203+
204+```typescript
205+import { faker } from '@faker-js/faker';
206+
207+// Generate unique test data
208+const contact = {
209+ firstName: faker.person.firstName(),
210+ lastName: faker.person.lastName(),
211+ email: faker.internet.email({ provider: `test.${faker.string.alphanumeric(5)}.pw` }),
212+ address: faker.location.streetAddress(),
213+};
214+```
215+
216+### Unique Identifiers
217+
218+- **Use timestamps or random strings** to ensure uniqueness:
219+```typescript
220+const uniqueEmail = `testuser+${Date.now()}@example.com`;
221+const uniqueListName = `List_${faker.string.alphanumeric(6)}`;
222+```
223+
224+### Test Credentials
225+
226+- **Store in environment variables** via `utils/data.ts`:
227+```typescript
228+export const testCredentials = {
229+ email: process.env.TEST_USER_EMAIL || 'testuser@example.com',
230+ password: process.env.TEST_USER_PASSWORD || 'StrongPassword123!',
231+};
232+```
233+
234+## Authentication and Setup
235+
236+### Global Setup
237+
238+- **Use `global-setup.ts`** for authentication state management
239+- **Save storage state** to avoid repeated logins:
240+```typescript
241+await page.context().storageState({ path: STORAGE_STATE_PATH });
242+```
243+
244+### Login Helper
245+
246+- **Create reusable login function** in `utils/authHelper.ts`:
247+```typescript
248+export async function loginUser(
249+ page: Page,
250+ postLoginUrlRegex: RegExp = /\/crm$/
251+): Promise<void> {
252+ await page.goto('/en/auth');
253+ await page.getByRole('textbox', { name: 'Email' }).fill(testCredentials.email);
254+ await page.getByRole('textbox', { name: 'Password' }).fill(testCredentials.password);
255+ await page.getByRole('button', { name: 'Sign in' }).click();
256+ await expect(page).toHaveURL(postLoginUrlRegex, { timeout: 15000 });
257+}
258+```
259+
260+## Test Execution Patterns
261+
262+### Waiting Strategies
263+
264+1. **Use Playwright's auto-waiting**:
265+```typescript
266+// ✅ Good - Playwright waits automatically
267+await button.click();
268+
269+// ❌ Avoid - unnecessary manual waits
270+await page.waitForTimeout(1000);
271+await button.click();
272+```
273+
274+2. **Use explicit waits for async operations**:
275+```typescript
276+// ✅ Good - wait for specific condition
277+await expect(element).toBeVisible({ timeout: 10000 });
278+
279+// ✅ Good - wait for URL change
280+await expect(page).toHaveURL(/\/contacts\/\d+\/details/);
281+```
282+
283+3. **Use `waitForTimeout` sparingly** (only when necessary):
284+```typescript
285+// Only when waiting for async operations that can't be detected
286+await page.waitForTimeout(300); // Wait for dropdown to render
287+```
288+
289+### Error Handling
290+
291+- **Use try/finally blocks** for cleanup:
292+```typescript
293+test('User can perform action', async ({ page, request }) => {
294+ const uniqueEmail = `test+${Date.now()}@example.com`;
295+
296+ try {
297+ // Test logic
298+ await createTestUser(request, { email: uniqueEmail });
299+ // ... test steps ...
300+ } finally {
301+ // Cleanup
302+ await deleteUserFromStrapi(request, uniqueEmail);
303+ await request.delete('http://localhost:8025/api/v1/messages');
304+ }
305+});
306+```
307+
308+### Test Isolation
309+
310+- **Each test should be independent** - don't rely on test execution order
311+- **Clean up test data** after each test
312+- **Use unique identifiers** to avoid conflicts
313+
314+## Assertions
315+
316+### Best Practices
317+
318+1. **Always include descriptive messages**:
319+```typescript
320+// ✅ Good
321+await expect(contactRow, 'Contact row should contain correct email')
322+ .toContainText(contact.email);
323+
324+// ❌ Avoid
325+await expect(contactRow).toContainText(contact.email);
326+```
327+
328+2. **Use appropriate matchers**:
329+```typescript
330+await expect(element).toBeVisible({ timeout: 10000 });
331+await expect(element).toHaveText('Expected Text');
332+await expect(element).toContainText('Partial Text');
333+await expect(page).toHaveURL(/\/crm$/);
334+await expect(locator).toHaveCount(1);
335+```
336+
337+3. **Set reasonable timeouts**:
338+```typescript
339+// Default timeout: 5000ms
340+await expect(element).toBeVisible();
341+
342+// Custom timeout for slow operations
343+await expect(element).toBeVisible({ timeout: 20000 });
344+```
345+
346+## Helper Functions
347+
348+### Reusable Test Helpers
349+
350+Create helper functions for common operations:
351+
352+```typescript
353+// In test file or utils
354+async function createContactViaUI(data: ContactData) {
355+ await contactsListPage.clickCreateButton();
356+ await contactCreateModal.waitForDialogVisible();
357+ await contactCreateModal.fillAndSubmit(data);
358+ await contactCreateModal.expectCreationStatusMessage(data.firstName);
359+ await contactsListPage.goto();
360+ await expect(contactsListPage.getRowLocator(data.email))
361+ .toBeVisible({ timeout: 10000 });
362+}
363+```
364+
365+### External Service Helpers
366+
367+For services like Mailpit, create helper classes:
368+
369+```typescript
370+export class MailpitHelper {
371+ readonly request: APIRequestContext;
372+
373+ constructor(request: APIRequestContext) {
374+ this.request = request;
375+ }
376+
377+ async waitForEmails(recipient: string, subject: string, expectedCount = 2) {
378+ // Implementation
379+ }
380+}
381+```
382+
383+## Test Configuration
384+
385+### Playwright Config
386+
387+Key configuration patterns:
388+
389+```typescript
390+export default defineConfig({
391+ testDir: './tests',
392+ timeout: TIMEOUT, // Default: 30000
393+ globalSetup: require.resolve('./tests/setup/global-setup'),
394+ expect: {
395+ timeout: EXPECT_TIMEOUT, // Default: 5000
396+ },
397+ fullyParallel: false, // Set to false for sequential execution
398+ retries: CI ? 1 : 0,
399+ workers: CI ? 1 : WORKERS,
400+ use: {
401+ baseURL: CRM_BASE_URL,
402+ trace: 'on-first-retry',
403+ screenshot: 'only-on-failure',
404+ video: 'on-first-retry',
405+ },
406+});
407+```
408+
409+### Environment Variables
410+
411+Required environment variables:
412+
413+- `CRM_BASE_URL` - Base URL for the application
414+- `TEST_USER_EMAIL` - Test user email
415+- `TEST_USER_PASSWORD` - Test user password
416+- `STRAPI_TEST_ADMIN_EMAIL` - Strapi admin email
417+- `STRAPI_TEST_ADMIN_PASSWORD` - Strapi admin password
418+- `PLAYWRIGHT_WORKERS` - Number of workers (optional)
419+- `PLAYWRIGHT_RETRIES` - Number of retries (optional)
420+- `PLAYWRIGHT_TIMEOUT` - Test timeout (optional)
421+
422+## Test Maintenance
423+
424+### Handling Flaky Tests
425+
426+1. **Increase timeouts** for slow operations
427+2. **Add explicit waits** for async operations
428+3. **Use more stable locators** (role-based over CSS)
429+4. **Retry logic** for known flaky operations:
430+```typescript
431+let langSelected = false;
432+for (let i = 0; i < 3; i++) {
433+ try {
434+ await langOption.click();
435+ langSelected = true;
436+ break;
437+ } catch (err) {
438+ if (i === 2) throw err;
439+ await this.page.waitForTimeout(100);
440+ }
441+}
442+```
443+
444+### Skipping Tests
445+
446+- **Use `test.skip()`** for temporarily disabled tests:
447+```typescript
448+test.skip('should allow creating a journey with drag-and-drop', async () => {
449+ // Test implementation
450+});
451+```
452+
453+- **Use `test.fail()`** for tests that are expected to fail (document why):
454+```typescript
455+// This test is marked as expected to fail due to a known application bug.
456+test.fail('User can edit a list name (expected failure due to edit bug)', async () => {
457+ // Test implementation
458+});
459+```
460+
461+## Code Style
462+
463+### Comments
464+
465+- **Add comments** explaining complex test logic
466+- **Document test steps** in multi-step tests:
467+```typescript
468+// Step 1: Navigate to the login page
469+await loginPage.goto();
470+
471+// Step 2: Fill in credentials
472+await loginPage.fillCredentials(email, password);
473+
474+// Step 3: Submit and verify
475+await loginPage.clickSignIn();
476+await commonPage.expectDashboardVisible();
477+```
478+
479+### Naming Conventions
480+
481+- **Test descriptions**: Use "User can..." or "should..." format
482+- **Helper functions**: Use descriptive verb phrases
483+- **Variables**: Use camelCase with descriptive names
484+
485+## Common Patterns
486+
487+### Row Operations
488+
489+```typescript
490+// Get row locator
491+const row = contactsListPage.getRowLocator(uniqueEmail);
492+
493+// Get row-specific elements
494+const checkbox = contactsListPage.getCheckboxForRow(row);
495+const link = contactsListPage.getLinkForRow(row, firstName);
496+const deleteButton = contactsListPage.getDeleteButtonForRow(row);
497+
498+// Interact with row
499+await checkbox.check();
500+await link.click();
501+await deleteButton.click();
502+```
503+
504+### Modal/Dialog Operations
505+
506+```typescript
507+// Wait for modal
508+await modal.waitForDialogVisible();
509+
510+// Fill form
511+await modal.fillAndSubmit(data);
512+
513+// Verify success
514+await modal.expectCreationStatusMessage(data.name);
515+```
516+
517+### Mass Actions
518+
519+```typescript
520+// Select items
521+await contactsListPage.getCheckboxForRow(row).check();
522+
523+// Open mass actions menu
524+await contactsListPage.openMassActionsMenu();
525+
526+// Perform action
527+await contactsListPage.clickDeleteMassAction();
528+await contactsListPage.clickDeleteConfirmMassAction();
529+```
530+
531+## Best Practices Summary
532+
533+1. ✅ **Use Page Object Model** for all page interactions
534+2. ✅ **Prefer role-based locators** over CSS selectors
535+3. ✅ **Use descriptive test names** and assertion messages
536+4. ✅ **Generate unique test data** using Faker
537+5. ✅ **Clean up test data** in finally blocks
538+6. ✅ **Use helper functions** for common operations
539+7. ✅ **Set appropriate timeouts** for async operations
540+8. ✅ **Keep tests independent** and isolated
541+9. ✅ **Document complex test logic** with comments
542+10. ✅ **Handle errors gracefully** with try/finally blocks
543+
544+## Anti-Patterns to Avoid
545+
546+1. ❌ **Hardcoded test data** - Use Faker or environment variables
547+2. ❌ **CSS selectors** - Prefer role-based or accessible selectors
548+3. ❌ **Unnecessary waits** - Use Playwright's auto-waiting
549+4. ❌ **Test dependencies** - Keep tests independent
550+5. ❌ **Missing cleanup** - Always clean up test data
551+6. ❌ **Vague assertions** - Include descriptive error messages
552+7. ❌ **Duplicate code** - Extract to helper functions or POMs
553+8. ❌ **Fragile locators** - Use stable, accessible selectors
554+
