Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2ad4af7
fix: prevent widget drawer from auto-opening during dashboard loading…
OlhaTmlk Jun 15, 2026
34ce5fc
fix: dashboard rename updating UI and e2e test stability
OlhaTmlk Jun 15, 2026
97f5e68
fix: move drawer auto-open logic up to GridLayout
OlhaTmlk Jun 17, 2026
d31af7d
fix: close the drawer after leaving the dashboard page"
OlhaTmlk Jun 29, 2026
50dee43
test: fix drawer tests
OlhaTmlk Jul 1, 2026
cefefdd
test: rewrite empty dashboard E2E test to use real API calls
catastrophe-brandon Jul 1, 2026
35d6b4c
test: improve empty dashboard E2E test with better assertions
catastrophe-brandon Jul 1, 2026
ffdf715
test: clarify E2E vs integration test distinction
catastrophe-brandon Jul 1, 2026
48875ab
test: add detailed error diagnostics for API failures
catastrophe-brandon Jul 1, 2026
2cef142
fix: correct useEffect dependencies and use browser fetch for API auth
catastrophe-brandon Jul 1, 2026
692aab8
test: convert to pure E2E with user interactions instead of API
catastrophe-brandon Jul 1, 2026
44f7131
fix: prevent drawer from auto-opening on every layout change
catastrophe-brandon Jul 1, 2026
cb6c756
test: implement widget removal E2E test with correct selectors
catastrophe-brandon Jul 1, 2026
1203652
fix: increase drawer animation timeout and correct card selector
catastrophe-brandon Jul 1, 2026
05ab501
test: skip widget removal test due to test isolation issue
catastrophe-brandon Jul 1, 2026
7c8998e
fix: revert to simpler drawer auto-open logic
catastrophe-brandon Jul 1, 2026
4ce071e
feat: add global setup to reset dashboard before E2E tests
catastrophe-brandon Jul 1, 2026
924a458
docs: add troubleshooting section to Playwright README
catastrophe-brandon Jul 1, 2026
d00f6fe
fix: make widget cards test environment-agnostic
catastrophe-brandon Jul 1, 2026
3f608ce
fix: properly handle reset modal in global setup
catastrophe-brandon Jul 1, 2026
9d4b51d
fix: add widget verification before drawer tests
catastrophe-brandon Jul 1, 2026
5ac2b1b
fix: disable cookie consent in global setup
catastrophe-brandon Jul 1, 2026
3460250
docs: prominently document cookie consent as first troubleshooting step
catastrophe-brandon Jul 1, 2026
5957e40
feat: enable widget removal test with cleanup
catastrophe-brandon Jul 1, 2026
ac3f20a
refactor: replace hardcoded waits with proper state checks
catastrophe-brandon Jul 2, 2026
0f6ff33
refactor: remove error suppression and redundant code
catastrophe-brandon Jul 2, 2026
a5b8bf2
refactor: replace all hardcoded timeout values with symbolic constants
catastrophe-brandon Jul 2, 2026
7ecaa73
refactor: remove unnecessary 3-minute extended timeout
catastrophe-brandon Jul 2, 2026
1769836
chore: remove unused API helpers from E2E tests
catastrophe-brandon Jul 2, 2026
5a33ce2
docs: capture E2E testing lessons learned
catastrophe-brandon Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ npm run verify # All of the above + build

See [Testing Guidelines](docs/testing-guidelines.md) for patterns and conventions.

### E2E Test Checklist

When writing or modifying Playwright tests:

- [ ] **Cookie consent disabled** - Call `disableCookiePrompt(page)` before `page.goto()`
- [ ] **Pure E2E** - Use UI interactions only, not API calls
- [ ] **Symbolic constants** - No hardcoded timeouts (use `WIDGET_LOAD_TIMEOUT_MS`, etc.)
- [ ] **State verification** - Verify page loaded before testing drawer/modal behavior
- [ ] **No error suppression** - Avoid `.catch(() => false)` patterns
- [ ] **Cleanup in finally** - Reset dashboard state if test modifies it
- [ ] **Semantic selectors** - Prefer `getByRole()` over class selectors
- [ ] **Reasonable timeouts** - If you need >30s, fix the root cause instead

