From 87ba70acd6ea0770ba446b0766f7970172e9e107 Mon Sep 17 00:00:00 2001 From: Shadowbroker <43977454+BigBodyCobain@users.noreply.github.com> Date: Wed, 20 May 2026 21:13:40 -0600 Subject: [PATCH] test: deflake messagesViewFirstContact remove-contact test (#264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This test asserts that clicking "Remove" on a contact: 1. Surfaces a toast "Removed contact: ." 2. Drops the contact from the visible list The Remove handler in MessagesView dispatches a tight cluster of React state updates in one event tick: removeContact(peerId) locallySavedContactIdsRef.current.delete(peerId) setContacts(...) setComposeError('') setComposeStatus(`Removed contact: ${displayNameForPeer(...)}.`) Locally those updates settle in <100ms and the toast appears under any findByText default. Under GitHub Actions runner load — especially the shared Node.js workers on the "CI Gate / Frontend Tests & Build" step — the reconcile-and-paint cycle has been measured at ~1.4s, which exceeds the 1s default findByText timeout. This is a load-sensitive timing flake, not a real bug — the toast always renders eventually because the state update chain is purely synchronous and the displayed text comes from the closure's pre-update contacts (so the "Remove Me" name is always available when the toast finally renders). Historical flake hits in CI on this exact assertion: PR #226 (zh-CN i18n landing, exposed by i18n parse error) PR #237 (GitLab mirror parity) PR #261 (post-#227 audit gap closures) PR #262 (AIS SPKI pinning — failed post-merge Docker Publish, skipping image publication for commit 729ea78) The last one is the worst — a post-merge flake that blocked the Docker image for an actual security fix from being published. The subsequent merge of #263 cumulatively re-published the image, but that's by accident, not by design. Fix: replace the 1-second findByText with waitFor + 5s timeout + 50ms polling. The 5s ceiling still surfaces a real "toast never renders" regression with a clear error; it just doesn't get racy under CI load anymore. Validation: Local sequential 10x run of just this test → 10 passed, 0 failed Full vitest suite → 707 passed, 72 files Co-authored-by: Claude Opus 4.7 --- .../mesh/messagesViewFirstContact.test.tsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/frontend/src/__tests__/mesh/messagesViewFirstContact.test.tsx b/frontend/src/__tests__/mesh/messagesViewFirstContact.test.tsx index 385ee60..dec5baa 100644 --- a/frontend/src/__tests__/mesh/messagesViewFirstContact.test.tsx +++ b/frontend/src/__tests__/mesh/messagesViewFirstContact.test.tsx @@ -859,10 +859,28 @@ describe('MessagesView first-contact trust UX', () => { renderMessagesView(); fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' })); - expect(await screen.findByText('Remove Me')).toBeInTheDocument(); + expect( + await screen.findByText('Remove Me', undefined, { timeout: 5000 }), + ).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Remove' })); - expect(await screen.findByText(/Removed contact: Remove Me\./i)).toBeInTheDocument(); + // The Remove handler dispatches several React state updates in one + // event (removeContact + setContacts + setComposeStatus + setComposeError). + // Under CI load the resulting render-and-paint cycle has been observed + // to take >1s, which is the default findByText timeout — that race has + // produced flakes on PRs #226, #237, #261, and #262 in succession. + // The settle window is bounded by React's reconciliation, not by any + // network/animation cost, so a generous timeout is the right deflake + // here (the failure mode this masks would be "toast never renders", + // which would still fail at 5s). + await waitFor( + () => { + expect( + screen.getByText(/Removed contact: Remove Me\./i), + ).toBeInTheDocument(); + }, + { timeout: 5000, interval: 50 }, + ); expect(screen.queryByText('Remove Me')).not.toBeInTheDocument(); });