From b577ac2650bf4ca78cb0b0dda57123cb730eb7e6 Mon Sep 17 00:00:00 2001 From: seiler Date: Fri, 23 Oct 2026 18:26:52 +0000 Subject: [PATCH] fix(desktop): keep community actions stable across hover transitions Cancel pending hover changes when community actions explicitly open or close. Cover stale open and close timers on the rendered component and use controlled time for pointer crossing coverage. Keep composer markdown assertions focused on the latest signed message so later read-state events cannot replace the observed content. Co-authored-by: Codex Signed-off-by: seiler --- .../ui/CommunitySwitcher.jsdom-test.mjs | 64 +++++++++++++++++++ .../communities/ui/CommunitySwitcher.tsx | 19 ++---- desktop/tests/e2e/community-rail.spec.ts | 52 ++++----------- .../e2e/composer-selection-formatting.spec.ts | 53 ++++----------- 4 files changed, 96 insertions(+), 92 deletions(-) create mode 100644 desktop/src/features/communities/ui/CommunitySwitcher.jsdom-test.mjs diff --git a/desktop/src/features/communities/ui/CommunitySwitcher.jsdom-test.mjs b/desktop/src/features/communities/ui/CommunitySwitcher.jsdom-test.mjs new file mode 100644 index 0000000000..b627493c3b --- /dev/null +++ b/desktop/src/features/communities/ui/CommunitySwitcher.jsdom-test.mjs @@ -0,0 +1,64 @@ +import assert from "node:assert/strict"; +import { mock, test } from "node:test"; +import React, { act } from "react"; +import { cleanup, fireEvent, render } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +import { CommunitySwitcher } from "./CommunitySwitcher.tsx"; + +const community = { + id: "alpha", + name: "Alpha", + relayUrl: "wss://alpha.example", + addedAt: "2026-01-01T00:00:00Z", +}; + +function renderSwitcher(t) { + const client = new QueryClient(); + client.setQueryData(["communityIcon", community.relayUrl], null); + const view = render( + React.createElement( + QueryClientProvider, + { client }, + React.createElement(CommunitySwitcher, { + activeCommunity: community, + communities: [community], + variant: "profile-menu", + onSwitchCommunity() {}, + onAddCommunity() {}, + onUpdateCommunity() {}, + async onLeaveCommunity() {}, + async onRemoveCommunityFromDevice() {}, + }), + ), + ); + t.after(() => { + cleanup(); + client.clear(); + mock.timers.reset(); + }); + mock.timers.enable({ apis: ["setTimeout"] }); + return view; +} + +test("explicitly opening community actions cancels a pending hover close", (t) => { + const view = renderSwitcher(t); + const trigger = view.getByTestId("community-switcher"); + fireEvent.mouseEnter(trigger); + fireEvent.mouseLeave(trigger); + fireEvent.click(trigger); + act(() => mock.timers.tick(160)); + assert.equal(trigger.getAttribute("aria-expanded"), "true"); + assert.ok(view.getByRole("menu", { name: "Community actions" })); +}); + +test("selecting a community action cancels a pending hover open", (t) => { + const view = renderSwitcher(t); + const trigger = view.getByTestId("community-switcher"); + fireEvent.click(trigger); + fireEvent.mouseEnter(view.getByRole("menu", { name: "Community actions" })); + fireEvent.click(view.getByRole("menuitem", { name: "Add a community" })); + act(() => mock.timers.tick(80)); + assert.equal(trigger.getAttribute("aria-expanded"), "false"); + assert.equal(view.queryByRole("menu", { name: "Community actions" }), null); +}); diff --git a/desktop/src/features/communities/ui/CommunitySwitcher.tsx b/desktop/src/features/communities/ui/CommunitySwitcher.tsx index 14e358b288..bf136e549d 100644 --- a/desktop/src/features/communities/ui/CommunitySwitcher.tsx +++ b/desktop/src/features/communities/ui/CommunitySwitcher.tsx @@ -156,13 +156,8 @@ export function CommunitySwitcher({ } function handleProfileMenuOpenChange(nextOpen: boolean) { - if (variant !== "profile-menu") { - setDropdownOpen(nextOpen); - return; - } - if (!nextOpen) { - clearProfileMenuHoverTimer(); - } + // Explicit actions take precedence over an earlier pointer transition. + clearProfileMenuHoverTimer(); setDropdownOpen(nextOpen); } @@ -318,7 +313,7 @@ export function CommunitySwitcher({