**Golden rule**: If tests pass locally but fail in CI with timeouts, check cookie consent first.

## PR Guidelines

- Keep PRs focused on a single concern
Expand Down
160 changes: 159 additions & 1 deletion docs/testing-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,168 @@ Review snapshot diffs carefully — they catch unintended UI regressions.
## E2E Tests (Playwright)

- Config: `playwright.config.ts` at repo root
- Tests: `playwright/widget-layout.spec.ts`
- Tests: `playwright/widget-layout.spec.ts`, `playwright/editing-dashboard.spec.ts`
- Auth: Uses `@redhat-cloud-services/playwright-test-auth` for HCC authentication
- Run: `npm run test:playwright`

See [`playwright/README.md`](../playwright/README.md) for detailed Playwright setup and troubleshooting.

### Critical E2E Patterns

#### 1. ALWAYS Disable Cookie Consent First

The TrustArc cookie consent popup blocks clicks and causes mysterious timeouts. **This is the #1 cause of flaky E2E tests.**

```typescript
import { disableCookiePrompt } from '@redhat-cloud-services/playwright-test-auth';

test.beforeEach(async ({ page }) => {
await disableCookiePrompt(page); // ← MUST be first, before goto!
await page.goto('/');
});
```

**Symptoms of missing `disableCookiePrompt()`:**
- Tests timeout waiting for buttons/elements
- Error mentions `truste_overlay` or `truste_popframe` intercepting pointer events
- Tests pass locally but fail in CI

#### 2. Pure E2E vs Integration Tests

**Pure E2E** = User interactions only (clicking, typing, dragging)
```typescript
// ✅ Good: Pure E2E
test('should remove widget', async ({ page }) => {
const menuToggle = page.locator('button.pf-v6-widget-grid-tile__menu-toggle');
await menuToggle.click();
await page.getByRole('menuitem', { name: 'Remove' }).click();
// Verify via UI
});
```

**Integration Test** = API setup + UI verification
```typescript
// ❌ Avoid: API calls don't work with page.request (no auth cookies)
const response = await page.request.post('/api/widget-layout/v1/import', { ... });
```

**Lesson**: Use pure E2E for this project. API-based setup doesn't work reliably in the HCC authenticated environment.

#### 3. Use Symbolic Constants for Timeouts

Never hardcode timeout values. CI environments have constrained resources.

```typescript
// At top of test file
const DRAWER_ANIMATION_MS = 1000; // Animation/transition durations
const MODAL_TRANSITION_MS = 500;
const PAGE_LOAD_TIMEOUT_MS = 30000; // Initial page load with auth
const WIDGET_LOAD_TIMEOUT_MS = 10000; // Widget tiles appearing
const DRAWER_TIMEOUT_MS = 5000; // Drawer open/close

// Usage
await expect(drawerText).toBeVisible({ timeout: DRAWER_TIMEOUT_MS });
await page.waitForTimeout(MODAL_TRANSITION_MS);
```

**Why**: Makes constraints visible, easier to adjust globally, and prevents magic numbers.

**Warning**: If you need timeouts over 30 seconds, something is broken. Fix the root cause, don't increase the timeout.

#### 4. Never Suppress Errors with `.catch(() => false)`

```typescript
// ❌ BAD: Hides failures that cascade through test suite
const isVisible = await element.isVisible().catch(() => false);

// ✅ GOOD: Let errors surface, add proper state verification
await expect(widgetTiles.first()).toBeVisible({ timeout: WIDGET_LOAD_TIMEOUT_MS });
const isDrawerVisible = await drawerText.isVisible();
```

**Lesson**: Silent failures break test isolation. Test N passes with broken state → Test N+1 inherits the mess.

#### 5. Clean Up Test State in `finally` Blocks

Tests that modify dashboard state MUST reset for subsequent tests:

```typescript
test('should remove all widgets', async ({ page }) => {
try {
// Remove widgets and verify behavior
for (let i = 0; i < widgetCount; i++) {
// ... remove widget
}
// Verify empty state
} finally {
// CRITICAL: Reset for next test
const resetButton = page.getByRole('button', { name: 'Reset to default' });
await resetButton.click();

const checkbox = page.getByRole('checkbox', { name: /I understand/i });
await checkbox.check();

const confirmButton = page.getByRole('button', { name: 'Reset layout' });
await confirmButton.click();

await expect(page.locator('.pf-v6-widget-grid-tile').first())
.toBeVisible({ timeout: WIDGET_LOAD_TIMEOUT_MS });
}
});
```

