- Added clarity and structure to README files, including recent updates and getting started sections. - Improved manual verification documentation for CrowdSec authentication, emphasizing expected outputs and success criteria. - Updated debugging guide with detailed output examples and automatic trace capture information. - Refined best practices for E2E tests, focusing on efficient polling, locator strategies, and state management. - Documented triage report for DNS Provider feature tests, highlighting issues fixed and test results before and after improvements. - Revised E2E test writing guide to include when to use specific helper functions and patterns for better test reliability. - Enhanced troubleshooting documentation with clear resolutions for common issues, including timeout and token configuration problems. - Updated tests README to provide quick links and best practices for writing robust tests.
15 KiB
Debug Logging in Action: How to Diagnose Test Failures
This document explains how the new comprehensive debugging infrastructure helps diagnose the E2E test failures with concrete examples.
What Changed: Before vs. After
BEFORE: Generic Failure Output
✗ [chromium] › tests/settings/account-settings.spec.ts › should validate certificate email format
Timeout 30s exceeded, waiting for expect(locator).toBeDisabled()
at account-settings.spec.ts:290
Problem: No information about:
- What page was displayed when it failed
- What network requests were in flight
- What the actual button state was
- How long the test ran before timing out
AFTER: Rich Debug Logging Output
1. Test Step Logging (From enhanced global-setup.ts)
✅ Global setup complete
🔍 Health Checks:
✅ Caddy admin API health (port 2019) [45ms]
✅ Emergency tier-2 server health (port 2020) [123ms]
✅ Security modules status verified [89ms]
🔓 Security Reset:
✅ Emergency reset via tier-2 server [134ms]
✅ Modules disabled (ACL, WAF, rate-limit, CrowdSec)
⏳ Waiting for propagation... [510ms]
2. Network Activity Logging (From network.ts interceptor)
📡 Network Log (automatic)
────────────────────────────────────────────────────────────
Timestamp │ Method │ URL │ Status │ Duration
────────────────────────────────────────────────────────────
03:48:12.456 │ GET │ /api/auth/profile │ 200 │ 234ms
03:48:12.690 │ GET │ /api/settings │ 200 │ 45ms
03:48:13.001 │ POST │ /api/certificates │ 200 │ 567ms
03:48:13.568 │ GET │ /api/acl/lists │ 200 │ 89ms
03:48:13.912 │ POST │ /api/account/email -PEND...│ 422 │ 234ms ⚠️ ERROR
Key Insight: The 422 error on email update shows the API is rejecting the input, which explains why the button didn't disable—the form never validated successfully.
3. Locator Matching Logs (From debug-logger.ts)
🎯 Locator Actions:
────────────────────────────────────────────────────────────
[03:48:14.123] ✅ getByRole('button', {name: /save certificate/i}) matched [8ms]
-> Elements found: 1
-> Action: click()
[03:48:14.234] ❌ getByRole('button', {name: /save certificate/i}) NOT matched [5000ms+]
-> Elements found: 0
-> Reason: Test timeout while waiting for element
-> DOM Analysis:
- Dialog present? YES
- Form visible? NO (display: none)
- Button HTML: <button disabled aria-label="Save...">
Key Insight: The form wasn't visible in the DOM when the test tried to click the button.
4. Assertion Logging (From debug-logger.ts)
✓ Assert: "button is enabled" PASS [15ms]
└─ Expected: enabled=true
└─ Actual: enabled=true
└─ Element state: aria-disabled=false
❌ Assert: "button is disabled" FAIL [5000ms+]
└─ Expected: disabled=true
└─ Actual: disabled=false
└─ Element state: aria-disabled=false, type=submit, form=cert-form
└─ Form status: pristine (no changes detected)
└─ Validation errors found:
- email: "Invalid email format" (hidden error div)
Key Insight: The validation error exists but is hidden, so the button remains enabled. The test expected it to disable.
5. Timing Analysis (From debug reporter)
📊 Test Timeline:
────────────────────────────────────────────────────────────
0ms │ ✅ Navigate to /account
150ms │ ✅ Fill email field with "invalid@"
250ms │ ✅ Trigger validation (blur event)
500ms │ ✅ Wait for API response
700ms │ ❌ FAIL: Button should be disabled (but it's not)
│ └─ Form validation failed on API side (422)
│ └─ Error message not visible in DOM
│ └─ Button has disabled=false
│ └─ Test timeout after 5000ms of waiting
Key Insight: The timing shows validation happened (API returned 422), but the form didn't update the UI properly.
How to Read the Debug Output in Playwright Report
Step 1: Open the Report
npx playwright show-report
Step 2: Click Failed Test
The test details page shows:
Console Logs Section:
[debug] 03:48:12.456: Step "Navigate to account settings"
[debug] └─ URL transitioned from / to /account
[debug] └─ Page loaded in 1234ms
[debug]
[debug] 03:48:12.690: Step "Fill certificate email with invalid value"
[debug] └─ Input focused [12ms]
[debug] └─ Value set: "invalid@" [23ms]
[debug]
[debug] 03:48:13.001: Step "Trigger validation"
[debug] └─ Blur event fired [8ms]
[debug] └─ API request sent: POST /api/account/email [timeout: 5000ms]
[debug]
[debug] 03:48:13.234: Network Response
[debug] └─ Status: 422 (Unprocessable Entity)
[debug] └─ Body: {"errors": {"email": "Invalid email format"}}
[debug] └─ Duration: 234ms
[debug]
[debug] 03:48:13.500: Error context
[debug] └─ Expected button to be disabled
[debug] └─ Actual state: enabled
[debug] └─ Form validation state: pristine
Step 3: Check the Trace
Click "Trace" tab:
- Timeline: See each action with exact timing
- Network: View all HTTP requests and responses
- DOM Snapshots: Inspect page state at each step
- Console: See browser console messages
Step 4: Watch the Video
The video shows:
- What the user would have seen
- Where the UI hung or stalled
- If spinners/loading states appeared
- Exact moment of failure
Failure Category Examples
Category 1: Timeout Failures
Indicator: Timeout 30s exceeded, waiting for...
Debug Output:
⏱️ Operation Timeline:
[03:48:14.000] ← Start waiting for locator
[03:48:14.100] Network request pending: GET /api/data [+2400ms]
[03:48:16.500] API response received (slow network)
[03:48:16.600] DOM updated with data
[03:48:17.000] ✅ Locator finally matched
[03:48:17.005] → Success after 3000ms wait
Diagnosis: The network was slow (2.4s for a 50KB response). Test didn't wait long enough.
Fix:
await page.waitForLoadState('networkidle'); // Wait for network before assertion
await expect(locator).toBeVisible({timeout: 10000}); // Increase timeout
Category 2: Assertion Failures
Indicator: expect(locator).toBeDisabled() failed
Debug Output:
✋ Assertion failed: toBeDisabled()
Expected: disabled=true
Actual: disabled=false
Button State:
- type: submit
- aria-disabled: false
- form-attached: true
- form-valid: false ← ISSUE!
Form Validation:
- Field 1: ✅ valid
- Field 2: ✅ valid
- Field 3: ❌ invalid (email format)
DOM Inspection:
- Error message exists: YES (display: none)
- Form has error attribute: NO
- Submit button has disabled attr: NO
Likely Cause:
Form validation logic doesn't disable button when form.valid=false
OR error message display doesn't trigger button disable
Diagnosis: The component's disable logic isn't working correctly.
Fix:
// In React component:
const isFormValid = !hasValidationErrors;
<button
disabled={!isFormValid} // ← Double-check this logic
type="submit"
>
Save
</button>
Category 3: Locator Failures
Indicator: getByRole('button', {name: /save/i}): multiple elements found
Debug Output:
🚨 Strict Mode Violation: Multiple elements matched
Selector: getByRole('button', {name: /save/i})
Elements found: 2
[1] ✓ <button type="submit">Save Certificate</button>
└─ Located in: Modal dialog
└─ Visible: YES
└─ Class: btn-primary
[2] ✗ <button type="button">Resave Settings</button>
└─ Located in: Table row
└─ Visible: YES
└─ Class: btn-ghost
Problem: Selector matches both buttons - test can't decide which to click
Solution: Scope selector to dialog context
page.getByRole('dialog').getByRole('button', {name: /save certificate/i})
Diagnosis: Locator is too broad and matches multiple elements.
Fix:
// ✅ Good: Scoped to dialog
await page.getByRole('dialog').getByRole('button', {name: /save certificate/i}).click();
// ✅ Also good: Use .first() if scoping isn't possible
await page.getByRole('button', {name: /save certificate/i}).first().click();
// ❌ Bad: Too broad
await page.getByRole('button', {name: /save/i}).click();
Category 4: Network/API Failures
Indicator: API returned 422 or POST /api/endpoint failed with 500
Debug Output:
❌ Network Error
Request: POST /api/account/email
Status: 422 Unprocessable Entity
Duration: 234ms
Request Body:
{
"email": "invalid@", ← Invalid format
"format": "personal"
}
Response Body:
{
"code": "INVALID_EMAIL",
"message": "Email must contain domain",
"field": "email",
"errors": [
"Invalid email format: missing @domain"
]
}
What Went Wrong:
1. Form submitted with invalid data
2. Backend rejected it (expected behavior)
3. Frontend didn't show error message
4. Test expected button to disable but it didn't
Root Cause:
Error handling code in frontend isn't updating the form state
Diagnosis: The API is working correctly, but the frontend error handling isn't working.
Fix:
// In frontend error handler:
try {
const response = await fetch('/api/account/email', {body});
if (!response.ok) {
const error = await response.json();
setFormErrors(error.errors); // ← Update form state with error
setFormErrorVisible(true); // ← Show error message
}
} catch (error) {
setFormError(error.message);
}
Real-World Example: The Certificate Email Test
Test Code (simplified):
test('should validate certificate email format', async ({page}) => {
await page.goto('/account');
// Fill with invalid email
await page.getByLabel('Certificate Email').fill('invalid@');
// Trigger validation
await page.getByLabel('Certificate Email').blur();
// Expect button to disable
await expect(
page.getByRole('button', {name: /save certificate/i})
).toBeDisabled(); // ← THIS FAILED
});
Debug Output Sequence:
1️⃣ Navigate to /account
✅ Page loaded [1234ms]
2️⃣ Fill certificate email field
✅ Input found and focused [45ms]
✅ Value set to "invalid@" [23ms]
3️⃣ Trigger validation (blur)
✅ Blur event fired [8ms]
📡 API request: POST /api/account/email [payload: {email: "invalid@"}]
4️⃣ Wait for API response
✋ Network activity: Waiting...
✅ Response received: 422 Unprocessable Entity [234ms]
└─ Error: "Email must contain @ domain"
5️⃣ Check form error state
✅ Form has errors: email = "Email must contain @ domain"
✅ Error message DOM element exists
❌ But error message has display: none (CSS)
6️⃣ Wait for button to disable
⏰ [03:48:14.000] Start waiting for button[disabled]
⏰ [03:48:14.500] Still waiting...
⏰ [03:48:15.000] Still waiting...
⏰ [03:48:19.000] Still waiting...
❌ [03:48:24.000] TIMEOUT after 10000ms
Button Found:
- HTML: <button type="submit" class="btn-primary">Save</button>
- Attribute disabled: MISSING (not disabled!)
- Aria-disabled: false
- Computed CSS: pointer-events: auto (not disabled)
Form State:
- Validation errors: YES (email invalid)
- Button should disable: YES (by test logic)
- Button actually disabled: NO (bug!)
🔍 ROOT CAUSE:
The form disables the button in HTML, but the CSS is hiding the error
message and not calling setState to disable the button. This suggests:
1. Form validation ran on backend (API returned 422)
2. Error wasn't set in React state
3. Button didn't re-render as disabled
LIKELY CODE BUG:
- Error response not processed in catch/error handler
- setFormErrors() not called
- Button disable logic checks form.state.errors but it's empty
How to Fix:
- Check the
Account.tsxform submission error handler - Ensure API errors update form state:
setFormErrors(response.errors) - Ensure button disable logic:
disabled={Object.keys(formErrors).length > 0} - Verify error message shows:
{formErrors.email && <p>{formErrors.email}</p>}
Interpreting the Report Summary
After tests complete, you'll see:
⏱️ Slow Tests (>5s):
────────────────────────────────────────────────────────────
1. test name [16.25s] ← Takes 16+ seconds to run/timeout
2. test name [12.61s] ← Long test setup or many operations
...
🔍 Failure Analysis by Type:
────────────────────────────────────────────────────────────
timeout │ ████░░░░░░░░░░░░░░░░ 4/11 (36%)
│ Action: Add waits, increase timeouts
│
assertion │ ███░░░░░░░░░░░░░░░░░ 3/11 (27%)
│ Action: Check component state logic
│
locator │ ██░░░░░░░░░░░░░░░░░░ 2/11 (18%)
│ Action: Make selectors more specific
│
other │ ██░░░░░░░░░░░░░░░░░░ 2/11 (18%)
│ Action: Review trace for error details
What this tells you:
- 36% Timeout: Network is slow or test expectations unrealistic
- 27% Assertion: Component behavior wrong (disable logic, form state, etc.)
- 18% Locator: Selector strategy needs improvement
- 18% Other: Exceptions or edge cases (need to investigate individually)
Next Steps When Tests Complete
- Run the tests: Already in progress ✅
- Open the report:
npx playwright show-report - For each failure:
- Click test name
- Read the assertion error
- Check the console logs (our debug output)
- Inspect the trace timeline
- Watch the video
- Categorize the failure: Timeout? Assertion? Locator? Network?
- Apply the appropriate fix based on the category
- Re-run just that test:
npx playwright test --grep "test name" - Validate: Confirm test now passes
The debugging infrastructure gives you everything you need to understand exactly why each test failed and what to fix.