diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 0d9899c7f..12fa6fd3b 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -1254,6 +1254,12 @@ declare global { __BUZZ_E2E_RELEASE_OBSERVER_ARCHIVE_POLICY__?: () => number; /** Number of observer archive policy commands currently held by the seam. */ __BUZZ_E2E_OBSERVER_ARCHIVE_POLICY_PENDING__?: number; + /** Hold channel reads until released. */ + __BUZZ_E2E_DEFER_NEXT_CHANNELS_READ__?: () => void; + /** Release every channel read held by the explicit E2E seam. */ + __BUZZ_E2E_RELEASE_CHANNELS_READ__?: () => number; + /** Number of channel reads currently held by the seam. */ + __BUZZ_E2E_CHANNELS_READ_PENDING__?: number; } } @@ -1358,6 +1364,8 @@ type DeferredGetEvent = { }; let deferredGetEventQueue: DeferredGetEvent[] = []; let deferredObserverArchivePolicyQueue: Array<() => void> = []; +let deferChannelsReads = false; +let deferredChannelsReadQueue: Array<() => void> = []; const mockDisplayNames = new Map([ [MOCK_IDENTITY_PUBKEY, DEFAULT_MOCK_IDENTITY.display_name], @@ -9783,6 +9791,19 @@ export function maybeInstallE2eTauriMocks() { for (const resolve of queued) resolve(); return queued.length; }; + deferChannelsReads = false; + deferredChannelsReadQueue = []; + window.__BUZZ_E2E_CHANNELS_READ_PENDING__ = 0; + window.__BUZZ_E2E_DEFER_NEXT_CHANNELS_READ__ = () => { + deferChannelsReads = true; + }; + window.__BUZZ_E2E_RELEASE_CHANNELS_READ__ = () => { + deferChannelsReads = false; + const queued = deferredChannelsReadQueue.splice(0); + window.__BUZZ_E2E_CHANNELS_READ_PENDING__ = 0; + for (const resolve of queued) resolve(); + return queued.length; + }; window.__BUZZ_E2E_RELEASE_GET_EVENT__ = () => { const queued = deferredGetEventQueue.splice(0); for (const entry of queued) { @@ -11087,6 +11108,13 @@ export function maybeInstallE2eTauriMocks() { activeConfig, ); case "get_channels": + if (deferChannelsReads) { + await new Promise((resolve) => { + deferredChannelsReadQueue.push(resolve); + window.__BUZZ_E2E_CHANNELS_READ_PENDING__ = + deferredChannelsReadQueue.length; + }); + } return handleGetChannels(activeConfig); case "get_feed": return handleGetFeed( diff --git a/desktop/tests/e2e/community-rail.spec.ts b/desktop/tests/e2e/community-rail.spec.ts index 3b24b87f3..f0de430c7 100644 --- a/desktop/tests/e2e/community-rail.spec.ts +++ b/desktop/tests/e2e/community-rail.spec.ts @@ -448,14 +448,7 @@ test.describe("community rail", () => { test("does not repair a remembered channel until live validation succeeds", async ({ page, }) => { - await installMockBridge( - page, - { - channelsReadDelayMs: 300, - channelsReadErrors: [null, "temporary channel read failure"], - }, - { skipCommunitySeed: true }, - ); + await installMockBridge(page, undefined, { skipCommunitySeed: true }); await seedCommunities(page, [COMMUNITY_A, COMMUNITY_B], COMMUNITY_A.id); await page.addInitScript((communityId) => { window.localStorage.setItem( @@ -488,20 +481,39 @@ test.describe("community rail", () => { JSON.stringify(snapshot), ); }); + await page.evaluate(() => { + const deferNextChannelsRead = ( + window as Window & { + __BUZZ_E2E_DEFER_NEXT_CHANNELS_READ__?: () => void; + } + ).__BUZZ_E2E_DEFER_NEXT_CHANNELS_READ__; + if (!deferNextChannelsRead) { + throw new Error("missing channel-read defer seam"); + } + deferNextChannelsRead(); + }); + await page.evaluate(() => { + const invalidateChannels = ( + window as Window & { + __BUZZ_E2E_INVALIDATE_CHANNELS__?: () => Promise; + } + ).__BUZZ_E2E_INVALIDATE_CHANNELS__; + if (!invalidateChannels) { + throw new Error("missing channel invalidation seam"); + } + void invalidateChannels(); + }); + await page.waitForFunction( + () => + ( + window as Window & { + __BUZZ_E2E_CHANNELS_READ_PENDING__?: number; + } + ).__BUZZ_E2E_CHANNELS_READ_PENDING__ === 1, + ); await page.getByTestId(`community-rail-button-${COMMUNITY_B.id}`).click(); - await expect(page).toHaveURL(/#\/channels\/general$/); - await expect - .poll(() => - page.evaluate((communityId) => { - const raw = window.localStorage.getItem( - "buzz-community-destinations", - ); - return raw ? JSON.parse(raw)[communityId] : null; - }, COMMUNITY_B.id), - ) - .toEqual({ kind: "channel", channelId: "general" }); - await page.waitForTimeout(400); + await expect(page).not.toHaveURL(/#\/channels\/general$/); await expect .poll(() => page.evaluate((communityId) => { @@ -513,7 +525,44 @@ test.describe("community rail", () => { ) .toEqual({ kind: "channel", channelId: "general" }); - await expect(page.getByTestId("channel-general")).toBeVisible(); + await page.evaluate(() => { + const config = ( + window as Window & { + __BUZZ_E2E__?: { + mock?: { channelsReadErrors?: (string | null)[] }; + }; + } + ).__BUZZ_E2E__; + if (!config) throw new Error("missing E2E config"); + config.mock = { + ...config.mock, + channelsReadErrors: ["temporary channel read failure"], + }; + }); + await expect + .poll(() => + page.evaluate( + () => window.__BUZZ_E2E__?.mock?.channelsReadErrors?.length ?? 0, + ), + ) + .toBe(1); + const released = await page.evaluate( + () => + ( + window as Window & { + __BUZZ_E2E_RELEASE_CHANNELS_READ__?: () => number; + } + ).__BUZZ_E2E_RELEASE_CHANNELS_READ__?.() ?? 0, + ); + expect(released).toBe(1); + await expect + .poll(() => + page.evaluate( + () => window.__BUZZ_E2E__?.mock?.channelsReadErrors?.length ?? 0, + ), + ) + .toBe(0); + await expect .poll(() => page.evaluate((communityId) => {