/* Copyright 2024 New Vector Ltd. Copyright 2022, 2023 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import { act, findByRole, getByRole, queryByRole, render, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { ThreepidMedium, IPushRules, MatrixClient, NotificationCountType, PushRuleKind, Room, RuleId, } from "matrix-js-sdk/src/matrix"; import React from "react"; import NotificationSettings2 from "../../../../../src/components/views/settings/notifications/NotificationSettings2"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import { StandardActions } from "../../../../../src/notifications/StandardActions"; import { mkMessage, stubClient } from "../../../../test-utils"; import Mock = jest.Mock; const waitForUpdate = (): Promise => new Promise((resolve) => setTimeout(resolve)); const labelGlobalMute = "Enable notifications for this account"; const labelLevelAllMessage = "All messages"; const labelLevelMentionsOnly = "Mentions and Keywords only"; const labelSoundPeople = "People"; const labelSoundMentions = "Mentions and Keywords"; const labelSoundCalls = "Audio and Video calls"; const labelActivityInvites = "Invited to a room"; const labelActivityStatus = "New room activity, upgrades and status messages occur"; const labelActivityBots = "Messages sent by bots"; const labelMentionUser = "Notify when someone mentions using @displayname or @mxid"; const labelMentionRoom = "Notify when someone mentions using @room"; const labelMentionKeyword = "Notify when someone uses a keyword" + "Enter keywords here, or use for spelling variations or nicknames"; const labelResetDefault = "Reset to default settings"; const keywords = ["justjann3", "justj4nn3", "justj4nne", "Janne", "J4nne", "Jann3", "jann3", "j4nne", "janne"]; describe("", () => { let cli: MatrixClient; let pushRules: IPushRules; beforeAll(async () => { pushRules = (await import("../../../../models/notificationsettings/pushrules_sample.json")) as IPushRules; }); beforeEach(() => { stubClient(); cli = MatrixClientPeg.safeGet(); cli.getPushRules = jest.fn(cli.getPushRules).mockResolvedValue(pushRules); cli.supportsIntentionalMentions = jest.fn(cli.supportsIntentionalMentions).mockReturnValue(false); cli.setPushRuleEnabled = jest.fn(cli.setPushRuleEnabled); cli.setPushRuleActions = jest.fn(cli.setPushRuleActions); cli.addPushRule = jest.fn(cli.addPushRule).mockResolvedValue({}); cli.deletePushRule = jest.fn(cli.deletePushRule).mockResolvedValue({}); cli.removePusher = jest.fn(cli.removePusher).mockResolvedValue({}); cli.setPusher = jest.fn(cli.setPusher).mockResolvedValue({}); }); it("matches the snapshot", async () => { cli.getPushers = jest.fn(cli.getPushers).mockResolvedValue({ pushers: [ { app_display_name: "Element", app_id: "im.vector.app", data: {}, device_display_name: "My EyeFon", kind: "http", lang: "en", pushkey: "", enabled: true, }, ], }); cli.getThreePids = jest.fn(cli.getThreePids).mockResolvedValue({ threepids: [ { medium: ThreepidMedium.Email, address: "test@example.tld", validated_at: 1656633600, added_at: 1656633600, }, ], }); const screen = render( , ); await act(waitForUpdate); expect(screen.container).toMatchSnapshot(); }); it("correctly handles the loading/disabled state", async () => { (cli.getPushRules as Mock).mockReturnValue(new Promise(() => {})); const user = userEvent.setup(); const screen = render( , ); await act(async () => { await waitForUpdate(); expect(screen.container).toMatchSnapshot(); const globalMute = screen.getByLabelText(labelGlobalMute); expect(globalMute).toHaveAttribute("aria-disabled", "true"); const levelAllMessages = screen.getByLabelText(labelLevelAllMessage); expect(levelAllMessages).toBeDisabled(); const soundPeople = screen.getByLabelText(labelSoundPeople); expect(soundPeople).toBeDisabled(); const soundMentions = screen.getByLabelText(labelSoundMentions); expect(soundMentions).toBeDisabled(); const soundCalls = screen.getByLabelText(labelSoundCalls); expect(soundCalls).toBeDisabled(); const activityInvites = screen.getByLabelText(labelActivityInvites); expect(activityInvites).toBeDisabled(); const activityStatus = screen.getByLabelText(labelActivityStatus); expect(activityStatus).toBeDisabled(); const activityBots = screen.getByLabelText(labelActivityBots); expect(activityBots).toBeDisabled(); const mentionUser = screen.getByLabelText(labelMentionUser.replace("@mxid", cli.getUserId()!)); expect(mentionUser).toBeDisabled(); const mentionRoom = screen.getByLabelText(labelMentionRoom); expect(mentionRoom).toBeDisabled(); const mentionKeyword = screen.getByLabelText(labelMentionKeyword); expect(mentionKeyword).toBeDisabled(); await Promise.all([ user.click(globalMute), user.click(levelAllMessages), user.click(soundPeople), user.click(soundMentions), user.click(soundCalls), user.click(activityInvites), user.click(activityStatus), user.click(activityBots), user.click(mentionUser), user.click(mentionRoom), user.click(mentionKeyword), ]); }); expect(cli.setPushRuleActions).not.toHaveBeenCalled(); expect(cli.setPushRuleEnabled).not.toHaveBeenCalled(); expect(cli.addPushRule).not.toHaveBeenCalled(); expect(cli.deletePushRule).not.toHaveBeenCalled(); }); describe("form elements actually toggle the model value", () => { it("global mute", async () => { const label = labelGlobalMute; const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); expect(screen.getByLabelText(label)).not.toBeDisabled(); await act(async () => { await user.click(screen.getByLabelText(label)); await waitForUpdate(); }); expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.Master, true); }); it("notification level", async () => { const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); expect(screen.getByLabelText(labelLevelAllMessage)).not.toBeDisabled(); await act(async () => { await user.click(screen.getByLabelText(labelLevelAllMessage)); await waitForUpdate(); }); expect(cli.setPushRuleEnabled).toHaveBeenCalledWith( "global", PushRuleKind.Underride, RuleId.EncryptedMessage, true, ); expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.Message, true); (cli.setPushRuleEnabled as Mock).mockClear(); expect(screen.getByLabelText(labelLevelMentionsOnly)).not.toBeDisabled(); await act(async () => { await user.click(screen.getByLabelText(labelLevelMentionsOnly)); await waitForUpdate(); }); expect(cli.setPushRuleEnabled).toHaveBeenCalledWith( "global", PushRuleKind.Underride, RuleId.EncryptedDM, true, ); expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.DM, true); }); describe("play a sound for", () => { it("people", async () => { const label = labelSoundPeople; const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); expect(screen.getByLabelText(label)).not.toBeDisabled(); await act(async () => { await user.click(screen.getByLabelText(label)); await waitForUpdate(); }); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Underride, RuleId.EncryptedDM, StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, ); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Underride, RuleId.DM, StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, ); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Override, RuleId.InviteToSelf, StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, ); }); it("mentions", async () => { const label = labelSoundMentions; const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); expect(screen.getByLabelText(label)).not.toBeDisabled(); await act(async () => { await user.click(screen.getByLabelText(label)); await waitForUpdate(); }); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Override, RuleId.ContainsDisplayName, StandardActions.ACTION_HIGHLIGHT, ); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.ContentSpecific, RuleId.ContainsUserName, StandardActions.ACTION_HIGHLIGHT, ); }); it("calls", async () => { const label = labelSoundCalls; const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); expect(screen.getByLabelText(label)).not.toBeDisabled(); await act(async () => { await user.click(screen.getByLabelText(label)); await waitForUpdate(); }); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Underride, RuleId.IncomingCall, StandardActions.ACTION_NOTIFY, ); }); }); describe("activity", () => { it("invite", async () => { const label = labelActivityInvites; const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); expect(screen.getByLabelText(label)).not.toBeDisabled(); await act(async () => { await user.click(screen.getByLabelText(label)); await waitForUpdate(); }); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Override, RuleId.InviteToSelf, StandardActions.ACTION_NOTIFY, ); }); it("status messages", async () => { const label = labelActivityStatus; const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); expect(screen.getByLabelText(label)).not.toBeDisabled(); await act(async () => { await user.click(screen.getByLabelText(label)); await waitForUpdate(); }); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Override, RuleId.MemberEvent, StandardActions.ACTION_NOTIFY, ); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Override, RuleId.Tombstone, StandardActions.ACTION_HIGHLIGHT, ); }); it("notices", async () => { const label = labelActivityBots; const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); expect(screen.getByLabelText(label)).not.toBeDisabled(); await act(async () => { await user.click(screen.getByLabelText(label)); await waitForUpdate(); }); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Override, RuleId.SuppressNotices, StandardActions.ACTION_DONT_NOTIFY, ); }); }); describe("mentions", () => { it("room mentions", async () => { const label = labelMentionRoom; const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); expect(screen.getByLabelText(label)).not.toBeDisabled(); await act(async () => { await user.click(screen.getByLabelText(label)); await waitForUpdate(); }); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Override, RuleId.AtRoomNotification, StandardActions.ACTION_DONT_NOTIFY, ); }); it("user mentions", async () => { const label = labelMentionUser.replace("@mxid", cli.getUserId()!); const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); expect(screen.getByLabelText(label)).not.toBeDisabled(); await act(async () => { await user.click(screen.getByLabelText(label)); await waitForUpdate(); }); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Override, RuleId.ContainsDisplayName, StandardActions.ACTION_DONT_NOTIFY, ); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.ContentSpecific, RuleId.ContainsUserName, StandardActions.ACTION_DONT_NOTIFY, ); }); it("keywords", async () => { const label = labelMentionKeyword; const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); expect(screen.getByLabelText(label)).not.toBeDisabled(); await act(async () => { await user.click(screen.getByLabelText(label)); await waitForUpdate(); }); for (const pattern of keywords) { expect(cli.setPushRuleEnabled).toHaveBeenCalledWith( "global", PushRuleKind.ContentSpecific, pattern, false, ); } }); }); describe("keywords", () => { it("allows adding keywords", async () => { const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); const inputField = screen.getByRole("textbox", { name: "Keyword" }); const addButton = screen.getByRole("button", { name: "Add" }); expect(inputField).not.toBeDisabled(); expect(addButton).not.toBeDisabled(); await act(async () => { await user.type(inputField, "testkeyword"); await user.click(addButton); await waitForUpdate(); }); expect(cli.addPushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "testkeyword", { kind: PushRuleKind.ContentSpecific, rule_id: "testkeyword", enabled: true, default: false, actions: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND, pattern: "testkeyword", }); }); it("allows deleting keywords", async () => { const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); const tag = screen.getByText("justj4nn3"); const deleteButton = getByRole(tag, "button", { name: "Remove" }); expect(deleteButton).not.toBeDisabled(); await act(async () => { await user.click(deleteButton); await waitForUpdate(); }); expect(cli.deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justj4nn3"); }); }); it("resets the model correctly", async () => { const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); const button = screen.getByText(labelResetDefault); expect(button).not.toBeDisabled(); await act(async () => { await user.click(button); await waitForUpdate(); }); expect(cli.setPushRuleEnabled).toHaveBeenCalledWith( "global", PushRuleKind.Underride, RuleId.EncryptedMessage, true, ); expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.Message, true); expect(cli.setPushRuleEnabled).toHaveBeenCalledWith( "global", PushRuleKind.Underride, RuleId.EncryptedDM, true, ); expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.DM, true); expect(cli.setPushRuleEnabled).toHaveBeenCalledWith( "global", PushRuleKind.Override, RuleId.SuppressNotices, false, ); expect(cli.setPushRuleEnabled).toHaveBeenCalledWith( "global", PushRuleKind.Override, RuleId.InviteToSelf, true, ); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Underride, RuleId.EncryptedMessage, StandardActions.ACTION_NOTIFY, ); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Underride, RuleId.Message, StandardActions.ACTION_NOTIFY, ); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Underride, RuleId.EncryptedDM, StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, ); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Underride, RuleId.DM, StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, ); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Override, RuleId.SuppressNotices, StandardActions.ACTION_DONT_NOTIFY, ); expect(cli.setPushRuleActions).toHaveBeenCalledWith( "global", PushRuleKind.Override, RuleId.InviteToSelf, StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, ); for (const pattern of keywords) { expect(cli.deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, pattern); } }); }); describe("pusher settings", () => { it("can create email pushers", async () => { cli.getPushers = jest.fn(cli.getPushers).mockResolvedValue({ pushers: [ { app_display_name: "Element", app_id: "im.vector.app", data: {}, device_display_name: "My EyeFon", kind: "http", lang: "en", pushkey: "", enabled: true, }, ], }); cli.getThreePids = jest.fn(cli.getThreePids).mockResolvedValue({ threepids: [ { medium: ThreepidMedium.Email, address: "test@example.tld", validated_at: 1656633600, added_at: 1656633600, }, ], }); const label = "test@example.tld"; const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); expect(screen.getByLabelText(label)).not.toBeDisabled(); await act(async () => { await user.click(screen.getByLabelText(label)); await waitForUpdate(); }); expect(cli.setPusher).toHaveBeenCalledWith({ app_display_name: "Email Notifications", app_id: "m.email", append: true, data: { brand: "Element" }, device_display_name: "test@example.tld", kind: "email", lang: "en-US", pushkey: "test@example.tld", }); }); it("can remove email pushers", async () => { cli.getPushers = jest.fn(cli.getPushers).mockResolvedValue({ pushers: [ { app_display_name: "Element", app_id: "im.vector.app", data: {}, device_display_name: "My EyeFon", kind: "http", lang: "en", pushkey: "abctest", }, { app_display_name: "Email Notifications", app_id: "m.email", data: { brand: "Element" }, device_display_name: "test@example.tld", kind: "email", lang: "en-US", pushkey: "test@example.tld", }, ], }); cli.getThreePids = jest.fn(cli.getThreePids).mockResolvedValue({ threepids: [ { medium: ThreepidMedium.Email, address: "test@example.tld", validated_at: 1656633600, added_at: 1656633600, }, ], }); const label = "test@example.tld"; const user = userEvent.setup(); const screen = render( , ); await act(waitForUpdate); expect(screen.getByLabelText(label)).not.toBeDisabled(); await act(async () => { await user.click(screen.getByLabelText(label)); await waitForUpdate(); }); expect(cli.removePusher).toHaveBeenCalledWith("test@example.tld", "m.email"); }); }); describe("clear all notifications", () => { it("is hidden when no notifications exist", async () => { const room = new Room("room123", cli, "@alice:example.org"); cli.getRooms = jest.fn(cli.getRooms).mockReturnValue([room]); const { container } = render( , ); await waitForUpdate(); expect( queryByRole(container, "button", { name: "Mark all messages as read", }), ).not.toBeInTheDocument(); }); it("clears all notifications", async () => { const room = new Room("room123", cli, "@alice:example.org"); cli.getRooms = jest.fn(cli.getRooms).mockReturnValue([room]); const message = mkMessage({ event: true, room: "room123", user: "@alice:example.org", ts: 1, }); room.addLiveEvents([message]); room.setUnreadNotificationCount(NotificationCountType.Total, 1); const user = userEvent.setup(); const { container } = render( , ); await waitForUpdate(); const clearNotificationEl = await findByRole(container, "button", { name: "Mark all messages as read", }); await act(async () => { await user.click(clearNotificationEl); await waitForUpdate(); }); expect(cli.sendReadReceipt).toHaveBeenCalled(); await waitFor(() => { expect(clearNotificationEl).not.toBeInTheDocument(); }); }); }); });