From 6a027b03f13728d149cf1c04a2d42d7f552ae3e4 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Thu, 4 Jun 2026 16:46:17 -0700 Subject: [PATCH] fix(test): mock /api/nodes/search in home-coverage E2E (closes #1313) (#1584) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Mock `/api/nodes/search` at the Playwright level in `test-home-coverage-e2e.js` so the home-coverage E2E search-suggestions step renders deterministically. ## Why The `step('search input renders suggestions for a 1-char query', …)` block was previously softened to a no-op (`pickAnyPubkey` + a `console.log('SKIP …')`) because the live fetch path flakes on cold CI: `home.js`'s `setupSearch` wraps `/api/nodes/search` in a try/catch that swallows network errors, so the dropdown's `.open` class never gets added and the `waitForSelector('.home-suggest.open')` hung. Per the triage fix path on #1313, install `page.route('**/api/nodes/search**', …)` to fulfill a deterministic JSON body and restore the real assertions. ## Red → Green - **Red commit `d062b35`** — adds the assertion (type into `#homeSearch`, wait for `.home-suggest.open`, assert ≥ 1 `.suggest-item` AND that `HomeFlakeFix-1313` is among the rendered names) **without** the `page.route` mock. The live fixture nodes don't include that sentinel name → `assert(names.includes(FIXTURE_NAME))` fires deterministically. This proves the test is meaningful and reaches the assertion (no build/import error). - **Green commit `9fc265a`** — installs the `page.route` handler returning `{ nodes: [{ public_key: , name: 'HomeFlakeFix-1313', role: 'companion' }] }`. The dropdown renders the sentinel name → assertion passes. A real fixture pubkey is reused (via `pickAnyPubkey`) so downstream steps that hit `/api/nodes//health` still see a valid backend response. E2E assertion added: `test-home-coverage-e2e.js:115-133`. ## Scope Test-only. No production code changed. Bonus suggestion in the issue body about adding a visible error state to `home.js`'s search catch branch is out of scope here — file separately if desired. Closes #1313 --------- Co-authored-by: mc-bot --- test-home-coverage-e2e.js | 53 +++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/test-home-coverage-e2e.js b/test-home-coverage-e2e.js index 4436930a..20968767 100644 --- a/test-home-coverage-e2e.js +++ b/test-home-coverage-e2e.js @@ -100,20 +100,51 @@ async function pickAnyPubkey(page) { }); // ── 2. Search flow ── + // Mock /api/nodes/search so the suggestion dropdown render is exercised + // against a deterministic response (issue #1313 — the live fetch path + // flakes intermittently on cold CI; home.js's try/catch swallows the + // error and the dropdown never opens, hanging the test). + // Use a REAL fixture pubkey (so downstream /api/nodes//health calls + // succeed) but pin a sentinel display name we can assert on. + const _fixtureNode = await pickAnyPubkey(page); + assert(_fixtureNode && _fixtureNode.public_key, + 'fixture must expose at least one node with a public_key (got ' + JSON.stringify(_fixtureNode) + ')'); + const FIXTURE_PUBKEY = _fixtureNode.public_key; + const FIXTURE_NAME = 'HomeFlakeFix-1313'; + // Register the route BEFORE the typing step (and BEFORE we even reach + // the step body, so there is zero chance the keyup→debounce→fetch + // beats the route handler being attached). PR #1584 originally + // registered this inside the step; under cold-CI load the + // page.route() promise occasionally lost the race with the input + // event, leaving the fetch to hit the real /api/nodes/search and + // depending on whether the fixture had a match for 'h' the dropdown + // could end up empty or never open — see #1313. + await page.route('**/api/nodes/search**', (route) => route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + nodes: [{ public_key: FIXTURE_PUBKEY, name: FIXTURE_NAME, role: 'companion' }], + }), + })); let pickedPubkey = null; let pickedName = null; await step('search input renders suggestions for a 1-char query', async () => { - // Populate pickedPubkey/pickedName so later steps (claim, My Mesh, etc.) - // still have a node to work with. The actual UI assertion is skipped — - // the /api/nodes/search fetch path is flaky in CI (home.js's try/catch - // swallows errors and never adds `.home-suggest.open`, causing the wait - // to time out). Proper fix needs Playwright-level page.route() mocking. - // See issue #1313. - const node = await pickAnyPubkey(page); - assert(node, 'fixture must have at least one node'); - pickedPubkey = node.public_key; - pickedName = node.name || ''; - console.log('SKIP: search test — flaky API fetch path, see issue #1313'); + // Make sure home.js has finished its async init (loadStats fetch + // resolves, setupSearch has bound the input listener) before we + // type. Otherwise the very first keystroke can fire before the + // 'input' handler is attached and the debounce timer never starts. + await page.waitForLoadState('networkidle'); + const input = await page.waitForSelector('#homeSearch', { timeout: 5000 }); + await input.click(); + await input.type('h', { delay: 20 }); + await page.waitForSelector('.home-suggest.open', { timeout: 5000 }); + const items = await page.$$('.suggest-item'); + assert(items.length >= 1, 'expected >=1 .suggest-item, got ' + items.length); + const names = await page.$$eval('.suggest-item .suggest-name', els => els.map(e => e.textContent)); + assert(names.includes(FIXTURE_NAME), + 'expected suggestion list to include mocked name "' + FIXTURE_NAME + '", got ' + JSON.stringify(names)); + pickedPubkey = FIXTURE_PUBKEY; + pickedName = FIXTURE_NAME; }); await step('claim button adds a node to My Mesh (localStorage)', async () => {