Skip to content

Commit 3f48954

Browse files
authored
Add comprehensive end-to-end testing infrastructure with Playwright (#749)
### Summary & Motivation This change establishes a comprehensive end-to-end testing infrastructure using Playwright, providing a robust foundation for testing user workflows across all self-contained systems. The implementation includes authentication fixtures, test utilities, and organized test suites that ensure application functionality works correctly from a user's perspective. Key features: - Shared Playwright configuration and utilities in shared-webapp for consistency across systems - Worker-scoped authentication fixtures enabling parallel test execution with isolated tenants - Reusable test utilities including step decorators, assertions, and test data generators - Organized test flows covering authentication, user management, permissions, themes, localization, and mobile views - Performance optimizations including toast auto-closing and improved error handling - IDE integration for VS Code with debugging support - Separation of smoke tests (@smoke), comprehensive tests (@comprehensive), and slow tests (@slow) The infrastructure supports multiple browsers (Chromium, Firefox, WebKit) and provides detailed test reporting with screenshots and traces on failures. Tests are organized by functional areas and use descriptive step names for maintainability. ### Downstream projects 1. Copy the Playwright test configuration files from Back Office to your self-contained system: - Copy `application/back-office/WebApp/tests/playwright.config.ts` to `your-self-contained-system/WebApp/tests/playwright.config.ts` - Copy `application/back-office/WebApp/tests/tsconfig.json` to `your-self-contained-system/WebApp/tests/tsconfig.json` 2. Update `your-self-contained-system/WebApp/rsbuild.config.ts` to exclude test files from the build: ```diff server: { port: Number.parseInt(process.env.PORT || "9003"), + publicDir: { + name: "public", + copyOnBuild: true + } }, + source: { + exclude: ["**/tests/**/*"] + } ``` 3. Update `your-self-contained-system/WebApp/tsconfig.json` to exclude test files: ```diff "compilerOptions": { // ... existing options }, - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "tests"] ``` 4. Create your first end-to-end test in `your-self-contained-system/WebApp/tests/e2e/homepage.spec.ts` following the pattern from Back Office. ### Checklist - [x] I have added tests, or done manual regression tests - [x] I have updated the documentation, if necessary
2 parents 0c9c100 + 8f07f5e commit 3f48954

Some content is hidden

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

42 files changed

+4956
-21
lines changed
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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+
```
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
description: Workflow for creating end-to-end tests
3+
globs:
4+
alwaysApply: false
5+
---
6+
# E2E Testing Workflow
7+
8+
This workflow guides you through the process of creating comprehensive end-to-end tests for specific features like login and signup. It focuses on identifying what tests to write, planning complex scenarios, and ensuring tests follow the established conventions.
9+
10+
## Workflow
11+
12+
1. Understand the feature under test:
13+
- Study the frontend components and their interactions.
14+
- Review API endpoints and authentication flows.
15+
- Understand validation rules and error handling.
16+
- Identify key user interactions and expected behaviors.
17+
18+
2. Use Browser MCP to explore the webapp functionality:
19+
- Navigate to the application: `mcp0_browser_navigate({ url: "https://localhost:9000" })`.
20+
- Interact with the feature manually to understand user flows.
21+
- Take snapshots to identify UI elements and their structure.
22+
- Document key interactions and expected behaviors.
23+
- Note any edge cases or potential issues discovered during exploration.
24+
25+
3. Review existing test examples:
26+
- Read [End-to-End Tests](/.cursor/rules/end-to-end-tests/e2e-tests.mdc) for detailed information.
27+
- Examine [signup.spec.ts](/application/account-management/WebApp/tests/e2e/signup.spec.ts) and [login.spec.ts](/application/account-management/WebApp/tests/e2e/login.spec.ts) for inspiration.
28+
- Note the structure, assertions, test organization, and the "Act & Assert:" comment format.
29+
30+
4. Plan comprehensive test scenarios:
31+
- Identify standard user journeys through the feature.
32+
- Plan for complex multi-session scenarios like:
33+
- Concurrent sessions: What happens when a user has two tabs open?
34+
- Cross-session state changes: What happens when state changes in one session affect another?
35+
- Authentication conflicts: How does the system handle authentication changes across sessions?
36+
- Form submissions across sessions: What happens with concurrent form submissions?
37+
- Antiforgery token handling: How are antiforgery tokens managed across tabs?
38+
- Browser navigation: Back/forward buttons, refresh, direct URL access.
39+
- Network conditions: Slow connections, disconnections during operations.
40+
- Input validation: Boundary values, special characters, extremely long inputs.
41+
- Accessibility: Keyboard navigation, screen reader compatibility.
42+
- Localization: Testing with different languages and formats.
43+
44+
5. Categorize tests appropriately:
45+
- `@smoke`: Essential functionality that will run on deployment of any system.
46+
- Create one comprehensive smoke.spec.ts per self-contained system.
47+
- Test complete user journeys: signup → profile setup → invite users → manage roles → tenant settings → logout.
48+
- Include validation errors, retries, and recovery scenarios within the journey.
49+
- `@comprehensive`: More thorough tests covering edge cases that will run on deployment of the system under test.
50+
- Focus on specific feature areas with deep testing of edge cases.
51+
- Group related scenarios to minimize test count while maximizing coverage.
52+
- `@slow`: Tests involving timeouts or waiting periods that will run ad-hoc, when features under test are changed.
53+
54+
6. Create or update test structure:
55+
- For smoke tests: Create/update `application/[scs-name]/WebApp/tests/e2e/smoke.spec.ts`.
56+
- For comprehensive tests: Create feature-specific files like `user-management.spec.ts`, `authentication.spec.ts`.
57+
- Avoid creating many small, isolated tests - prefer comprehensive scenarios that test multiple aspects.
58+
59+
## Key principles
60+
61+
- Comprehensive coverage: Test all critical paths and important edge cases.
62+
- Follow conventions: Adhere to the established patterns in [End-to-End Tests](/.cursor/rules/end-to-end-tests/e2e-tests.mdc).
63+
- Clear organization: Properly categorize tests and use descriptive names.
64+
- Realistic user journeys: Test scenarios that reflect actual user behavior.

.cursor/rules/workflows/implement-product-increment.mdc

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@ Follow these steps which describe in detail how you must implement the tasks in
1717

1818
Before implementing each task, review the relevant rules thoroughly:
1919

20-
- For **backend tasks**:
21-
- Review all the [Backend](mdc:.cursor/rules/backend) rule files.
22-
- For **frontend tasks**:
23-
- Review all the [Frontend](mdc:.cursor/rules/frontend) rule files.
20+
- For **backend tasks**:
21+
- Review all the [Backend](mdc:.cursor/rules/backend) rule files.
22+
- For **frontend tasks**:
23+
- Review all the [Frontend](mdc:.cursor/rules/frontend) rule files.
24+
- For **end-to-end tests**:
25+
- Review the [E2E Testing Workflow](mdc:.cursor/rules/workflows/create-e2e-tests.mdc) and all the [End-to-End Tests](mdc:.cursor/rules/end-to-end-tests) rule files.
26+
- For **Developer CLI commands**:
27+
- Review all the [Developer CLI](mdc:.cursor/rules/developer-cli) rule files.
2428

2529
These rules define the conventions and must be strictly adhered to during implementation.
2630

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,3 +400,10 @@ dist/
400400

401401
# Git submodules
402402
.gitmodules
403+
404+
# Playwright E2E testing artifacts
405+
test-results/
406+
playwright-report/
407+
**/playwright/.cache/
408+
**/.auth/
409+

.vscode/extensions.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
22
"recommendations": [
3+
"biomejs.biome",
34
"bradlc.vscode-tailwindcss",
45
"ms-azuretools.vscode-bicep",
5-
"github.vscode-github-actions",
6-
"biomejs.biome",
6+
"ms-playwright.playwright",
7+
"github.vscode-github-actions"
78
]
89
}

0 commit comments

Comments
 (0)