/* Copyright 2022-2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ 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(); }); }); }); });