diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index aad2580dad0..91d4641e37e 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ name: "smoke", testMatch: [ "**/smoke.spec.ts", + "**/dm-hide-failure.spec.ts", "**/owned-agent-discovery.spec.ts", "**/thread-head-stale-edit.spec.ts", "**/sidebar-offcanvas-rail.spec.ts", diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index b4c2039023e..ccfc108e847 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { ProtectedGlobalOverlay } from "@protected-feature-components"; import { useQueryClient } from "@tanstack/react-query"; import { Outlet, useLocation } from "@tanstack/react-router"; +import { toast } from "sonner"; import { deriveShellRoute, markAllReadSources } from "@/app/AppShell.helpers"; import { useTerminalContext } from "@/app/useTerminalContext"; import { AppShellProvider } from "@/app/AppShellContext"; @@ -625,7 +626,12 @@ export function AppShell() { async (channelId: string) => { try { await hideDmMutation.mutateAsync(channelId); - } catch { + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Failed to close direct message", + ); return; } diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 6ddc1111b03..e2c7d27ca46 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -1364,6 +1364,8 @@ declare global { __BUZZ_E2E_UNSUPPORTED_PROJECT_ANNOUNCEMENTS__?: boolean; /** Project event kinds accepted once but reported as failed to test lost acknowledgements. */ __BUZZ_E2E_FAIL_PROJECT_EVENT_ACK_KINDS__?: number[]; + /** Makes hiding a DM fail, to exercise the error path of the sidebar close action. */ + __BUZZ_E2E_FAIL_HIDE_DM__?: string; /** * Extra project events appended to the mock store on first access. * Use to seed standalone repositories (kind 30617) or other project-scoped @@ -7401,6 +7403,11 @@ async function handleHideDm( args: { channelId: string }, config: E2eConfig | undefined, ) { + const forcedFailure = window.__BUZZ_E2E_FAIL_HIDE_DM__; + if (forcedFailure) { + throw new Error(forcedFailure); + } + const identity = getIdentity(config); if (!identity) { const index = mockChannels.findIndex( diff --git a/desktop/tests/e2e/dm-hide-failure.spec.ts b/desktop/tests/e2e/dm-hide-failure.spec.ts new file mode 100644 index 00000000000..725e812dc71 --- /dev/null +++ b/desktop/tests/e2e/dm-hide-failure.spec.ts @@ -0,0 +1,30 @@ +import { expect, test } from "@playwright/test"; + +import { installMockBridge } from "../helpers/bridge"; + +const DM_NAME = "alice-tyler"; +const RELAY_ERROR = "relay error 400: forbidden: not a member of this DM"; + +test("a rejected DM close reports the relay error and keeps the row", async ({ + page, +}) => { + await page.addInitScript((message) => { + window.__BUZZ_E2E_FAIL_HIDE_DM__ = message; + }, RELAY_ERROR); + await installMockBridge(page); + await page.goto("/", { waitUntil: "domcontentloaded" }); + + const dmRow = page.getByTestId(`channel-${DM_NAME}`); + await expect(dmRow).toBeVisible(); + + await dmRow.hover(); + await page.getByTestId(`hide-dm-${DM_NAME}`).click(); + + await expect( + page.locator("[data-sonner-toast]").filter({ hasText: RELAY_ERROR }), + ).toBeVisible(); + + // The optimistic removal must roll back: the conversation is still there, + // so the sidebar must not claim it went away. + await expect(dmRow).toBeVisible(); +});