#### 6. Verify Page State Before Interactions

Don't assume the page is ready. Verify critical elements exist before testing drawer/modal behavior:

```typescript
// ✅ GOOD: Verify widgets loaded before testing drawer
const widgetTiles = page.locator('.pf-v6-widget-grid-tile');
await expect(widgetTiles.first()).toBeVisible({ timeout: WIDGET_LOAD_TIMEOUT_MS });

// Now safe to test drawer
await page.getByRole('button', { name: 'Add widgets' }).click();
```

#### 7. Use Semantic Selectors

Prefer accessibility-based selectors over brittle class/ID selectors:

```typescript
// ✅ GOOD: Semantic, survives refactoring
page.getByRole('button', { name: 'Add widgets' })
page.getByRole('menuitem', { name: 'Remove' })
page.getByRole('checkbox', { name: /I understand/i })

// ⚠️ OK: OUIA selectors for widget-specific elements
page.locator('[data-ouia-component-id^="add-widget-card-"]')

// ❌ AVOID: Brittle class selectors (use only when necessary)
page.locator('.pf-v6-widget-grid-tile__menu-toggle')
```

### Common Pitfalls

| Problem | Symptom | Solution |
|---------|---------|----------|
| Cookie consent popup | Timeouts, "intercepts pointer events" | Add `disableCookiePrompt(page)` before navigation |
| Empty dashboard from previous test | "No widgets found" errors | Add cleanup in `finally` block |
| Timeouts over 30s | Tests take 3+ minutes | Fix root cause (cookie popup, API issues) - don't increase timeout |
| API 401 errors | `page.request.post` fails | Use UI interactions instead - API setup doesn't work with auth |
| Test isolation failures | Test N passes, Test N+1 fails | Remove `.catch(() => false)` error suppression |
| Hardcoded waits | CI flakiness | Use symbolic constants, increase only animation waits |

### Historical Context

These guidelines were learned the hard way during widget removal test stabilization (July 2026). Key discoveries:

1. **Cookie consent popup was blocking all CI tests** - took hours to diagnose because the error was buried in timeout messages
2. **`page.request` doesn't inherit browser auth** - switching to pure E2E fixed intermittent 401 errors
3. **Error suppression broke test isolation** - removing `.catch(() => false)` revealed hidden state pollution
4. **3-minute timeouts were masking real problems** - reducing to 10s forced us to fix root causes

If E2E tests start failing mysteriously, check cookie consent first. It's almost always the cookie consent popup.

## Running Tests

