From 23f8591acf758d2cf6aee679b6096e7995661ac1 Mon Sep 17 00:00:00 2001 From: QuicksilverSlick Date: Mon, 2 Nov 2026 22:03:59 -0600 Subject: [PATCH] fix(desktop): gate the sidebar-snapshot loading tests on the boot read Three tests waited 500 ms after goto for the loading skeleton while the mock channels read resolved at 600 ms, so a render later than about 350 ms missed the window. They failed 3/3 on fork #75 and #76 and fail every time at 2x CPU. Arm the bridge's existing one-shot channels-read hold from config, so it covers the boot read, and hold that read until the test releases it. Co-Authored-By: Claude Opus 5.5 --- desktop/src/testing/e2eBridge.ts | 6 ++- desktop/tests/e2e/sidebar-snapshot.spec.ts | 48 +++++++++++++--------- desktop/tests/helpers/bridge.ts | 14 +++++++ 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 8f7c7bd70ff..48c44ecd984 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -386,6 +386,8 @@ type E2eConfig = { /** Reject successive mock `join_channel` calls, then resume. */ joinChannelErrors?: string[]; channelsReadDelayMs?: number; + /** Hold the boot `get_channels` read until the E2E release seam is invoked. */ + deferFirstChannelsRead?: boolean; /** Return not-modified for this many reads before resuming full payloads. */ channelsNotModifiedResponses?: number; /** When true, a matching knownHash returns a not-modified channel payload. */ @@ -11911,7 +11913,9 @@ export function maybeInstallE2eTauriMocks() { for (const release of held) release(); return held.length; }; - deferNextChannelsRead = false; + // Armed from config because the boot read fires before a spec can call the + // defer seam below. + deferNextChannelsRead = config.mock?.deferFirstChannelsRead ?? false; deferredChannelsReadResolve = null; window.__BUZZ_E2E_CHANNELS_READ_PENDING__ = 0; window.__BUZZ_E2E_DEFER_NEXT_CHANNELS_READ__ = () => { diff --git a/desktop/tests/e2e/sidebar-snapshot.spec.ts b/desktop/tests/e2e/sidebar-snapshot.spec.ts index bff5af61209..a45963d42da 100644 --- a/desktop/tests/e2e/sidebar-snapshot.spec.ts +++ b/desktop/tests/e2e/sidebar-snapshot.spec.ts @@ -1,6 +1,10 @@ import { expect, test, type Page } from "@playwright/test"; -import { installMockBridge } from "../helpers/bridge"; +import { + installMockBridge, + pendingChannelsRead, + releaseChannelsRead, +} from "../helpers/bridge"; const RELAY_URL = "ws://localhost:3000"; const OTHER_RELAY_URL = "ws://localhost:3001"; @@ -317,17 +321,13 @@ test("matching not-modified preserves display mutations without persisting them" exact: true, }); }); - await expect - .poll(() => page.evaluate(() => window.__BUZZ_E2E_CHANNELS_READ_PENDING__)) - .toBe(1); + await expect.poll(() => pendingChannelsRead(page)).toBe(1); await mutateDisplayedChannels(page, optimisticName); await expect( page.locator('[data-channel-id="optimistic-channel"]'), ).toBeVisible(); - expect( - await page.evaluate(() => window.__BUZZ_E2E_RELEASE_CHANNELS_READ__?.()), - ).toBe(1); + expect(await releaseChannelsRead(page)).toBe(1); // Invocation logging only proves the request started. Wait for the query to // consume its response before checking either display state or persistence. await expect @@ -358,12 +358,12 @@ test("matching not-modified preserves display mutations without persisting them" test("first-ever boot without a snapshot sends null and shows loading", async ({ page, }) => { - await installMockBridge(page, { channelsReadDelayMs: READ_DELAY_MS }); + await installMockBridge(page, { deferFirstChannelsRead: true }); await page.goto("/"); - await expect(page.getByTestId("sidebar-loading")).toBeVisible({ - timeout: 500, - }); + // The gate holds the boot read, so the loading frame cannot race the runner. + await expect(page.getByTestId("sidebar-loading")).toBeVisible(); + await expect.poll(() => pendingChannelsRead(page)).toBe(1); await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0); await expect .poll(() => getChannelsPayloads(page)) @@ -375,7 +375,10 @@ test("first-ever boot without a snapshot sends null and shows loading", async ({ presence: "absent", serializedBytes: 0, }); + + expect(await releaseChannelsRead(page)).toBe(1); await expect(page.getByTestId("channel-general")).toBeVisible(); + await expect(page.getByTestId("sidebar-loading")).toHaveCount(0); await expect .poll(() => getFullSidebarMeasure(page)) .toEqual({ @@ -391,12 +394,12 @@ test("a different identity's snapshot is ignored", async ({ page }) => { keyOwnerPubkey: OWNER_PUBKEY, ownerPubkey: STALE_COMMUNITY_PUBKEY, }); - await installMockBridge(page, { channelsReadDelayMs: READ_DELAY_MS }); + await installMockBridge(page, { deferFirstChannelsRead: true }); await page.goto("/"); - await expect(page.getByTestId("sidebar-loading")).toBeVisible({ - timeout: 500, - }); + // The gate holds the boot read, so the loading frame cannot race the runner. + await expect(page.getByTestId("sidebar-loading")).toBeVisible(); + await expect.poll(() => pendingChannelsRead(page)).toBe(1); await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0); await expect .poll(() => getChannelsPayloads(page)) @@ -407,7 +410,11 @@ test("a different identity's snapshot is ignored", async ({ page }) => { channelCount: 0, presence: "invalid", }); + + expect(await releaseChannelsRead(page)).toBe(1); await expect(page.getByTestId("channel-general")).toBeVisible(); + await expect(page.getByTestId("sidebar-loading")).toHaveCount(0); + await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0); }); test("display-only community pubkey cannot expose another identity's snapshot", async ({ @@ -447,14 +454,14 @@ test("partial hash/list write fails toward a full fetch", async ({ page }) => { ); }, snapshotKey(RELAY_URL)); await installMockBridge(page, { - channelsReadDelayMs: READ_DELAY_MS, + deferFirstChannelsRead: true, honorChannelsKnownHash: true, }); await page.goto("/"); - await expect(page.getByTestId("sidebar-loading")).toBeVisible({ - timeout: 500, - }); + // The gate holds the boot read, so the loading frame cannot race the runner. + await expect(page.getByTestId("sidebar-loading")).toBeVisible(); + await expect.poll(() => pendingChannelsRead(page)).toBe(1); await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0); await expect .poll(() => getChannelsPayloads(page)) @@ -465,7 +472,10 @@ test("partial hash/list write fails toward a full fetch", async ({ page }) => { channelCount: 0, presence: "invalid", }); + + expect(await releaseChannelsRead(page)).toBe(1); await expect(page.getByTestId("channel-general")).toBeVisible(); + await expect(page.getByTestId("sidebar-loading")).toHaveCount(0); await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0); }); diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index 843e4ac452b..02955b6dffa 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -288,6 +288,8 @@ type MockBridgeOptions = { /** Reject successive mock `join_channel` calls, then resume. */ joinChannelErrors?: string[]; channelsReadDelayMs?: number; + /** Hold the boot `get_channels` read until `releaseChannelsRead` runs. */ + deferFirstChannelsRead?: boolean; /** Return not-modified for this many reads before resuming full payloads. */ channelsNotModifiedResponses?: number; /** When true, a matching knownHash returns a not-modified channel payload. */ @@ -1064,6 +1066,18 @@ export async function installRelayBridge( }); } +/** Number of `get_channels` reads currently held. */ +export async function pendingChannelsRead(page: Page): Promise { + return page.evaluate(() => window.__BUZZ_E2E_CHANNELS_READ_PENDING__ ?? 0); +} + +/** Release the `get_channels` read held by the mock bridge, if any. */ +export async function releaseChannelsRead(page: Page): Promise { + return page.evaluate( + () => window.__BUZZ_E2E_RELEASE_CHANNELS_READ__?.() ?? 0, + ); +} + /** Release every `get_channel_members` read held by `deferChannelMembersReads`. */ export async function releaseChannelMembersReads(page: Page): Promise { return page.evaluate(