# Playwright tests (Aether) Quick guide for running and editing the Playwright tests in this repo. Running tests - Run the full test suite (uses `playwright.config.ts`): ```bash npx playwright test -c playwright.config.ts ``` - Run a single test file: ```bash npx playwright test tests/path/to/file.test.ts -c playwright.config.ts ``` - Run a single test by title (grep): ```bash npx playwright test -g "Badge - interaction" -c playwright.config.ts ``` Notes - Tests in `tests/disabled/` are ignored by default (see `playwright.config.ts`). Move flaky or environment-dependent tests there. - The runner starts a local dev server via `npm run dev` by default (see `playwright.config.ts:webServer`). Ensure the app can start on port `5173` or update the config. Writing / modifying tests - Tests are TypeScript files under `tests/` and should export Playwright `test` blocks. Example header: ```ts import { test, expect } from '@playwright/test'; test('example', async ({ page }) => { await page.goto('/'); await expect(page).toHaveTitle(/OSIT/); }); ``` - Use `page.route('**/v3/**', handler)` to mock backend responses for deterministic tests. - Use `page.addInitScript` to inject `ae_loc` localStorage defaults when tests need authenticated/admin state. Adding new tests - Create a new file `tests/my_feature.test.ts`. - Keep tests focused and deterministic: mock network calls and avoid relying on external services. - Place environment-sensitive tests in `tests/disabled/` so they are not run in CI by default. Committing - Stage and commit test changes as usual. Example: ```bash git add tests/ git commit -m "test: add " ``` Help - If a test fails due to external network calls or platform-specific behavior, try mocking the relevant endpoints and move the test to `tests/disabled` if it cannot be made deterministic. --- ## Deep Dive: Testing IDAA Edit Form Components This section documents hard-won lessons from writing `idaa_recovery_meeting_edit.test.ts`. These issues are not obvious and cost significant debugging time — read this before writing tests for any Svelte 5 form component. ### The #1 Trap: HTML5 Form Validation Silently Blocks Submission **Symptom:** `page.waitForRequest(...)` times out after clicking the submit button. No network request appears. No JS error is thrown. The button click registers in Playwright, but nothing happens. **Cause:** A form field with `required` has `value=""` at the moment the button is clicked. The browser's native HTML5 form validation cancels `onsubmit` *before* any JavaScript runs — including Svelte's event handler. Zero network activity. Zero error output. **How to diagnose:** ```typescript // Add this before the click to log ALL outgoing requests: page.on('request', r => console.log(`REQ: ${r.method()} ${r.url()}`)); await page.locator('button[type="submit"]').first().click(); // If nothing is printed after the click, HTML5 validation is the culprit. ``` **How to find the empty required field:** ```typescript // Run in browser via page.evaluate(): const bad = [...document.querySelectorAll('[required]')].filter(el => !el.value); console.log(bad.map(el => el.name || el.id)); ``` --- ### The Svelte 5 `value=` One-Time Bind Trap **Symptom:** A `` renders with the correct value in the browser, but in Playwright the field is empty. **Cause:** In Svelte 5, `value={someReactiveExpr}` on an uncontrolled `` or ` ← renders with value ✓ {:else} ← renders with value="" ✗ {/if} ``` In tests, `$ae_loc.lu_time_zone_list` was populated *after* mount by an API call, so the `{:else}` branch rendered first with `value=""` — then the store updated and switched to ` has options) await page.waitForFunction(() => { try { const sub = localStorage.getItem('lu_country_subdivision_list'); const cty = localStorage.getItem('lu_country_list'); return sub !== null && JSON.parse(sub).length > 50 && cty !== null && JSON.parse(cty).length > 50; } catch { return false; } }, { timeout: 10000 }); // Wait for the timezone required field to have a value (submit guard) await page.waitForFunction(() => { const tz = document.querySelector('[name="timezone"]') as HTMLInputElement | null; return tz !== null && tz.value.length > 0; }, { timeout: 10000 }); ``` Use `waitForSelector` only for DOM presence. Use `waitForFunction` when you need a value or condition inside the element. --- ### Capturing PATCH / POST Payloads Register `page.waitForRequest(...)` **before** the click, then `await` the result after: ```typescript async function capture_patch_body(page: any): Promise> { const req_promise = page.waitForRequest( (r: any) => r.url().includes(`/v3/crud/event/${TEST_EVENT_ID}`) && r.method() === 'PATCH', { timeout: 5000 } ); await page.locator('button[type="submit"]').first().click(); const req = await req_promise; return JSON.parse(req.postData() ?? '{}'); } ``` If `waitForRequest` times out, the cause is almost always HTML5 form validation (see top of this section). --- ### Route Mocking Strategy `setup_api_mocks()` accepts flags to selectively mock vs. pass through: - **`pass_through_lookups: true`** — lets country/subdivision/timezone GET calls reach the real API. Preferred for payload-verification tests because the real API returns 50+ entries, naturally satisfying the component's cache threshold without fixture arrays. - **`pass_through_site_domain: true`** — lets the site domain lookup reach the real API so `$ae_api` is built with real credentials. Required when the test needs to send a real PATCH. - Default (both false) — all API calls are intercepted and answered with fixtures. Use for pure UI tests that do not need real data. Mock the event PATCH endpoint to return success without hitting the real database when you only need to verify the payload shape: ```typescript await page.route(`**/v3/crud/event/${event_id}`, async (route) => { if (route.request().method() === 'PATCH') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, id: event_id }) }); } else { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mock_event) }); } }); ``` ## Development / Testing / Demo environment information * Use snake_case (or Snake_Case or Snake_case or test_NASA_example or test_API_key) * Aether test/demo base URL: 'http://demo.localhost:5173' * Aether development API: 'https://dev-api.oneskyit.com' These are IDs for records that we can use for testing. Please do not delete them. They are also used for demo purposes with clients. ### Core Modules * Aether test/demo Account: '_XY7DXtc9MY' (1) "One Sky IT Demo" * Aether test/demo Site: '92vkYC4fVEl' (12) "One Sky IT Demo" * Aether test/demo Site Domain: '_6jcTbnJk-o' (12) "demo.localhost:5173" * Aether test/demo Site Domain: 'heXRgHOs4ns' (30) "sk-demo.oneskyit.com" * Aether test/demo Site Domain: 'DASm8fP92yw' (69) "dev-demo.oneskyit.com" * Aether test/demo Site Domain: '2i_0Za6yRPo' (2) "demo.oneskyit.com" * Aether test/demo Person: 'QWODAPCNLQU' (49) "Osiris Idem" * Aether test/demo Person: 'HMQRNPIXQMK' (48) "Cleo Idem" ### Events Modules * Aether test/demo Event: 'pjrcghqwert' (1) "Demo One Sky IT Conference" * Aether test/demo Event Session: 'DOW3h7v6H42' (703) "How To Do Things" * Aether test/demo Event Presentation: '7U2eXSjR6H4' (1670) "Build a House" * Aether test/demo Event Presenter: 'gT-hxnifb-0' (2202) "Bob The Builder" * Aether test/demo Event File: 'OOsHXtng5mr' (2985) "1 Quick Test for macOS.mp4" * Aether test/demo Event Badge: 'UIJT-73-63-61' (37163) "Scott Idem" * Aether test/demo Event Person: 'ffkKxiHpOEC' (16603) "Scott Idem" * Aether test/demo Event Badge Template: 'jgfixEpYp1B' (18) "Dev Demo 202x" * Aether test/demo Event Badge Template: 'rzmUgsk7mkq' (19) "Dev Demo 202x Workshops" * Aether test/demo Event Location: 'VXXY-98-46-14' (26) "Ballroom 1" * Aether test/demo Event Location: 'FGRN-67-92-45' (298) "Ballroom AB" * Aether test/demo Event Location: 'PQKB-15-39-81' (78) "Poster Display Station A" ### Journals Module * Aether test/demo Journal: 'BVYE-94-46-29' (42) "Testing Things" * Aether test/demo Journal Entry: 'xRx-Y4-h3-fU' (233) "Another Journal Entry in the Test Journal" ### Archives Module (IDAA Archives) * Aether test/demo Archive: 'nAA2bHLv8RK' (1) "One Sky Test Archive" * Aether test/demo Archive Content: 'UjKzrk-GKu5' (1) "Hosted File Test" ### Posts Module (IDAA Bulletin Board) * Aether test/demo Post: * Aether test/demo Post: ### Events Module (IDAA Recovery Meetings) * Aether test/demo Event: '1Pkd025vvxU' (36) "IDAA Recovery Meeting Test" * Aether test/demo Event: 'gIZgAjISkf8' (43) "IDAA Recovery Meeting Test"