diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItemMoreOptionsMenu.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItemMoreOptionsMenu.stories.tsx/default-auto.png
new file mode 100644
index 0000000000..74d48285cc
Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItemMoreOptionsMenu.stories.tsx/default-auto.png differ
diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.stories.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.stories.tsx
new file mode 100644
index 0000000000..5b1ac48d4c
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.stories.tsx
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2026 Element Creations Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import React, { type JSX } from "react";
+import { fn } from "storybook/test";
+import { userEvent, within } from "storybook/test";
+
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { RoomListItemMoreOptionsMenu } from "./RoomListItemMoreOptionsMenu";
+import { type RoomListItemSnapshot, type RoomListItemActions } from "./RoomListItem";
+import { useMockedViewModel } from "../../viewmodel";
+import { defaultSnapshot } from "./default-snapshot";
+
+type MoreOptionsMenuProps = RoomListItemSnapshot & RoomListItemActions;
+
+// Wrapper component that creates a mocked ViewModel
+const MoreOptionsMenuWrapper = ({
+ onOpenRoom,
+ onMarkAsRead,
+ onMarkAsUnread,
+ onToggleFavorite,
+ onToggleLowPriority,
+ onInvite,
+ onCopyRoomLink,
+ onLeaveRoom,
+ onSetRoomNotifState,
+ ...rest
+}: MoreOptionsMenuProps): JSX.Element => {
+ const vm = useMockedViewModel(rest, {
+ onOpenRoom,
+ onMarkAsRead,
+ onMarkAsUnread,
+ onToggleFavorite,
+ onToggleLowPriority,
+ onInvite,
+ onCopyRoomLink,
+ onLeaveRoom,
+ onSetRoomNotifState,
+ });
+ return ;
+};
+
+const meta = {
+ title: "Room List/RoomListItem/MoreOptionsMenu",
+ component: MoreOptionsMenuWrapper,
+ tags: ["autodocs"],
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ args: {
+ ...defaultSnapshot,
+ showMoreOptionsMenu: true,
+ onOpenRoom: fn(),
+ onMarkAsRead: fn(),
+ onMarkAsUnread: fn(),
+ onToggleFavorite: fn(),
+ onToggleLowPriority: fn(),
+ onInvite: fn(),
+ onCopyRoomLink: fn(),
+ onLeaveRoom: fn(),
+ onSetRoomNotifState: fn(),
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+// Closed state
+export const Default: Story = {
+ args: {
+ canMarkAsRead: false,
+ canMarkAsUnread: true,
+ isFavourite: false,
+ isLowPriority: false,
+ },
+};
+
+// Open state - default (can mark as unread, favourite off, low priority off)
+export const Open: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const trigger = canvas.getByRole("button", { name: "More Options" });
+ await userEvent.click(trigger);
+ },
+};
+
+// Open state - can mark as read (has unread messages)
+export const OpenCanMarkAsRead: Story = {
+ args: {
+ canMarkAsRead: true,
+ canMarkAsUnread: false,
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const trigger = canvas.getByRole("button", { name: "More Options" });
+ await userEvent.click(trigger);
+ },
+};
+
+// Open state - favourite enabled
+export const OpenFavouriteOn: Story = {
+ args: {
+ isFavourite: true,
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const trigger = canvas.getByRole("button", { name: "More Options" });
+ await userEvent.click(trigger);
+ },
+};
+
+// Open state - low priority enabled
+export const OpenLowPriorityOn: Story = {
+ args: {
+ isLowPriority: true,
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const trigger = canvas.getByRole("button", { name: "More Options" });
+ await userEvent.click(trigger);
+ },
+};
+
+// Open state - without invite option (DM or no permission)
+export const OpenWithoutInvite: Story = {
+ args: {
+ canInvite: false,
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const trigger = canvas.getByRole("button", { name: "More Options" });
+ await userEvent.click(trigger);
+ },
+};
+
+// Open state - without copy room link (DM room)
+export const OpenWithoutCopyLink: Story = {
+ args: {
+ canCopyRoomLink: false,
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const trigger = canvas.getByRole("button", { name: "More Options" });
+ await userEvent.click(trigger);
+ },
+};
diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.test.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.test.tsx
index 40b9917c5b..b077c388d4 100644
--- a/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.test.tsx
+++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.test.tsx
@@ -5,223 +5,161 @@
* Please see LICENSE files in the repository root for full details.
*/
-import React, { type JSX } from "react";
+import React from "react";
import { render, screen } from "@test-utils";
import userEvent from "@testing-library/user-event";
-import { describe, it, expect, vi } from "vitest";
+import { composeStories } from "@storybook/react-vite";
+import { describe, it, expect } from "vitest";
-import { RoomListItemMoreOptionsMenu } from "./RoomListItemMoreOptionsMenu";
-import { useMockedViewModel } from "../../viewmodel";
-import type { RoomListItemSnapshot } from "./RoomListItem";
-import { defaultSnapshot } from "./default-snapshot";
+import * as stories from "./RoomListItemMoreOptionsMenu.stories";
-describe("", () => {
- const mockCallbacks = {
- onOpenRoom: vi.fn(),
- onMarkAsRead: vi.fn(),
- onMarkAsUnread: vi.fn(),
- onToggleFavorite: vi.fn(),
- onToggleLowPriority: vi.fn(),
- onInvite: vi.fn(),
- onCopyRoomLink: vi.fn(),
- onLeaveRoom: vi.fn(),
- onSetRoomNotifState: vi.fn(),
- };
+const { Default, Open, OpenCanMarkAsRead, OpenFavouriteOn, OpenLowPriorityOn, OpenWithoutInvite, OpenWithoutCopyLink } =
+ composeStories(stories);
- const renderMenu = (overrides: Partial = {}): ReturnType => {
- const TestComponent = (): JSX.Element => {
- const vm = useMockedViewModel(
- {
- ...defaultSnapshot,
- showMoreOptionsMenu: true,
- showNotificationMenu: false,
- ...overrides,
- } as RoomListItemSnapshot,
- mockCallbacks,
- );
- return ;
- };
- return render();
- };
-
- it("should render the more options button", () => {
- renderMenu();
- expect(screen.getByRole("button", { name: "More Options" })).toBeInTheDocument();
+describe(" stories", () => {
+ it("renders Default story (closed)", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
});
- it("should open menu when clicked", async () => {
- const user = userEvent.setup();
- renderMenu();
-
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
-
- expect(screen.getByRole("menu")).toBeInTheDocument();
+ it("renders Open story", async () => {
+ const { container } = render();
+ await Open.play?.({ canvasElement: container });
+ expect(container).toMatchSnapshot();
});
- it("should show mark as read option when canMarkAsRead is true", async () => {
- const user = userEvent.setup();
- renderMenu({ canMarkAsRead: true });
-
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
-
- expect(screen.getByRole("menuitem", { name: "Mark as read" })).toBeInTheDocument();
+ it("renders OpenCanMarkAsRead story", async () => {
+ const { container } = render();
+ await OpenCanMarkAsRead.play?.({ canvasElement: container });
+ expect(container).toMatchSnapshot();
});
- it("should not show mark as read option when canMarkAsRead is false", async () => {
- const user = userEvent.setup();
- renderMenu({ canMarkAsRead: false });
+ it("renders OpenFavouriteOn story", async () => {
+ const { container } = render();
+ await OpenFavouriteOn.play?.({ canvasElement: container });
+ expect(container).toMatchSnapshot();
+ });
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
+ it("renders OpenLowPriorityOn story", async () => {
+ const { container } = render();
+ await OpenLowPriorityOn.play?.({ canvasElement: container });
+ expect(container).toMatchSnapshot();
+ });
+ it("renders OpenWithoutInvite story", async () => {
+ const { container } = render();
+ await OpenWithoutInvite.play?.({ canvasElement: container });
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders OpenWithoutCopyLink story", async () => {
+ const { container } = render();
+ await OpenWithoutCopyLink.play?.({ canvasElement: container });
+ expect(container).toMatchSnapshot();
+ });
+
+ it("should show mark as unread by default", async () => {
+ const { container } = render();
+ await Open.play?.({ canvasElement: container });
+
+ expect(screen.getByRole("menuitem", { name: "Mark as unread" })).toBeInTheDocument();
expect(screen.queryByRole("menuitem", { name: "Mark as read" })).not.toBeInTheDocument();
});
- it("should call onMarkAsRead when mark as read clicked", async () => {
- const user = userEvent.setup();
- renderMenu({ canMarkAsRead: true });
+ it("should show mark as read when canMarkAsRead is true", async () => {
+ const { container } = render();
+ await OpenCanMarkAsRead.play?.({ canvasElement: container });
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
-
- const markAsReadOption = screen.getByRole("menuitem", { name: "Mark as read" });
- await user.click(markAsReadOption);
-
- expect(mockCallbacks.onMarkAsRead).toHaveBeenCalled();
+ expect(screen.getByRole("menuitem", { name: "Mark as read" })).toBeInTheDocument();
+ expect(screen.queryByRole("menuitem", { name: "Mark as unread" })).not.toBeInTheDocument();
});
- it("should show mark as unread option when canMarkAsUnread is true", async () => {
- const user = userEvent.setup();
- renderMenu({ canMarkAsUnread: true });
+ it("should show favourite as checked when isFavourite is true", async () => {
+ const { container } = render();
+ await OpenFavouriteOn.play?.({ canvasElement: container });
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
-
- expect(screen.getByRole("menuitem", { name: "Mark as unread" })).toBeInTheDocument();
+ const favouriteOption = screen.getByRole("menuitemcheckbox", { name: "Favourited" });
+ expect(favouriteOption).toHaveAttribute("aria-checked", "true");
});
- it("should call onMarkAsUnread when mark as unread clicked", async () => {
- const user = userEvent.setup();
- renderMenu({ canMarkAsUnread: true });
+ it("should show favourite as unchecked by default", async () => {
+ const { container } = render();
+ await Open.play?.({ canvasElement: container });
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
-
- const markAsUnreadOption = screen.getByRole("menuitem", { name: "Mark as unread" });
- await user.click(markAsUnreadOption);
-
- expect(mockCallbacks.onMarkAsUnread).toHaveBeenCalled();
+ const favouriteOption = screen.getByRole("menuitemcheckbox", { name: "Favourited" });
+ expect(favouriteOption).toHaveAttribute("aria-checked", "false");
});
- it("should show favorite option and call onToggleFavorite", async () => {
- const user = userEvent.setup();
- renderMenu({ isFavourite: false });
-
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
-
- const favoriteOption = screen.getByRole("menuitemcheckbox", { name: "Favourited" });
- expect(favoriteOption).toBeInTheDocument();
- expect(favoriteOption).toHaveAttribute("aria-checked", "false");
-
- await user.click(favoriteOption);
- expect(mockCallbacks.onToggleFavorite).toHaveBeenCalled();
- });
-
- it("should show favorite as checked when isFavourite is true", async () => {
- const user = userEvent.setup();
- renderMenu({ isFavourite: true });
-
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
-
- const favoriteOption = screen.getByRole("menuitemcheckbox", { name: "Favourited" });
- expect(favoriteOption).toHaveAttribute("aria-checked", "true");
- });
-
- it("should show low priority option and call onToggleLowPriority", async () => {
- const user = userEvent.setup();
- renderMenu({ isLowPriority: false });
-
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
+ it("should show low priority as checked when isLowPriority is true", async () => {
+ const { container } = render();
+ await OpenLowPriorityOn.play?.({ canvasElement: container });
const lowPriorityOption = screen.getByRole("menuitemcheckbox", { name: "Low priority" });
- expect(lowPriorityOption).toBeInTheDocument();
- expect(lowPriorityOption).toHaveAttribute("aria-checked", "false");
-
- await user.click(lowPriorityOption);
- expect(mockCallbacks.onToggleLowPriority).toHaveBeenCalled();
+ expect(lowPriorityOption).toHaveAttribute("aria-checked", "true");
});
- it("should show invite option when canInvite is true", async () => {
- const user = userEvent.setup();
- renderMenu({ canInvite: true });
+ it("should show low priority as unchecked by default", async () => {
+ const { container } = render();
+ await Open.play?.({ canvasElement: container });
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
+ const lowPriorityOption = screen.getByRole("menuitemcheckbox", { name: "Low priority" });
+ expect(lowPriorityOption).toHaveAttribute("aria-checked", "false");
+ });
+
+ it("should show invite option by default", async () => {
+ const { container } = render();
+ await Open.play?.({ canvasElement: container });
expect(screen.getByRole("menuitem", { name: "Invite" })).toBeInTheDocument();
});
- it("should call onInvite when invite clicked", async () => {
- const user = userEvent.setup();
- renderMenu({ canInvite: true });
+ it("should hide invite option when canInvite is false", async () => {
+ const { container } = render();
+ await OpenWithoutInvite.play?.({ canvasElement: container });
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
-
- const inviteOption = screen.getByRole("menuitem", { name: "Invite" });
- await user.click(inviteOption);
-
- expect(mockCallbacks.onInvite).toHaveBeenCalled();
+ expect(screen.queryByRole("menuitem", { name: "Invite" })).not.toBeInTheDocument();
});
- it("should show copy link option when canCopyRoomLink is true", async () => {
- const user = userEvent.setup();
- renderMenu({ canCopyRoomLink: true });
-
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
+ it("should show copy room link by default", async () => {
+ const { container } = render();
+ await Open.play?.({ canvasElement: container });
expect(screen.getByRole("menuitem", { name: "Copy room link" })).toBeInTheDocument();
});
- it("should call onCopyRoomLink when copy link clicked", async () => {
- const user = userEvent.setup();
- renderMenu({ canCopyRoomLink: true });
+ it("should hide copy room link when canCopyRoomLink is false", async () => {
+ const { container } = render();
+ await OpenWithoutCopyLink.play?.({ canvasElement: container });
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
-
- const copyLinkOption = screen.getByRole("menuitem", { name: "Copy room link" });
- await user.click(copyLinkOption);
-
- expect(mockCallbacks.onCopyRoomLink).toHaveBeenCalled();
+ expect(screen.queryByRole("menuitem", { name: "Copy room link" })).not.toBeInTheDocument();
});
- it("should show leave room option", async () => {
- const user = userEvent.setup();
- renderMenu();
-
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
+ it("should always show leave room option", async () => {
+ const { container } = render();
+ await Open.play?.({ canvasElement: container });
expect(screen.getByRole("menuitem", { name: "Leave room" })).toBeInTheDocument();
});
- it("should call onLeaveRoom when leave room clicked", async () => {
+ it("should call onToggleFavorite when favourite is clicked", async () => {
const user = userEvent.setup();
- renderMenu();
+ const { container } = render();
+ await Open.play?.({ canvasElement: container });
- const button = screen.getByRole("button", { name: "More Options" });
- await user.click(button);
+ const favouriteOption = screen.getByRole("menuitemcheckbox", { name: "Favourited" });
+ await user.click(favouriteOption);
- const leaveRoomOption = screen.getByRole("menuitem", { name: "Leave room" });
- await user.click(leaveRoomOption);
+ expect(Open.args.onToggleFavorite).toHaveBeenCalled();
+ });
- expect(mockCallbacks.onLeaveRoom).toHaveBeenCalled();
+ it("should call onToggleLowPriority when low priority is clicked", async () => {
+ const user = userEvent.setup();
+ const { container } = render();
+ await Open.play?.({ canvasElement: container });
+
+ const lowPriorityOption = screen.getByRole("menuitemcheckbox", { name: "Low priority" });
+ await user.click(lowPriorityOption);
+
+ expect(Open.args.onToggleLowPriority).toHaveBeenCalled();
});
});
diff --git a/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItemMoreOptionsMenu.stories.test.tsx.snap b/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItemMoreOptionsMenu.stories.test.tsx.snap
new file mode 100644
index 0000000000..424ea36322
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItemMoreOptionsMenu.stories.test.tsx.snap
@@ -0,0 +1,312 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[` stories > renders Default story (closed) 1`] = `
+
+`;
+
+exports[` stories > renders Open story 1`] = `
+
+`;
+
+exports[` stories > renders OpenCanMarkAsRead story 1`] = `
+
+`;
+
+exports[` stories > renders OpenFavouriteOn story 1`] = `
+
+`;
+
+exports[` stories > renders OpenLowPriorityOn story 1`] = `
+
+`;
+
+exports[` stories > renders OpenWithoutCopyLink story 1`] = `
+
+`;
+
+exports[` stories > renders OpenWithoutInvite story 1`] = `
+
+`;
diff --git a/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItemMoreOptionsMenu.test.tsx.snap b/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItemMoreOptionsMenu.test.tsx.snap
new file mode 100644
index 0000000000..424ea36322
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItemMoreOptionsMenu.test.tsx.snap
@@ -0,0 +1,312 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[` stories > renders Default story (closed) 1`] = `
+
+`;
+
+exports[` stories > renders Open story 1`] = `
+
+`;
+
+exports[` stories > renders OpenCanMarkAsRead story 1`] = `
+
+`;
+
+exports[` stories > renders OpenFavouriteOn story 1`] = `
+
+`;
+
+exports[` stories > renders OpenLowPriorityOn story 1`] = `
+
+`;
+
+exports[` stories > renders OpenWithoutCopyLink story 1`] = `
+
+`;
+
+exports[` stories > renders OpenWithoutInvite story 1`] = `
+
+`;