test: deflake messagesViewFirstContact remove-contact test (#264)

This test asserts that clicking "Remove" on a contact:
  1. Surfaces a toast "Removed contact: <name>."
  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 <noreply@anthropic.com>
This commit is contained in:
Shadowbroker
2026-05-20 21:13:40 -06:00
committed by GitHub
parent bcc2d036b3
commit 87ba70acd6
@@ -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();
});