From 8c3b2e22489973ee8a3cdf0d0a666f450a41e57a Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Thu, 30 Apr 2026 20:46:03 -0700 Subject: [PATCH] test(e2e): retry click on table rows when handles detach (#943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem E2E test `Node detail loads` intermittently fails with: > elementHandle.click: Element is not attached to the DOM (e.g. PR #938 CI run job 73889426640.) Same flake class as #ngStats hydration race fixed in #940. ## Root cause ```js const firstRow = await page.$('table tbody tr'); await firstRow.click(); ``` Between the `$()` and `.click()`, the nodes table re-renders from a WebSocket push. The captured handle is detached from the new DOM, click throws. ## Fix Switch to a selector-based click with a small retry loop (3 attempts × 200ms backoff), so a detach mid-attempt re-resolves a fresh element. Test logic unchanged; just defensive against re-render between query and click. Co-authored-by: Kpa-clawbot --- test-e2e-playwright.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test-e2e-playwright.js b/test-e2e-playwright.js index 7c3a3de4..5077f5d0 100644 --- a/test-e2e-playwright.js +++ b/test-e2e-playwright.js @@ -224,10 +224,22 @@ async function run() { // Test 5: Node detail loads (reuses nodes page from test 2) await test('Node detail loads', async () => { await page.waitForSelector('table tbody tr'); - // Click first row - const firstRow = await page.$('table tbody tr'); - assert(firstRow, 'No node rows found'); - await firstRow.click(); + // Use a stable selector + retry-on-detach pattern. Querying a row handle + // and clicking it later races with WebSocket-driven table re-renders that + // detach the original element. Click via a fresh selector each time and + // retry on the "not attached" error. + let lastErr; + for (let attempt = 0; attempt < 3; attempt++) { + try { + await page.click('table tbody tr:first-child', { timeout: 2000 }); + lastErr = null; + break; + } catch (err) { + lastErr = err; + await page.waitForTimeout(200); + } + } + if (lastErr) throw lastErr; // Wait for detail pane to appear await page.waitForSelector('.node-detail'); const html = await page.content();