diff --git a/package.json b/package.json index d8c0a6b1ba..53886be6bd 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "@types/png-chunks-extract": "^1.0.2", "@types/react-virtualized": "^9.21.30", "@vector-im/compound-design-tokens": "^4.0.0", - "@vector-im/compound-web": "^7.7.2", + "@vector-im/compound-web": "^7.9.0", "@vector-im/matrix-wysiwyg": "2.38.2", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", diff --git a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts index ed402bc625..49966ecca1 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts @@ -13,6 +13,9 @@ test.describe("Room list", () => { test.use({ displayName: "Alice", labsFlags: ["feature_new_room_list"], + botCreateOpts: { + displayName: "BotBob", + }, }); /** @@ -26,71 +29,195 @@ test.describe("Room list", () => { test.beforeEach(async ({ page, app, user }) => { // The notification toast is displayed above the search section await app.closeNotificationToast(); - for (let i = 0; i < 30; i++) { - await app.client.createRoom({ name: `room${i}` }); - } }); - test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => { - const roomListView = getRoomList(page); - await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible(); - await expect(roomListView).toMatchScreenshot("room-list.png"); + test.describe("Room list", () => { + test.beforeEach(async ({ page, app, user }) => { + for (let i = 0; i < 30; i++) { + await app.client.createRoom({ name: `room${i}` }); + } + }); - await roomListView.hover(); - // Scroll to the end of the room list - await page.mouse.wheel(0, 1000); - await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); - await expect(roomListView).toMatchScreenshot("room-list-scrolled.png"); + test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => { + const roomListView = getRoomList(page); + await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible(); + await expect(roomListView).toMatchScreenshot("room-list.png"); + + await roomListView.hover(); + // Scroll to the end of the room list + await page.mouse.wheel(0, 1000); + await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); + await expect(roomListView).toMatchScreenshot("room-list-scrolled.png"); + }); + + test("should open the room when it is clicked", async ({ page, app, user }) => { + const roomListView = getRoomList(page); + await roomListView.getByRole("gridcell", { name: "Open room room29" }).click(); + await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible(); + }); + + test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => { + const roomListView = getRoomList(page); + const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" }); + await roomItem.hover(); + + await expect(roomItem).toMatchScreenshot("room-list-item-hover.png"); + const roomItemMenu = roomItem.getByRole("button", { name: "More Options" }); + await roomItemMenu.click(); + await expect(page).toMatchScreenshot("room-list-item-open-more-options.png"); + + // It should make the room favourited + await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click(); + + // Check that the room is favourited + await roomItem.hover(); + await roomItemMenu.click(); + await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked(); + // It should show the invite dialog + await page.getByRole("menuitem", { name: "invite" }).click(); + await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible(); + await app.closeDialog(); + + // It should leave the room + await roomItem.hover(); + await roomItemMenu.click(); + await page.getByRole("menuitem", { name: "leave room" }).click(); + await expect(roomItem).not.toBeVisible(); + }); + + test("should scroll to the current room", async ({ page, app, user }) => { + const roomListView = getRoomList(page); + await roomListView.hover(); + // Scroll to the end of the room list + await page.mouse.wheel(0, 1000); + + await roomListView.getByRole("gridcell", { name: "Open room room0" }).click(); + + const filters = page.getByRole("listbox", { name: "Room list filters" }); + await filters.getByRole("option", { name: "People" }).click(); + await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible(); + + await filters.getByRole("option", { name: "People" }).click(); + await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); + }); }); - test("should open the room when it is clicked", async ({ page, app, user }) => { - const roomListView = getRoomList(page); - await roomListView.getByRole("gridcell", { name: "Open room room29" }).click(); - await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible(); - }); + test.describe("Notification decoration", () => { + test("should render the invitation decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); - test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => { - const roomListView = getRoomList(page); - const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" }); - await roomItem.hover(); + await bot.createRoom({ + name: "invited room", + invite: [user.userId], + is_direct: true, + }); + const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" }); + await expect(invitedRoom).toBeVisible(); + await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png"); + }); - await expect(roomItem).toMatchScreenshot("room-list-item-hover.png"); - const roomItemMenu = roomItem.getByRole("button", { name: "More Options" }); - await roomItemMenu.click(); - await expect(page).toMatchScreenshot("room-list-item-open-more-options.png"); + test("should render the regular decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); - // It should make the room favourited - await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click(); + const roomId = await app.client.createRoom({ name: "2 notifications" }); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); - // Check that the room is favourited - await roomItem.hover(); - await roomItemMenu.click(); - await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked(); - // It should show the invite dialog - await page.getByRole("menuitem", { name: "invite" }).click(); - await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible(); - await app.closeDialog(); + await bot.sendMessage(roomId, "I am a robot. Beep."); + await bot.sendMessage(roomId, "I am a robot. Beep."); - // It should leave the room - await roomItem.hover(); - await roomItemMenu.click(); - await page.getByRole("menuitem", { name: "leave room" }).click(); - await expect(roomItem).not.toBeVisible(); - }); + const room = roomListView.getByRole("gridcell", { name: "2 notifications" }); + await expect(room).toBeVisible(); + await expect(room.getByTestId("notification-decoration")).toHaveText("2"); + await expect(room).toMatchScreenshot("room-list-item-notification.png"); + }); - test("should scroll to the current room", async ({ page, app, user }) => { - const roomListView = getRoomList(page); - await roomListView.hover(); - // Scroll to the end of the room list - await page.mouse.wheel(0, 1000); + test("should render the mention decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); - await roomListView.getByRole("gridcell", { name: "Open room room0" }).click(); + const roomId = await app.client.createRoom({ name: "mention" }); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); - const filters = page.getByRole("listbox", { name: "Room list filters" }); - await filters.getByRole("option", { name: "People" }).click(); - await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible(); + const clientBot = await bot.prepareClient(); + await clientBot.evaluate( + async (client, { roomId, userId }) => { + await client.sendMessage(roomId, { + // @ts-ignore ignore usage of MsgType.text + "msgtype": "m.text", + "body": "User", + "format": "org.matrix.custom.html", + "formatted_body": `User`, + "m.mentions": { + user_ids: [userId], + }, + }); + }, + { roomId, userId: user.userId }, + ); + await bot.sendMessage(roomId, "I am a robot. Beep."); - await filters.getByRole("option", { name: "People" }).click(); - await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); + const room = roomListView.getByRole("gridcell", { name: "mention" }); + await expect(room).toBeVisible(); + await expect(room).toMatchScreenshot("room-list-item-mention.png"); + }); + + test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); + + const roomId = await app.client.createRoom({ name: "activity" }); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + + await app.viewRoomById(roomId); + await app.settings.openRoomSettings("Notifications"); + await page.getByText("@mentions & keywords").click(); + await app.settings.closeDialog(); + + await app.settings.openUserSettings("Notifications"); + await page.getByText("Show all activity in the room list (dots or number of unread messages)").click(); + await app.settings.closeDialog(); + + await bot.sendMessage(roomId, "I am a robot. Beep."); + + const room = roomListView.getByRole("gridcell", { name: "activity" }); + await expect(room.getByTestId("notification-decoration")).toBeVisible(); + await expect(room).toMatchScreenshot("room-list-item-activity.png"); + }); + + test("should render a mark as unread decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); + + const roomId = await app.client.createRoom({ name: "mark as unread" }); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + + const room = roomListView.getByRole("gridcell", { name: "mark as unread" }); + await room.hover(); + await room.getByRole("button", { name: "More Options" }).click(); + await page.getByRole("menuitem", { name: "mark as unread" }).click(); + + // Remove hover on the room list item + await roomListView.hover(); + + await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png"); + }); + + test("should render silent decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); + + const roomId = await app.client.createRoom({ name: "silent" }); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + + await app.viewRoomById(roomId); + await app.settings.openRoomSettings("Notifications"); + await page.getByText("Off").click(); + await app.settings.closeDialog(); + + const room = roomListView.getByRole("gridcell", { name: "silent" }); + await expect(room.getByTestId("notification-decoration")).toBeVisible(); + await expect(room).toMatchScreenshot("room-list-item-silent.png"); + }); }); }); diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png new file mode 100644 index 0000000000..436fe129c4 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png new file mode 100644 index 0000000000..c6d11fc65c Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mark-as-unread-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mark-as-unread-linux.png new file mode 100644 index 0000000000..784ac1b1be Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mark-as-unread-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mention-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mention-linux.png new file mode 100644 index 0000000000..5e04d46ba0 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mention-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-notification-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-notification-linux.png new file mode 100644 index 0000000000..f5a542c93b Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-notification-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-silent-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-silent-linux.png new file mode 100644 index 0000000000..7922675ba3 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-silent-linux.png differ diff --git a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx index 9e38e6e8d8..1d5d9aba11 100644 --- a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx @@ -5,13 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; import dispatcher from "../../../dispatcher/dispatcher"; import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../../dispatcher/actions"; import { hasAccessToOptionsMenu } from "./utils"; +import { _t } from "../../../languageHandler"; +import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState"; +import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; export interface RoomListItemViewState { /** @@ -22,6 +25,14 @@ export interface RoomListItemViewState { * Open the room having given roomId. */ openRoom: () => void; + /** + * The a11y label for the room list item. + */ + a11yLabel: string; + /** + * The notification state of the room. + */ + notificationState: RoomNotificationState; } /** @@ -31,6 +42,8 @@ export interface RoomListItemViewState { export function useRoomListItemViewModel(room: Room): RoomListItemViewState { // incoming: Check notification menu rights const showHoverMenu = hasAccessToOptionsMenu(room); + const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]); + const a11yLabel = getA11yLabel(room, notificationState); // Actions @@ -43,7 +56,38 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState { }, [room]); return { + notificationState, showHoverMenu, openRoom, + a11yLabel, }; } + +/** + * Get the a11y label for the room list item + * @param room + * @param notificationState + */ +function getA11yLabel(room: Room, notificationState: RoomNotificationState): string { + if (notificationState.isUnsetMessage) { + return _t("a11y|room_messsage_not_sent", { + roomName: room.name, + }); + } else if (notificationState.invited) { + return _t("a11y|room_n_unread_invite", { + roomName: room.name, + }); + } else if (notificationState.isMention) { + return _t("a11y|room_n_unread_messages_mentions", { + roomName: room.name, + count: notificationState.count, + }); + } else if (notificationState.hasUnreadCount) { + return _t("a11y|room_n_unread_messages", { + roomName: room.name, + count: notificationState.count, + }); + } else { + return _t("room_list|room|open_room", { roomName: room.name }); + } +} diff --git a/src/components/views/rooms/NotificationDecoration.tsx b/src/components/views/rooms/NotificationDecoration.tsx new file mode 100644 index 0000000000..2d993c2e82 --- /dev/null +++ b/src/components/views/rooms/NotificationDecoration.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2025 New Vector 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 HTMLProps, type JSX } from "react"; +import MentionIcon from "@vector-im/compound-design-tokens/assets/web/icons/mention"; +import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error"; +import NotificationOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications-off-solid"; +import { UnreadCounter, Unread } from "@vector-im/compound-web"; + +import { Flex } from "../../utils/Flex"; +import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState"; + +interface NotificationDecorationProps extends HTMLProps { + /** + * The notification state of the room or thread. + */ + notificationState: RoomNotificationState; +} + +/** + * Displays the notification decoration for a room or a thread. + */ +export function NotificationDecoration({ + notificationState, + ...props +}: NotificationDecorationProps): JSX.Element | null { + const { + hasAnyNotificationOrActivity, + isUnsetMessage, + invited, + isMention, + isActivityNotification, + isNotification, + count, + muted, + } = notificationState; + if (!hasAnyNotificationOrActivity && !muted) return null; + + return ( + + {isUnsetMessage && } + {invited && } + {isMention && } + {(isMention || isNotification) && } + {isActivityNotification && } + {muted && } + + ); +} diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx index 37ad4ec848..151ca416e2 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx @@ -12,8 +12,8 @@ import classNames from "classnames"; import { useRoomListItemViewModel } from "../../../viewmodels/roomlist/RoomListItemViewModel"; import DecoratedRoomAvatar from "../../avatars/DecoratedRoomAvatar"; import { Flex } from "../../../utils/Flex"; -import { _t } from "../../../../languageHandler"; import { RoomListItemMenuView } from "./RoomListItemMenuView"; +import { NotificationDecoration } from "../NotificationDecoration"; interface RoomListItemViewPropsProps extends React.HTMLAttributes { /** @@ -46,7 +46,7 @@ export function RoomListItemView({ room, isSelected, ...props }: RoomListItemVie })} type="button" aria-selected={isSelected} - aria-label={_t("room_list|room|open_room", { roomName: room.name })} + aria-label={vm.a11yLabel} onClick={() => vm.openRoom()} onMouseOver={() => setIsHover(true)} onMouseOut={() => setIsHover(false)} @@ -65,7 +65,7 @@ export function RoomListItemView({ room, isSelected, ...props }: RoomListItemVie > {/* We truncate the room name when too long. Title here is to show the full name on hover */} {room.name} - {showHoverDecoration && ( + {showHoverDecoration ? ( { @@ -74,6 +74,11 @@ export function RoomListItemView({ room, isSelected, ...props }: RoomListItemVie else setTimeout(() => setIsMenuOpen(isOpen), 0); }} /> + ) : ( + <> + {/* aria-hidden because we summarise the unread count/notification status in a11yLabel variable */} + + )} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ccf14fa4ba..b7339fd9f4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -12,6 +12,16 @@ "other": "%(count)s unread messages including mentions." }, "recent_rooms": "Recent rooms", + "room_messsage_not_sent": "Open room %(roomName)s with an unset message.", + "room_n_unread_invite": "Open room %(roomName)s invitation.", + "room_n_unread_messages": { + "one": "Open room %(roomName)s with 1 unread message.", + "other": "Open room %(roomName)s with %(count)s unread messages." + }, + "room_n_unread_messages_mentions": { + "one": "Open room %(roomName)s with 1 unread mention.", + "other": "Open room %(roomName)s with %(count)s unread messages including mentions." + }, "room_name": "Room %(name)s", "room_status_bar": "Room status bar", "seek_bar_label": "Audio seek bar", diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx index 2854c433e7..caba9abd1e 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx @@ -14,6 +14,8 @@ import { Action } from "../../../../../src/dispatcher/actions"; import { useRoomListItemViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel"; import { createTestClient, mkStubRoom } from "../../../../test-utils"; import { hasAccessToOptionsMenu } from "../../../../../src/components/viewmodels/roomlist/utils"; +import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState"; +import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore"; jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({ hasAccessToOptionsMenu: jest.fn().mockReturnValue(false), @@ -46,4 +48,49 @@ describe("RoomListItemViewModel", () => { const { result: vm } = renderHook(() => useRoomListItemViewModel(room)); expect(vm.current.showHoverMenu).toBe(true); }); + + describe("a11yLabel", () => { + let notificationState: RoomNotificationState; + beforeEach(() => { + notificationState = new RoomNotificationState(room, false); + jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState); + }); + + it.each([ + { + label: "unset message", + mock: () => jest.spyOn(notificationState, "isUnsetMessage", "get").mockReturnValue(true), + expected: "Open room roomName with an unset message.", + }, + { + label: "invitation", + mock: () => jest.spyOn(notificationState, "invited", "get").mockReturnValue(true), + expected: "Open room roomName invitation.", + }, + { + label: "mention", + mock: () => { + jest.spyOn(notificationState, "isMention", "get").mockReturnValue(true); + jest.spyOn(notificationState, "count", "get").mockReturnValue(3); + }, + expected: "Open room roomName with 3 unread messages including mentions.", + }, + { + label: "unread", + mock: () => { + jest.spyOn(notificationState, "hasUnreadCount", "get").mockReturnValue(true); + jest.spyOn(notificationState, "count", "get").mockReturnValue(3); + }, + expected: "Open room roomName with 3 unread messages.", + }, + { + label: "default", + expected: "Open room roomName", + }, + ])("should return the $label label", ({ mock, expected }) => { + mock?.(); + const { result: vm } = renderHook(() => useRoomListItemViewModel(room)); + expect(vm.current.a11yLabel).toBe(expected); + }); + }); }); diff --git a/test/unit-tests/components/views/rooms/NotificationDecoration-test.tsx b/test/unit-tests/components/views/rooms/NotificationDecoration-test.tsx new file mode 100644 index 0000000000..dd3ae7bc31 --- /dev/null +++ b/test/unit-tests/components/views/rooms/NotificationDecoration-test.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2025 New Vector 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 from "react"; +import { render, screen } from "jest-matrix-react"; + +import { type RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState"; +import { NotificationDecoration } from "../../../../../src/components/views/rooms/NotificationDecoration"; + +describe("", () => { + it("should not render if RoomNotificationState.isSilent=true", () => { + const state = { hasAnyNotificationOrActivity: false } as RoomNotificationState; + render(); + expect(screen.queryByTestId("notification-decoration")).toBeNull(); + }); + + it("should render the unset message decoration", () => { + const state = { hasAnyNotificationOrActivity: true, isUnsetMessage: true } as RoomNotificationState; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should render the invitation decoration", () => { + const state = { hasAnyNotificationOrActivity: true, invited: true } as RoomNotificationState; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should render the mention decoration", () => { + const state = { hasAnyNotificationOrActivity: true, isMention: true, count: 1 } as RoomNotificationState; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should render the notification decoration", () => { + const state = { hasAnyNotificationOrActivity: true, isNotification: true, count: 1 } as RoomNotificationState; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should render the notification decoration without count", () => { + const state = { hasAnyNotificationOrActivity: true, isNotification: true, count: 0 } as RoomNotificationState; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should render the activity decoration", () => { + const state = { hasAnyNotificationOrActivity: true, isActivityNotification: true } as RoomNotificationState; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should render the muted decoration", () => { + const state = { hasAnyNotificationOrActivity: true, muted: true } as RoomNotificationState; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx index 217015451c..d08b24667b 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx @@ -18,26 +18,32 @@ import { type RoomListItemViewState, useRoomListItemViewModel, } from "../../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel"; +import { RoomNotificationState } from "../../../../../../src/stores/notifications/RoomNotificationState"; jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel", () => ({ useRoomListItemViewModel: jest.fn(), })); describe("", () => { - const defaultValue: RoomListItemViewState = { - openRoom: jest.fn(), - showHoverMenu: false, - }; + let defaultValue: RoomListItemViewState; let matrixClient: MatrixClient; let room: Room; beforeEach(() => { - mocked(useRoomListItemViewModel).mockReturnValue(defaultValue); matrixClient = stubClient(); room = mkRoom(matrixClient, "room1"); DMRoomMap.makeShared(matrixClient); jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(null); + + defaultValue = { + openRoom: jest.fn(), + showHoverMenu: false, + notificationState: new RoomNotificationState(room, false), + a11yLabel: "Open room room1", + }; + + mocked(useRoomListItemViewModel).mockReturnValue(defaultValue); }); test("should render a room item", () => { diff --git a/test/unit-tests/components/views/rooms/__snapshots__/NotificationDecoration-test.tsx.snap b/test/unit-tests/components/views/rooms/__snapshots__/NotificationDecoration-test.tsx.snap new file mode 100644 index 0000000000..ee0818e232 --- /dev/null +++ b/test/unit-tests/components/views/rooms/__snapshots__/NotificationDecoration-test.tsx.snap @@ -0,0 +1,137 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render the activity decoration 1`] = ` + +
+
+
+
+
+ +`; + +exports[` should render the invitation decoration 1`] = ` + +
+ + 1 + +
+
+`; + +exports[` should render the mention decoration 1`] = ` + +
+ + + + + 1 + +
+
+`; + +exports[` should render the muted decoration 1`] = ` + +
+ + + + +
+
+`; + +exports[` should render the notification decoration 1`] = ` + +
+ + 1 + +
+
+`; + +exports[` should render the notification decoration without count 1`] = ` + +
+
+
+ +`; + +exports[` should render the unset message decoration 1`] = ` + +
+ + + +
+
+`; diff --git a/yarn.lock b/yarn.lock index d5a5f6d701..cd09507552 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3597,10 +3597,10 @@ resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-4.0.1.tgz#5c4ea7ad664d8e6206dc42e41649c80ef060a760" integrity sha512-V4AsK1FVFxZ6DmmCoeAi8FyvE7ODMlXPWjqRGotcnVaoGNrDQrVz2ZGV85DCz5ISxB3iynYASe6OXsDVXT1zFA== -"@vector-im/compound-web@^7.7.2": - version "7.7.2" - resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.7.2.tgz#07e04a546b86e568b13263092b324efc76398487" - integrity sha512-RhPyKzfPo1HRyFi3wy8oc25IXbLLzTmw6A5QvPJgRlMW+LidwqCCYqmFeZrvWxK3pZPqE7hTJbHgUhGe7kxznw== +"@vector-im/compound-web@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.9.0.tgz#72eccdd501e54f7b88317ba927bfeca61af72de0" + integrity sha512-2rBD+1Mit+kOd7+ZPUxdH7y6V1mi7Fga85cyC2cvUeL/sXBn0Q5HuyJ8whmdgLmgZiI4LkLriCFaeogYipKE+Q== dependencies: "@floating-ui/react" "^0.27.0" "@radix-ui/react-context-menu" "^2.2.1"