|
| 1 | +--- |
| 2 | +description: Rules for end-to-end tests |
| 3 | +globs: */tests/e2e/** |
| 4 | +alwaysApply: false |
| 5 | +--- |
| 6 | +# End-to-End Tests |
| 7 | + |
| 8 | +These rules outline the structure, patterns, and best practices for writing end-to-end tests. |
| 9 | + |
| 10 | +## Implementation |
| 11 | + |
| 12 | +1. Use `[CLI_ALIAS] e2e` with these option categories to optimize test execution: |
| 13 | + - Test filtering: `--smoke`, `--include-slow`, search terms (e.g., `"@smoke"`, `"smoke"`, `"user"`, `"localization"`), `--browser` |
| 14 | + - Change scoping: `--last-failed`, `--only-changed` |
| 15 | + - Flaky test detection: `--repeat-each`, `--retries`, `--stop-on-first-failure` |
| 16 | + - Performance: `--debug-timings` shows step execution times with color coding |
| 17 | + |
| 18 | +2. Test Search and Filtering: |
| 19 | + - Search by test tags: `[CLI_ALIAS] e2e "@smoke"` or `[CLI_ALIAS] e2e "smoke"` (both work the same) |
| 20 | + - Search by test content: `[CLI_ALIAS] e2e "user"` (finds tests with "user" in title or content) |
| 21 | + - Search by filename: `[CLI_ALIAS] e2e "localization"` (finds localization-flows.spec.ts) |
| 22 | + - Search by specific file: `[CLI_ALIAS] e2e "user-management-flows.spec.ts"` |
| 23 | + - Multiple search terms: `[CLI_ALIAS] e2e "user" "management"` |
| 24 | + - The CLI automatically detects which self-contained systems contain matching tests and only runs those |
| 25 | + |
| 26 | +3. Test-Driven Debugging Process: |
| 27 | + - Focus on one failing test at a time and make it pass before moving to the next. |
| 28 | + - Ensure tests use Playwright's built-in auto-waiting assertions: `toHaveURL()`, `toBeVisible()`, `toBeEnabled()`, `toHaveValue()`, `toContainText()`. |
| 29 | + - Consider if root causes can be fixed in the application code, and fix application bugs rather than masking them with test workarounds. |
| 30 | + |
| 31 | +4. Organize tests in a consistent file structure: |
| 32 | + - All e2e test files must be located in `[self-contained-system]/WebApp/tests/e2e/` folder (e.g., `application/account-management/WebApp/tests/e2e/`). |
| 33 | + - All test files use the `*-flows.spec.ts` naming convention (e.g., `login-flows.spec.ts`, `signup-flows.spec.ts`, `user-management-flows.spec.ts`). |
| 34 | + - Top-level describe blocks must use only these 3 approved tags: `test.describe("@smoke", () => {})`, `test.describe("@comprehensive", () => {})`, `test.describe("@slow", () => {})`. |
| 35 | + - `@smoke` tests: |
| 36 | + - Critical tests run on deployment of any self-contained system. |
| 37 | + - Should be comprehensive scenarios that test core user journeys. |
| 38 | + - Keep tests focused on specific flows to reduce fragility while maintaining coverage. |
| 39 | + - Focus on must-work functionality with extensive validation steps. |
| 40 | + - Include boundary cases and error handling within the same test scenario. |
| 41 | + - Avoid testing the same functionality multiple times across different tests. |
| 42 | + |
| 43 | + - `@comprehensive` tests: |
| 44 | + - Thorough tests run when a specific self-contained system is deployed. |
| 45 | + - Focus on edge cases, error conditions, and less common scenarios. |
| 46 | + - Test specific features in depth with various input combinations. |
| 47 | + - Include tests for concurrency, validation rules, accessibility, etc. |
| 48 | + - Group related edge cases together to reduce test count while maintaining coverage. |
| 49 | + |
| 50 | + - `@slow` tests: |
| 51 | + - Optional and run only ad-hoc using `--include-slow` flag. |
| 52 | + - Any tests that require waiting like `waitForTimeout` (e.g., for OTP timeouts) must be marked as `@slow`. |
| 53 | + - Include tests for rate limiting with actual wait times, session timeouts, etc. |
| 54 | + - Use `test.setTimeout()` at the individual test level based on actual wait times needed. |
| 55 | + |
| 56 | +5. Write clear test descriptions and documentation: |
| 57 | + - Test descriptions must accurately reflect what the test covers and be kept in sync with test implementation. |
| 58 | + - Use descriptive test names that clearly indicate the functionality being tested (e.g., "should handle single and bulk user deletion workflows with dashboard integration"). |
| 59 | + - Include JSDoc comments above complex tests listing all major features/scenarios covered. |
| 60 | + - When adding new functionality to existing tests, update both the test description and JSDoc comments to reflect changes. |
| 61 | + |
| 62 | +6. Structure each test with step decorators and proper monitoring: |
| 63 | + - All tests must start with `const context = createTestContext(page);` for proper error monitoring. |
| 64 | + - Use step decorators: `await step("Complete signup & verify account creation")(async () => { /* test logic */ })();` |
| 65 | + - Step naming conventions: |
| 66 | + - Always follow "[Business action + details] & [expected outcome]" pattern. |
| 67 | + - Use business action verbs like "Sign up", "Login", "Invite", "Rename", "Update", "Delete", "Create", "Submit". |
| 68 | + - Never use test/assertion prefixes like "Test", "Verify", "Check", "Validate", "Ensure"; use descriptive business actions instead. |
| 69 | + - Every step must include an action (arrange/act) followed by assertions, not pure assertion steps. |
| 70 | + - Step structure: |
| 71 | + - Use blank lines to separate arrange/act/assert sections within steps. |
| 72 | + - Keep shared variable declarations outside steps when used across multiple steps. |
| 73 | + - Use section headers with `// === SECTION NAME ===` to group related steps. |
| 74 | + - Add JSDoc comments for complex test workflows. |
| 75 | + - Use semantic selectors: `page.getByRole("button", { name: "Submit" })`, `page.getByText("Welcome")`, `page.getByLabel("Email")`. |
| 76 | + - Assert side effects immediately after actions using `expectToastMessage`, `expectValidationError`, `expectNetworkErrors`. |
| 77 | + - Form validation pattern: Use `await blurActiveElement(page);` when updating a textbox the second time before submitting a form to trigger validation. |
| 78 | + |
| 79 | +7. Timeout Configuration: |
| 80 | + - Always use Playwright's built-in auto-waiting assertions: `toHaveURL()`, `toBeVisible()`, `toBeEnabled()`, `toHaveValue()`, `toContainText()`. |
| 81 | + - Never add timeouts to `.click()`, `.waitForSelector()`, etc. |
| 82 | + - Global timeout configuration is handled in the shared Playwright. Don't change this. |
| 83 | + |
| 84 | +8. Write deterministic tests - This is critical for reliable testing: |
| 85 | + - Each test should have a clear, linear flow of actions and assertions. |
| 86 | + - Never use if statements, custom error handling, or try/catch blocks in tests. |
| 87 | + - Never use regular expressions in tests; use simple string matching instead. |
| 88 | + |
| 89 | +9. What to test: |
| 90 | + - Enter invalid values, such as empty strings, only whitespace characters, long strings, negative numbers, Unicode, etc. |
| 91 | + - Tooltips, keyboard navigation, accessibility, validation messages, translations, responsiveness, etc. |
| 92 | + |
| 93 | +10. Test Fixtures and Page Management: |
| 94 | + - Use appropriate fixtures: `{ page }` for basic tests, `{ anonymousPage }` for tests with existing tenant/owner but not logged in, `{ ownerPage }`, `{ adminPage }`, `{ memberPage }` for authenticated tests. |
| 95 | + - Destructure anonymous page data: `const { page, tenant } = anonymousPage; const existingUser = tenant.owner;` |
| 96 | + - Pre-logged in users (`ownerPage`, `adminPage`, `memberPage`) are isolated between workers and will not conflict between tests. |
| 97 | + - When using pre-logged in users, do not put the tenant or user into an invalid state that could affect other tests. |
| 98 | + |
| 99 | +11. Test Data and Constants: |
| 100 | + - Use underscore separators: `const timeout = 30_000; // 30 seconds` |
| 101 | + - Generate unique data: `const email = uniqueEmail();` |
| 102 | + - Use faker.js to generate realistic test data: `const firstName = faker.person.firstName(); const email = faker.internet.email();` |
| 103 | + - Long string testing: `const longEmail = \`${"a".repeat(90)}@example.com\`; // 101 characters total` |
| 104 | + |
| 105 | +12. Memory Management in E2E Tests: |
| 106 | + - Playwright automatically handles browser context cleanup after tests |
| 107 | + - Manual cleanup steps are unnecessary - focus on test clarity over micro-optimizations |
| 108 | + - E2E test suites have minimal memory leak concerns due to their limited scope and duration |
| 109 | + |
| 110 | +## Examples |
| 111 | + |
| 112 | +### ✅ Good Step Naming Examples |
| 113 | +```typescript |
| 114 | +// ✅ DO: Business action + details & expected outcome |
| 115 | +await step("Submit invalid email & verify validation error")(async () => { |
| 116 | + await page.getByLabel("Email").fill("invalid-email"); |
| 117 | + await blurActiveElement(page); |
| 118 | + |
| 119 | + await expectValidationError(context, "Invalid email."); |
| 120 | +})(); |
| 121 | + |
| 122 | +await step("Sign up with valid credentials & verify account creation")(async () => { |
| 123 | + await page.getByRole("button", { name: "Submit" }).click(); |
| 124 | + |
| 125 | + await expect(page.getByText("Welcome")).toBeVisible(); |
| 126 | +})(); |
| 127 | + |
| 128 | +await step("Update user role to admin & verify permission change")(async () => { |
| 129 | + const userRow = page.locator("tbody tr").first(); |
| 130 | + |
| 131 | + await userRow.getByLabel("User actions").click(); |
| 132 | + await page.getByRole("menuitem", { name: "Change role" }).click(); |
| 133 | + |
| 134 | + await expect(page.getByRole("alertdialog", { name: "Change user role" })).toBeVisible(); |
| 135 | +})(); |
| 136 | +``` |
| 137 | + |
| 138 | +### ❌ Bad Step Naming Examples |
| 139 | +```typescript |
| 140 | +// ❌ DON'T: Pure assertion steps without actions |
| 141 | +await step("Verify button is visible")(async () => { |
| 142 | + await expect(page.getByRole("button")).toBeVisible(); // No action, only assertion |
| 143 | +})(); |
| 144 | + |
| 145 | +// ❌ DON'T: Using test/assertion prefixes |
| 146 | +await step("Check user permissions")(async () => { // "Check" is assertion prefix |
| 147 | + await expect(page.getByText("Admin")).toBeVisible(); |
| 148 | +})(); |
| 149 | + |
| 150 | +await step("Validate form state")(async () => { // "Validate" is assertion prefix |
| 151 | + await expect(page.getByRole("textbox")).toBeEmpty(); |
| 152 | +})(); |
| 153 | + |
| 154 | +await step("Ensure user is deleted")(async () => { // "Ensure" is assertion prefix |
| 155 | + await expect(page.getByText("user@example.com")).not.toBeVisible(); |
| 156 | +})(); |
| 157 | +``` |
| 158 | + |
| 159 | +### ✅ Complete Test Example |
| 160 | +```typescript |
| 161 | +import { step } from "@shared/e2e/utils/step-decorator"; |
| 162 | +import { expectValidationError, blurActiveElement, createTestContext } from "@shared/e2e/utils/test-assertions"; |
| 163 | +import { testUser } from "@shared/e2e/utils/test-data"; |
| 164 | + |
| 165 | +test.describe("@smoke", () => { |
| 166 | + test("should complete signup with validation", async ({ page }) => { |
| 167 | + const context = createTestContext(page); |
| 168 | + const user = testUser(); |
| 169 | + |
| 170 | + await step("Submit invalid email & verify validation error")(async () => { |
| 171 | + await page.goto("/signup"); |
| 172 | + await page.getByLabel("Email").fill("invalid-email"); |
| 173 | + await blurActiveElement(page); // ✅ DO: Trigger validation when updating textbox second time |
| 174 | + |
| 175 | + await expectValidationError(context, "Invalid email."); |
| 176 | + })(); |
| 177 | + |
| 178 | + await step("Sign up with valid email & verify verification redirect")(async () => { |
| 179 | + await page.getByLabel("Email").fill(user.email); |
| 180 | + await page.getByRole("button", { name: "Continue" }).click(); |
| 181 | + |
| 182 | + await expect(page).toHaveURL("/verify"); |
| 183 | + })(); |
| 184 | + }); |
| 185 | +}); |
| 186 | + |
| 187 | +test.describe("@comprehensive", () => { |
| 188 | + test("should handle user management with pre-logged owner", async ({ ownerPage }) => { |
| 189 | + createTestContext(ownerPage); // ✅ DO: Create context for pre-logged users |
| 190 | + |
| 191 | + await step("Access user management & verify owner permissions")(async () => { |
| 192 | + await ownerPage.getByRole("button", { name: "Users" }).click(); |
| 193 | + |
| 194 | + await expect(ownerPage.getByRole("heading", { name: "Users" })).toBeVisible(); |
| 195 | + })(); |
| 196 | + }); |
| 197 | +}); |
| 198 | + |
| 199 | +test.describe("@slow", () => { |
| 200 | + const requestNewCodeTimeout = 30_000; // 30 seconds |
| 201 | + const codeValidationTimeout = 60_000; // 5 minutes |
| 202 | + const sessionTimeout = codeValidationTimeout + 60_000; // 6 minutes |
| 203 | + |
| 204 | + test("should handle user logout after to many login attempts", async ({ page }) => { // ✅ DO: use new page, when testing e.g. account lockout |
| 205 | + test.setTimeout(sessionTimeout); // ✅ DO: Set timeout based on actual wait times |
| 206 | + const context = createTestContext(page); |
| 207 | + |
| 208 | + // ... |
| 209 | + |
| 210 | + await step("Wait for code expiration & verify timeout behavior")(async () => { |
| 211 | + await page.goto("/login/verify"); |
| 212 | + await page.waitForTimeout(codeValidationTimeout); // ✅ DO: Use actual waits in @slow tests |
| 213 | + |
| 214 | + await expect(page.getByText("Your verification code has expired")).toBeVisible(); |
| 215 | + })(); |
| 216 | + }); |
| 217 | +}); |
| 218 | +``` |
| 219 | + |
| 220 | +```typescript |
| 221 | +test.describe("@security", () => { // ❌ DON'T: Don't invent new tags - use @smoke, @comprehensive, @slow only |
| 222 | + test("should handle login", async ({ page }) => { |
| 223 | + // ❌ DON'T: Skip createTestContext(page); step |
| 224 | + page.setDefaultTimeout(5000); // ❌ DON'T: Set timeouts manually - use global config |
| 225 | + |
| 226 | + // ❌ DON'T: Use test/assertion prefixes in step descriptions |
| 227 | + await step("Test login functionality")(async () => { // ❌ Should be "Submit login form & verify authentication" |
| 228 | + await step("Verify button is visible")(async () => { // ❌ Should be "Navigate to page & verify button is visible" |
| 229 | + await step("Check user permissions")(async () => { // ❌ Should be "Click user menu & verify permissions" |
| 230 | + if (page.url().includes("/login/verify")) { // ❌ DON'T: Add conditional logic - tests should be linear |
| 231 | + await page.waitForTimeout(2000); // ❌ DON'T: Add manual timeouts |
| 232 | + // Continue with verification... // ❌ DON'T: Write verbose explanatory comments |
| 233 | + } |
| 234 | + |
| 235 | + await page.click("#submit-btn"); // ❌ DON'T: Use CSS selectors - use semantic selectors |
| 236 | + |
| 237 | + // ❌ DON'T: Skip assertions for side effects |
| 238 | + })(); |
| 239 | + |
| 240 | + // ❌ DON'T: Use regular expressions - use simple string matching instead |
| 241 | + await expect(page.getByText(/welcome.*home/i)).toBeVisible(); // ❌ Should be: page.getByText("Welcome home") |
| 242 | + await expect(page.locator('input[name*="email"]')).toBeFocused(); // ❌ Should be: page.getByLabel("Email") |
| 243 | + }); |
| 244 | + |
| 245 | + // ❌ DON'T: Place assertions outside test functions |
| 246 | + expect(page.url().includes("/admin") || page.url().includes("/login")).toBeTruthy(); // ❌ DON'T: Use ambiguous assertions |
| 247 | + |
| 248 | + // ❌ DON'T: Use try/catch to handle flaky behavior - makes tests unreliable |
| 249 | + try { |
| 250 | + await page.waitForLoadState("networkidle"); // ❌ DON'T: Add timeout logic in tests |
| 251 | + await page.getByRole("button", { name: "Submit" }).click({ timeout: 1000 }); // ❌ DON'T: Add timeouts to actions |
| 252 | + } catch (error) { |
| 253 | + await page.waitForTimeout(1000); // ❌ DON'T: Add manual waits |
| 254 | + console.log("Retrying..."); // ❌ DON'T: Add custom error handling |
| 255 | + } |
| 256 | +}); |
| 257 | + |
| 258 | +// ❌ DON'T: Create tests without proper organization |
| 259 | +test("isolated test without describe block", async ({ page }) => { |
| 260 | + // ❌ DON'T: Violates organization rules |
| 261 | +}); |
| 262 | +``` |
0 commit comments