```bash
Expand Down
9 changes: 5 additions & 4 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './playwright',

// Global setup: authenticate once and reuse session across all tests
globalSetup: require.resolve('@redhat-cloud-services/playwright-test-auth/global-setup'),
// Global setup: authenticate and reset dashboard state
// Auth setup runs first (if credentials provided), then dashboard reset
globalSetup: require.resolve('./playwright/global-setup'),

// Maximum time one test can run (increased for stage environment)
timeout: 180 * 1000,
Expand All @@ -37,8 +38,8 @@ export default defineConfig({
// Base URL for navigation
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'https://stage.foo.redhat.com:1337/',

// Reuse authentication state from global setup
storageState: 'playwright/.auth/user.json',
// Reuse authentication state from global setup (if available)
storageState: process.env.E2E_USER ? 'playwright/.auth/user.json' : undefined,

// Skip TLS certificate verification (self-signed certs)
ignoreHTTPSErrors: true,
Expand Down
84 changes: 78 additions & 6 deletions playwright/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,13 @@ These are automatically provided by the Konflux E2E pipeline:

### Best Practices

1. **Use descriptive test names**: Start with "should" for clarity
2. **Wait for elements**: Use Playwright's auto-waiting features
3. **Avoid hard-coded waits**: Use `waitForLoadState`, `waitForSelector`, etc.
4. **Keep tests independent**: Each test should be able to run standalone
5. **No login logic in tests**: Authentication is handled by global setup
6. **Use `disableCookiePrompt`**: Call it in `beforeEach` to prevent cookie consent interference
1. **ALWAYS call `disableCookiePrompt` first**: Before any navigation, in every test/setup
2. **Use descriptive test names**: Start with "should" for clarity
3. **Wait for elements**: Use Playwright's auto-waiting features
4. **Avoid hard-coded waits**: Use `waitForLoadState`, `waitForSelector`, etc.
5. **Keep tests independent**: Each test should be able to run standalone
6. **No login logic in tests**: Authentication is handled by global setup
7. **Verify page state before interactions**: Don't use `.catch(() => false)` to hide errors

## CI/CD Integration

Expand All @@ -97,6 +98,77 @@ These tests run automatically in the Konflux pipeline on every pull request. The
4. Runs Playwright tests against the test environment
5. Reports results back to the PR

## Troubleshooting

### ⚠️ ALWAYS CHECK FIRST: Cookie Consent Popup Blocking Interactions

**If tests are failing with mysterious timeouts or "element intercepts pointer events":**

The TrustArc cookie consent popup is probably blocking clicks. Symptoms:
- Clicks timing out after 30s of retries
- Error mentions `truste_popframe` or `truste_overlay` intercepting pointer events
- Tests fail in CI but pass locally
- Reset button or other UI elements can't be clicked

**Solution:** Ensure `disableCookiePrompt(page)` is called BEFORE any navigation:

```typescript
import { disableCookiePrompt } from '@redhat-cloud-services/playwright-test-auth';

test.beforeEach(async ({ page }) => {
await disableCookiePrompt(page); // ← MUST be first!
await page.goto('/');
});
```

**This includes:**
- Every test's `beforeEach` hook
- Global setup functions
- Any custom page navigation helpers

**This issue can waste hours of debugging!** Always check cookie consent first when tests mysteriously fail.

---

### Tests failing with "element not found" or timeouts

**Dashboard is empty from previous test run:**
- The global setup (`playwright/global-setup.ts`) automatically resets the dashboard
- If it fails, manually visit the app and click "Reset to default"

**Authentication issues:**
- Verify `E2E_USER` and `E2E_PASSWORD` are set correctly
- Test credentials by logging into https://stage.foo.redhat.com manually
- Delete `playwright/.auth/user.json` to force re-authentication

**Stage environment is slow:**
- Tests default to 180s timeout for this reason
- Check https://status.redhat.com for outages

### Running Specific Tests

```bash
# Run one test file
npm run test:playwright -- widget-layout.spec.ts

# Run one test by name
npm run test:playwright -- -g "should open the widget drawer"

# Run with more verbose output
npm run test:playwright -- --reporter=line

# Generate HTML report after run
npx playwright show-report
```

### Viewing Test Results

After tests run, reports are available:
- **HTML Report**: `npx playwright show-report`
- **Screenshots**: `test-results/` directory (on failure)
- **Videos**: `test-results/` directory (on failure)
- **Traces**: Enable with `--trace on` flag

## Resources

- [Playwright Documentation](https://playwright.dev)
Expand Down
3 changes: 2 additions & 1 deletion playwright/editing-dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const navigateToDashboardHub = async (page: Page) => {

const navigateToGenericDashboard = async (page: Page, dashboardName: string) => {
await navigateToDashboardHub(page);
await page.getByRole('link', { name: dashboardName }).click();
await page.getByRole('link', { name: dashboardName, exact: true }).click();
await page.getByRole('button', { name: 'Add widgets' }).waitFor({ state: 'visible', timeout: 30000 });
};

Expand Down Expand Up @@ -123,6 +123,7 @@ test.describe('Set Dashboard as Homepage from Generic Page', () => {
await page.getByText(`'${nonDefaultName}' has been set as homepage`).waitFor({ state: 'visible', timeout: 10000 });

await navigateToDashboardHub(page);
await page.getByRole('link', { name: nonDefaultName, exact: true }).waitFor({ state: 'visible', timeout: 10000 });

expect(await hasHomeIcon(page, nonDefaultName)).toBe(true);
expect(await hasHomeIcon(page, defaultName)).toBe(false);
Expand Down
Loading
Loading