diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItemNotificationMenu.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItemNotificationMenu.stories.tsx/default-auto.png
new file mode 100644
index 0000000000..039b8d9253
Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItemNotificationMenu.stories.tsx/default-auto.png differ
diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItemNotificationMenu.stories.tsx/muted-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItemNotificationMenu.stories.tsx/muted-auto.png
new file mode 100644
index 0000000000..8c7d5de46b
Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItemNotificationMenu.stories.tsx/muted-auto.png differ
diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.stories.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.stories.tsx
new file mode 100644
index 0000000000..e0b5dbc09b
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.stories.tsx
@@ -0,0 +1,130 @@
+/*
+ * 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 { RoomListItemNotificationMenu } from "./RoomListItemNotificationMenu";
+import { type RoomListItemSnapshot, type RoomListItemActions } from "./RoomListItem";
+import { useMockedViewModel } from "../../viewmodel";
+import { defaultSnapshot } from "./default-snapshot";
+import { RoomNotifState } from "./RoomNotifs";
+
+type NotificationMenuProps = RoomListItemSnapshot & RoomListItemActions;
+
+// Wrapper component that creates a mocked ViewModel
+const NotificationMenuWrapper = ({
+ onOpenRoom,
+ onMarkAsRead,
+ onMarkAsUnread,
+ onToggleFavorite,
+ onToggleLowPriority,
+ onInvite,
+ onCopyRoomLink,
+ onLeaveRoom,
+ onSetRoomNotifState,
+ ...rest
+}: NotificationMenuProps): JSX.Element => {
+ const vm = useMockedViewModel(rest, {
+ onOpenRoom,
+ onMarkAsRead,
+ onMarkAsUnread,
+ onToggleFavorite,
+ onToggleLowPriority,
+ onInvite,
+ onCopyRoomLink,
+ onLeaveRoom,
+ onSetRoomNotifState,
+ });
+ return ;
+};
+
+const meta = {
+ title: "Room List/RoomListItem/NotificationMenu",
+ component: NotificationMenuWrapper,
+ tags: ["autodocs"],
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ args: {
+ ...defaultSnapshot,
+ 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;
+
+export const Default: Story = {
+ args: {
+ roomNotifState: RoomNotifState.AllMessages,
+ },
+};
+
+export const Muted: Story = {
+ args: {
+ roomNotifState: RoomNotifState.Mute,
+ },
+};
+
+export const Open: Story = {
+ args: {
+ roomNotifState: RoomNotifState.AllMessages,
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const trigger = canvas.getByRole("button", { name: "Notification options" });
+ await userEvent.click(trigger);
+ },
+};
+
+export const OpenMuted: Story = {
+ args: {
+ roomNotifState: RoomNotifState.Mute,
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const trigger = canvas.getByRole("button", { name: "Notification options" });
+ await userEvent.click(trigger);
+ },
+};
+
+export const AllMessagesLoud: Story = {
+ args: {
+ roomNotifState: RoomNotifState.AllMessagesLoud,
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const trigger = canvas.getByRole("button", { name: "Notification options" });
+ await userEvent.click(trigger);
+ },
+};
+
+export const MentionsOnly: Story = {
+ args: {
+ roomNotifState: RoomNotifState.MentionsOnly,
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const trigger = canvas.getByRole("button", { name: "Notification options" });
+ await userEvent.click(trigger);
+ },
+};
diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.test.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.test.tsx
index 3f88e2f8a1..af1d32e346 100644
--- a/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.test.tsx
+++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.test.tsx
@@ -5,160 +5,80 @@
* 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 { RoomListItemNotificationMenu } from "./RoomListItemNotificationMenu";
+import * as stories from "./RoomListItemNotificationMenu.stories";
import { RoomNotifState } from "./RoomNotifs";
-import { useMockedViewModel } from "../../viewmodel";
-import type { RoomListItemSnapshot } from "./RoomListItem";
-import { defaultSnapshot } from "./default-snapshot";
-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, Muted, Open, OpenMuted, AllMessagesLoud, MentionsOnly } = composeStories(stories);
- const renderMenu = (roomNotifState: RoomNotifState = RoomNotifState.AllMessages): ReturnType => {
- const TestComponent = (): JSX.Element => {
- const vm = useMockedViewModel(
- {
- ...defaultSnapshot,
- showMoreOptionsMenu: false,
- showNotificationMenu: true,
- roomNotifState,
- } as RoomListItemSnapshot,
- mockCallbacks,
- );
- return ;
- };
- return render();
- };
-
- it("should render the notification menu button", () => {
- renderMenu();
- expect(screen.getByRole("button", { name: "Notification options" })).toBeInTheDocument();
+describe(" stories", () => {
+ it("renders Default story (closed, unmuted)", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
});
- it("should show muted icon when notifications are muted", () => {
- renderMenu(RoomNotifState.Mute);
+ it("renders Muted story (closed, muted icon)", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders Open story", async () => {
+ const { container } = render();
+ // Wait for play function to open the menu
+ await Open.play?.({ canvasElement: container });
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders OpenMuted story", async () => {
+ const { container } = render();
+ // Wait for play function to open the menu
+ await OpenMuted.play?.({ canvasElement: container });
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders AllMessagesLoud story", async () => {
+ const { container } = render();
+ // Wait for play function to open the menu
+ await AllMessagesLoud.play?.({ canvasElement: container });
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders MentionsOnly story", async () => {
+ const { container } = render();
+ // Wait for play function to open the menu
+ await MentionsOnly.play?.({ canvasElement: container });
+ expect(container).toMatchSnapshot();
+ });
+
+ it("should show unmuted icon by default", () => {
+ render();
const button = screen.getByRole("button", { name: "Notification options" });
- expect(button.querySelector("svg")).toBeInTheDocument();
+ expect(button).toBeInTheDocument();
});
- it("should open menu when clicked", async () => {
+ it("should show muted icon when muted", () => {
+ render();
+ const button = screen.getByRole("button", { name: "Notification options" });
+ expect(button).toBeInTheDocument();
+ });
+
+ it("should call onSetRoomNotifState when menu item is clicked", async () => {
const user = userEvent.setup();
- renderMenu();
-
- const button = screen.getByRole("button", { name: "Notification options" });
- await user.click(button);
+ const { container } = render();
+ await Open.play?.({ canvasElement: container });
+ // Menu should be open
expect(screen.getByRole("menu")).toBeInTheDocument();
- });
-
- it("should call onSetRoomNotifState with AllMessages when default settings selected", async () => {
- const user = userEvent.setup();
- renderMenu();
-
- const button = screen.getByRole("button", { name: "Notification options" });
- await user.click(button);
-
- const defaultOption = screen.getByRole("menuitem", { name: "Match default settings" });
- await user.click(defaultOption);
-
- expect(mockCallbacks.onSetRoomNotifState).toHaveBeenCalledWith(RoomNotifState.AllMessages);
- });
-
- it("should call onSetRoomNotifState with AllMessagesLoud when all messages selected", async () => {
- const user = userEvent.setup();
- renderMenu();
-
- const button = screen.getByRole("button", { name: "Notification options" });
- await user.click(button);
-
- const allMessagesOption = screen.getByRole("menuitem", { name: "All messages" });
- await user.click(allMessagesOption);
-
- expect(mockCallbacks.onSetRoomNotifState).toHaveBeenCalledWith(RoomNotifState.AllMessagesLoud);
- });
-
- it("should call onSetRoomNotifState with MentionsOnly when mentions and keywords selected", async () => {
- const user = userEvent.setup();
- renderMenu();
-
- const button = screen.getByRole("button", { name: "Notification options" });
- await user.click(button);
-
- const mentionsOption = screen.getByRole("menuitem", { name: "Mentions and keywords" });
- await user.click(mentionsOption);
-
- expect(mockCallbacks.onSetRoomNotifState).toHaveBeenCalledWith(RoomNotifState.MentionsOnly);
- });
-
- it("should call onSetRoomNotifState with Mute when mute selected", async () => {
- const user = userEvent.setup();
- renderMenu();
-
- const button = screen.getByRole("button", { name: "Notification options" });
- await user.click(button);
+ // Click on "Mute room" option
const muteOption = screen.getByRole("menuitem", { name: "Mute room" });
await user.click(muteOption);
- expect(mockCallbacks.onSetRoomNotifState).toHaveBeenCalledWith(RoomNotifState.Mute);
- });
-
- it("should show check mark next to selected option - AllMessage", async () => {
- const user = userEvent.setup();
- renderMenu(RoomNotifState.AllMessages);
-
- const button = screen.getByRole("button", { name: "Notification options" });
- await user.click(button);
-
- const defaultOption = screen.getByRole("menuitem", { name: "Match default settings" });
- expect(defaultOption).toHaveAttribute("aria-selected", "true");
- });
-
- it("should show check mark next to selected option - AllMessagesLoud", async () => {
- const user = userEvent.setup();
- renderMenu(RoomNotifState.AllMessagesLoud);
-
- const button = screen.getByRole("button", { name: "Notification options" });
- await user.click(button);
-
- const allMessagesOption = screen.getByRole("menuitem", { name: "All messages" });
- expect(allMessagesOption).toHaveAttribute("aria-selected", "true");
- });
-
- it("should show check mark next to selected option - MentionsOnly", async () => {
- const user = userEvent.setup();
- renderMenu(RoomNotifState.MentionsOnly);
-
- const button = screen.getByRole("button", { name: "Notification options" });
- await user.click(button);
-
- const mentionsOption = screen.getByRole("menuitem", { name: "Mentions and keywords" });
- expect(mentionsOption).toHaveAttribute("aria-selected", "true");
- });
-
- it("should show check mark next to selected option - Mute", async () => {
- const user = userEvent.setup();
- renderMenu(RoomNotifState.Mute);
-
- const button = screen.getByRole("button", { name: "Notification options" });
- await user.click(button);
-
- const muteOption = screen.getByRole("menuitem", { name: "Mute room" });
- expect(muteOption).toHaveAttribute("aria-selected", "true");
+ expect(Open.args.onSetRoomNotifState).toHaveBeenCalledWith(RoomNotifState.Mute);
});
});
diff --git a/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItemNotificationMenu.stories.test.tsx.snap b/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItemNotificationMenu.stories.test.tsx.snap
new file mode 100644
index 0000000000..48870d3c21
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItemNotificationMenu.stories.test.tsx.snap
@@ -0,0 +1,269 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[` stories > renders AllMessagesLoud story 1`] = `
+
+`;
+
+exports[` stories > renders Default story (closed, unmuted) 1`] = `
+
+`;
+
+exports[` stories > renders MentionsOnly story 1`] = `
+
+`;
+
+exports[` stories > renders Muted story (closed, muted icon) 1`] = `
+
+`;
+
+exports[` stories > renders Open story 1`] = `
+
+`;
+
+exports[` stories > renders OpenMuted story 1`] = `
+
+`;
diff --git a/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItemNotificationMenu.test.tsx.snap b/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItemNotificationMenu.test.tsx.snap
new file mode 100644
index 0000000000..48870d3c21
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItemNotificationMenu.test.tsx.snap
@@ -0,0 +1,269 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[` stories > renders AllMessagesLoud story 1`] = `
+
+`;
+
+exports[` stories > renders Default story (closed, unmuted) 1`] = `
+
+`;
+
+exports[` stories > renders MentionsOnly story 1`] = `
+
+`;
+
+exports[` stories > renders Muted story (closed, muted icon) 1`] = `
+
+`;
+
+exports[` stories > renders Open story 1`] = `
+
+`;
+
+exports[` stories > renders OpenMuted story 1`] = `
+
+`;