| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 1 | 24 | 43 | 1% |
| Commands | 0 | 0 | 0 | — |
| Section tags | 2 | 1 | 5 | 25% |
What each file covers
Sections
1 shared · 24 only in A · 43 only in B- − Translation Guidelines
- − Internationalization (i18n) Overview
- − Supported Languages
- − i18n Architecture
- − File Structure
- − Translation Files
- − Translation Keys
- − Translation Implementation
- − React Components
- − Interpolation
- − Pluralization
- − Translation Management
- − Adding New Strings
- − Translation Validation
- − Key Naming
- − String Guidelines
- − Context Information
- − Workflow
- − Development Process
- − Translation Updates
- − Quality Assurance
- − Maintenance
- − Regular Tasks
- − Tools and Automation
- + 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
- + Helper Functions
- + Reusable Test Helpers
- + External Service Helpers
- + Test Configuration
- + Playwright Config
- + Environment Variables
- + Test Maintenance
- + Handling Flaky Tests
- + Skipping Tests
- + Code Style
- + Comments
- + Naming Conventions
- + Common Patterns
- + Row Operations
- + Modal/Dialog Operations
- + Mass Actions
- + Best Practices Summary
- + Anti-Patterns to Avoid
- Best Practices
Commands
neither file has anySection tags
2 shared · 1 only in A · 5 only in B- − agent-behaviour
- + setup
- + test
- + security
- + do-not
- + docs
- code-style
- architecture
Line diff
nowtec/nowCRM · .cursor/rules/translations.mdc
@@ −1 @@
1---
2description: Translation guidelines for NOWCRM
3alwaysApply: false
4---
5# Translation Guidelines
6
7## Internationalization (i18n) Overview
8
9### Supported Languages
10- English (en) - Primary language
11- French (fr) - Secondary language
12- Italian (it) - Secondary language
13- German (de) - Secondary language
14
15### i18n Architecture
16- Use next-intl for React components
17- Store translations in JSON files
18- Implement namespace-based organization
19- Support for interpolation and pluralization
20
21## File Structure
22
23### Translation Files
24```
25/apps/nowcrm/messages/
26├── en.json # English translations
27├── fr.json # French translations
28├── de.json # German translations
29└── it.json # Italian translations
30```
31
32### Translation Keys
33- Use nested objects for organization
34- Follow consistent naming patterns
35- Include context in key names
36 ```json
37 {
38 "auth": {
39 "login": {
40 "title": "Sign In",
41 "email": "Email Address",
42 "password": "Password",
43 "submit": "Sign In",
44 "forgotPassword": "Forgot Password?"
45 },
46 "register": {
47 "title": "Create Account",
48 "confirmPassword": "Confirm Password"
49 }
50 }
51 }
52 ```
53
54## Translation Implementation
55
56### React Components
57- Specify namespaces for better organization
58- Handle loading states properly
59
60#### Server Components
61
62```ts
63import { getTranslations } from 'next-intl';
64
65export default async function ContactsPage() {
66 const t = await getTranslations('Contacts');
67 return (
68 <main>
69 <h1>{t('contacts.header')}</h1>
70 {/* … */}
71 </main>
72 );
73}
74```
75
76#### Client Components
77
78```tsx
79'use client';
80import { useTranslations } from 'next-intl';
81
82export default function LoginForm() {
83 const t = useTranslations('auth');
84
85 return (
86 <form>
87 <h1>{t('login.title')}</h1>
88 <input placeholder={t('login.email')} type="email" />
89 <input placeholder={t('login.password')} type="password" />
90 <button type="submit">{t('login.submit')}</button>
91 </form>
92 );
93}
94```
95
96### Interpolation
97- Use interpolation for dynamic content
98- Pass variables through t() function
99- Keep interpolation simple and readable
100 ```typescript
101 // ✅ Correct
102 const WelcomeMessage = ({ userName }: { userName: string }) => {
103 const { t } = useTranslations('common');
104
105 return (
106 <h1>{t('welcome.message', { name: userName })}</h1>
107 );
108 };
109
110 // Translation file
111 {
112 "welcome": {
113 "message": "Welcome back, {{name}}!"
114 }
115 }
116 ```
117
118### Pluralization
119- Handle singular/plural forms correctly
120- Use count-based pluralization
121- Support different plural rules per language
122 ```typescript
123 // ✅ Correct
124 const ItemCount = ({ count }: { count: number }) => {
125 const { t } = useTranslations('common');
126
127 return (
128 <span>{t('items.count', { count })}</span>
129 );
130 };
131
132 // Translation file
133 {
134 "items": {
135 "count_one": "{{count}} item",
136 "count_other": "{{count}} items"
137 }
138 }
139 ```
140
141## Translation Management
142
143### Adding New Strings
1441. Add English translation first
1452. Use descriptive keys that indicate context
1463. Include comments for translators when needed
1474. Test with long translations to ensure UI flexibility
148 ```json
149 {
150 "user": {
151 "profile": {
152 // Displayed in user profile header
153 "displayName": "Display Name",
154 // Used in forms when editing profile
155 "editDisplayName": "Edit Display Name",
156 // Confirmation message after profile update
157 "updateSuccess": "Profile updated successfully"
158 }
159 }
160 }
161 ```
162
163### Translation Validation
164- Use TypeScript for translation key validation
165- Implement automated checks for missing translations
166- Validate interpolation parameters
167 ```typescript
168 // ✅ Correct - Type-safe translations
169 type TranslationKey =
170 | 'auth.login.title'
171 | 'auth.login.email'
172 | 'auth.login.password'
173 | 'common.welcome.message';
174
175 const t = (key: TranslationKey, options?: any) => {
176 // Translation implementation
177 };
178 ```
179
180## Best Practices
181
182### Key Naming
183- Use descriptive, hierarchical keys
184- Avoid abbreviations
185- Group related translations
186- Keep keys consistent across languages
187 ```json
188 // ✅ Correct
189 {
190 "dashboard": {
191 "header": {
192 "title": "Dashboard",
193 "subtitle": "Welcome to your workspace"
194 },
195 "actions": {
196 "createNew": "Create New",
197 "refresh": "Refresh Data",
198 "export": "Export"
199 }
200 }
201 }
202
203 // ❌ Incorrect
204 {
205 "dash_title": "Dashboard",
206 "newBtn": "New",
207 "refreshData": "Refresh"
208 }
209 ```
210
211### String Guidelines
212- Write clear, concise text
213- Use consistent terminology
214- Consider character limits for UI elements
215- Avoid concatenating translated strings
216 ```json
217 // ✅ Correct
218 {
219 "user": {
220 "status": {
221 "online": "Online",
222 "offline": "Offline",
223 "away": "Away"
224 }
225 }
226 }
227
228 // ❌ Incorrect - Don't concatenate
229 {
230 "user": {
231 "statusPrefix": "User is ",
232 "statusOnline": "online"
233 }
234 }
235 ```
236
237### Context Information
238- Provide context for translators
239- Include character limits when relevant
240- Explain when/where text appears
241- Note any technical constraints
242 ```json
243 {
244 "button": {
245 // Primary action button, max 20 characters
246 "save": "Save Changes",
247 // Secondary button in modal footer
248 "cancel": "Cancel",
249 // Destructive action, should sound cautious
250 "delete": "Delete Permanently"
251 }
252 }
253 ```
254
255## Workflow
256
257### Development Process
2581. Develop features with English translations
2592. Use placeholder keys during development
2603. Finalize translation keys before feature completion
2614. Add translations to all supported languages
2625. Test with different language strings
263
264### Translation Updates
2651. Create translation tasks for new features
2662. Provide context and screenshots to translators
2673. Review translations for consistency
2684. Test UI with translated strings
2695. Update documentation when needed
270
271### Quality Assurance
272- Review translations in context
273- Test with longest expected translations
274- Verify formatting with interpolation
275- Check for cultural appropriateness
276- Ensure accessibility with screen readers
277
278## Maintenance
279
280### Regular Tasks
281- Review and update outdated translations
282- Check for unused translation keys
283- Maintain consistency across languages
284- Monitor for missing translations in new features
285
286### Tools and Automation
287- Use automated translation validation
288- Implement missing translation detection
289- Set up continuous integration checks
290- Use translation management platforms when needed
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 @@
11 ---
2−description: Translation guidelines for NOWCRM
2+description: Testing guidelines for NOWCRM
3+globs: ["**/*.spec.ts", "**/*.test.ts", "**/tests/**", "**/playwright.config.ts"]
34 alwaysApply: false
45 ---
5−# Translation Guidelines
66
7−## Internationalization (i18n) Overview
7+# Testing Guidelines for NOWCRM
88
9−### Supported Languages
10−- English (en) - Primary language
11−- French (fr) - Secondary language
12−- Italian (it) - Secondary language
13−- German (de) - Secondary language
9+## Overview
1410
15−### i18n Architecture
16−- Use next-intl for React components
17−- Store translations in JSON files
18−- Implement namespace-based organization
19−- Support for interpolation and pluralization
11+NOWCRM uses **Playwright** for end-to-end (E2E) testing. All tests follow the **Page Object Model (POM)** pattern for maintainability and reusability.
2012
21−## File Structure
13+## Test Structure
2214
23−### Translation Files
15+### Directory Organization
16+
2417 ```
25−/apps/nowcrm/messages/
26−├── en.json # English translations
27−├── fr.json # French translations
28−├── de.json # German translations
29−└── it.json # Italian translations
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
3032 ```
3133
32−### Translation Keys
33−- Use nested objects for organization
34−- Follow consistent naming patterns
35−- Include context in key names
36− ```json
37− {
38− "auth": {
39− "login": {
40− "title": "Sign In",
41− "email": "Email Address",
42− "password": "Password",
43− "submit": "Sign In",
44− "forgotPassword": "Forgot Password?"
45− },
46− "register": {
47− "title": "Create Account",
48− "confirmPassword": "Confirm Password"
49− }
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' });
5059 }
51− }
52− ```
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+```
5373
54−## Translation Implementation
74+### Locator Best Practices
5575
56−### React Components
57−- Specify namespaces for better organization
58−- Handle loading states properly
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' });
5981
60−#### Server Components
82+// ❌ Avoid - fragile CSS selectors
83+this.createButton = page.locator('.btn-primary');
84+```
6185
62−```ts
63−import { getTranslations } from 'next-intl';
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+```
6494
65−export default async function ContactsPage() {
66− const t = await getTranslations('Contacts');
67− return (
68− <main>
69− <h1>{t('contacts.header')}</h1>
70− {/* … */}
71− </main>
72− );
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();
73116 }
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+}
74124 ```
75125
76−#### Client Components
126+### Assertion Methods
77127
78−```tsx
79−'use client';
80−import { useTranslations } from 'next-intl';
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
81131
82−export default function LoginForm() {
83− const t = useTranslations('auth');
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+}
84138
85− return (
86− <form>
87− <h1>{t('login.title')}</h1>
88− <input placeholder={t('login.email')} type="email" />
89− <input placeholder={t('login.password')} type="password" />
90− <button type="submit">{t('login.submit')}</button>
91− </form>
92− );
139+async expectDashboardVisible(timeout: number = 10000) {
140+ await expect(this.page, 'URL should indicate CRM dashboard')
141+ .toHaveURL(/\/crm$/, { timeout });
93142 }
94143 ```
95144
96−### Interpolation
97−- Use interpolation for dynamic content
98−- Pass variables through t() function
99−- Keep interpolation simple and readable
100− ```typescript
101− // ✅ Correct
102− const WelcomeMessage = ({ userName }: { userName: string }) => {
103− const { t } = useTranslations('common');
145+## Test File Structure
104146
105− return (
106− <h1>{t('welcome.message', { name: userName })}</h1>
107− );
108− };
147+### Basic Template
109148
110− // Translation file
111− {
112− "welcome": {
113− "message": "Welcome back, {{name}}!"
114− }
115− }
116− ```
149+```typescript
150+import { test, expect } from '@playwright/test';
151+import { faker } from '@faker-js/faker';
117152
118−### Pluralization
119−- Handle singular/plural forms correctly
120−- Use count-based pluralization
121−- Support different plural rules per language
122− ```typescript
123− // ✅ Correct
124− const ItemCount = ({ count }: { count: number }) => {
125− const { t } = useTranslations('common');
153+// Import Page Object Models
154+import { ContactsListPage } from './pages/ContactsListPage';
155+import { ContactCreateModal } from './pages/ContactCreateModal';
126156
127− return (
128− <span>{t('items.count', { count })}</span>
129− );
130− };
157+// Import utilities
158+import { loginUser } from './utils/authHelper';
131159
132− // Translation file
133− {
134− "items": {
135− "count_one": "{{count}} item",
136− "count_other": "{{count}} items"
137− }
138− }
139− ```
160+test.describe('Feature Name', () => {
161+ let pageObject1: ContactsListPage;
162+ let pageObject2: ContactCreateModal;
140163
141−## Translation Management
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+ });
142173
143−### Adding New Strings
144−1. Add English translation first
145−2. Use descriptive keys that indicate context
146−3. Include comments for translators when needed
147−4. Test with long translations to ensure UI flexibility
148− ```json
149− {
150− "user": {
151− "profile": {
152− // Displayed in user profile header
153− "displayName": "Display Name",
154− // Used in forms when editing profile
155− "editDisplayName": "Edit Display Name",
156− // Confirmation message after profile update
157− "updateSuccess": "Profile updated successfully"
158− }
159− }
160− }
161− ```
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+```
162192
163−### Translation Validation
164−- Use TypeScript for translation key validation
165−- Implement automated checks for missing translations
166−- Validate interpolation parameters
167− ```typescript
168− // ✅ Correct - Type-safe translations
169− type TranslationKey =
170− | 'auth.login.title'
171− | 'auth.login.email'
172− | 'auth.login.password'
173− | 'common.welcome.message';
193+### Test Organization
174194
175− const t = (key: TranslationKey, options?: any) => {
176− // Translation implementation
177− };
178− ```
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')
179199
180−## Best Practices
200+## Test Data Management
181201
182−### Key Naming
183−- Use descriptive, hierarchical keys
184−- Avoid abbreviations
185−- Group related translations
186−- Keep keys consistent across languages
187− ```json
188− // ✅ Correct
189− {
190− "dashboard": {
191− "header": {
192− "title": "Dashboard",
193− "subtitle": "Welcome to your workspace"
194− },
195− "actions": {
196− "createNew": "Create New",
197− "refresh": "Refresh Data",
198− "export": "Export"
199− }
200− }
201− }
202+### Using Faker for Test Data
202203
203− // ❌ Incorrect
204− {
205− "dash_title": "Dashboard",
206− "newBtn": "New",
207− "refreshData": "Refresh"
208− }
209− ```
204+```typescript
205+import { faker } from '@faker-js/faker';
210206
211−### String Guidelines
212−- Write clear, concise text
213−- Use consistent terminology
214−- Consider character limits for UI elements
215−- Avoid concatenating translated strings
216− ```json
217− // ✅ Correct
218− {
219− "user": {
220− "status": {
221− "online": "Online",
222− "offline": "Offline",
223− "away": "Away"
224− }
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');
225304 }
226− }
305+});
306+```
227307
228− // ❌ Incorrect - Don't concatenate
229− {
230− "user": {
231− "statusPrefix": "User is ",
232− "statusOnline": "online"
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;
233375 }
234− }
235− ```
376+
377+ async waitForEmails(recipient: string, subject: string, expectedCount = 2) {
378+ // Implementation
379+ }
380+}
381+```
236382
237−### Context Information
238−- Provide context for translators
239−- Include character limits when relevant
240−- Explain when/where text appears
241−- Note any technical constraints
242− ```json
243− {
244− "button": {
245− // Primary action button, max 20 characters
246− "save": "Save Changes",
247− // Secondary button in modal footer
248− "cancel": "Cancel",
249− // Destructive action, should sound cautious
250− "delete": "Delete Permanently"
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);
251440 }
252− }
253− ```
441+}
442+```
254443
255−## Workflow
444+### Skipping Tests
256445
257−### Development Process
258−1. Develop features with English translations
259−2. Use placeholder keys during development
260−3. Finalize translation keys before feature completion
261−4. Add translations to all supported languages
262−5. Test with different language strings
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+```
263452
264−### Translation Updates
265−1. Create translation tasks for new features
266−2. Provide context and screenshots to translators
267−3. Review translations for consistency
268−4. Test UI with translated strings
269−5. Update documentation when needed
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+```
270460
271−### Quality Assurance
272−- Review translations in context
273−- Test with longest expected translations
274−- Verify formatting with interpolation
275−- Check for cultural appropriateness
276−- Ensure accessibility with screen readers
461+## Code Style
277462
278−## Maintenance
463+### Comments
279464
280−### Regular Tasks
281−- Review and update outdated translations
282−- Check for unused translation keys
283−- Maintain consistency across languages
284−- Monitor for missing translations in new features
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();
285470
286−### Tools and Automation
287−- Use automated translation validation
288−- Implement missing translation detection
289−- Set up continuous integration checks
290−- Use translation management platforms when needed
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+
