461 lines
18 KiB
TypeScript
461 lines
18 KiB
TypeScript
|
/*
|
||
|
Copyright 2024 New Vector Ltd.
|
||
|
Copyright 2015-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 React from "react";
|
||
|
import { render, RenderResult } from "jest-matrix-react";
|
||
|
import { ConditionKind, EventType, IPushRule, MatrixEvent, ClientEvent, PushRuleKind } from "matrix-js-sdk/src/matrix";
|
||
|
import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler";
|
||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||
|
import userEvent from "@testing-library/user-event";
|
||
|
|
||
|
import LoggedInView from "../../../../src/components/structures/LoggedInView";
|
||
|
import { SDKContext } from "../../../../src/contexts/SDKContext";
|
||
|
import { StandardActions } from "../../../../src/notifications/StandardActions";
|
||
|
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
|
||
|
import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
|
||
|
import { TestSdkContext } from "../../TestSdkContext";
|
||
|
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
|
||
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||
|
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||
|
import { Action } from "../../../../src/dispatcher/actions";
|
||
|
import Modal from "../../../../src/Modal";
|
||
|
import { SETTINGS } from "../../../../src/settings/Settings";
|
||
|
|
||
|
describe("<LoggedInView />", () => {
|
||
|
const userId = "@alice:domain.org";
|
||
|
const mockClient = getMockClientWithEventEmitter({
|
||
|
...mockClientMethodsUser(userId),
|
||
|
getAccountData: jest.fn(),
|
||
|
getRoom: jest.fn(),
|
||
|
getSyncState: jest.fn().mockReturnValue(null),
|
||
|
getSyncStateData: jest.fn().mockReturnValue(null),
|
||
|
getMediaHandler: jest.fn(),
|
||
|
setPushRuleEnabled: jest.fn(),
|
||
|
setPushRuleActions: jest.fn(),
|
||
|
getCrypto: jest.fn().mockReturnValue(undefined),
|
||
|
setExtendedProfileProperty: jest.fn().mockResolvedValue(undefined),
|
||
|
deleteExtendedProfileProperty: jest.fn().mockResolvedValue(undefined),
|
||
|
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(true),
|
||
|
});
|
||
|
const mediaHandler = new MediaHandler(mockClient);
|
||
|
const mockSdkContext = new TestSdkContext();
|
||
|
|
||
|
const defaultProps = {
|
||
|
matrixClient: mockClient,
|
||
|
onRegistered: jest.fn(),
|
||
|
resizeNotifier: new ResizeNotifier(),
|
||
|
collapseLhs: false,
|
||
|
hideToSRUsers: false,
|
||
|
config: {
|
||
|
brand: "Test",
|
||
|
element_call: {},
|
||
|
},
|
||
|
currentRoomId: "",
|
||
|
currentUserId: "@bob:server",
|
||
|
};
|
||
|
|
||
|
const getComponent = (props = {}): RenderResult =>
|
||
|
render(<LoggedInView {...defaultProps} {...props} />, {
|
||
|
wrapper: ({ children }) => <SDKContext.Provider value={mockSdkContext}>{children}</SDKContext.Provider>,
|
||
|
});
|
||
|
|
||
|
beforeEach(() => {
|
||
|
jest.clearAllMocks();
|
||
|
mockClient.getMediaHandler.mockReturnValue(mediaHandler);
|
||
|
mockClient.setPushRuleActions.mockReset().mockResolvedValue({});
|
||
|
});
|
||
|
|
||
|
describe("synced push rules", () => {
|
||
|
const pushRulesEvent = new MatrixEvent({ type: EventType.PushRules });
|
||
|
|
||
|
const oneToOneRule = {
|
||
|
conditions: [
|
||
|
{ kind: ConditionKind.RoomMemberCount, is: "2" },
|
||
|
{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.message" },
|
||
|
],
|
||
|
actions: StandardActions.ACTION_NOTIFY,
|
||
|
rule_id: ".m.rule.room_one_to_one",
|
||
|
default: true,
|
||
|
enabled: true,
|
||
|
} as IPushRule;
|
||
|
|
||
|
const oneToOneRuleDisabled = {
|
||
|
...oneToOneRule,
|
||
|
enabled: false,
|
||
|
};
|
||
|
|
||
|
const groupRule = {
|
||
|
conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.message" }],
|
||
|
actions: StandardActions.ACTION_NOTIFY,
|
||
|
rule_id: ".m.rule.message",
|
||
|
default: true,
|
||
|
enabled: true,
|
||
|
} as IPushRule;
|
||
|
|
||
|
const pollStartOneToOne = {
|
||
|
conditions: [
|
||
|
{
|
||
|
kind: ConditionKind.RoomMemberCount,
|
||
|
is: "2",
|
||
|
},
|
||
|
{
|
||
|
kind: ConditionKind.EventMatch,
|
||
|
key: "type",
|
||
|
pattern: "org.matrix.msc3381.poll.start",
|
||
|
},
|
||
|
],
|
||
|
actions: StandardActions.ACTION_NOTIFY,
|
||
|
rule_id: ".org.matrix.msc3930.rule.poll_start_one_to_one",
|
||
|
default: true,
|
||
|
enabled: true,
|
||
|
} as IPushRule;
|
||
|
|
||
|
const pollEndOneToOne = {
|
||
|
conditions: [
|
||
|
{
|
||
|
kind: ConditionKind.RoomMemberCount,
|
||
|
is: "2",
|
||
|
},
|
||
|
{
|
||
|
kind: ConditionKind.EventMatch,
|
||
|
key: "type",
|
||
|
pattern: "org.matrix.msc3381.poll.end",
|
||
|
},
|
||
|
],
|
||
|
actions: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||
|
rule_id: ".org.matrix.msc3930.rule.poll_end_one_to_one",
|
||
|
default: true,
|
||
|
enabled: true,
|
||
|
} as IPushRule;
|
||
|
|
||
|
const pollStartGroup = {
|
||
|
conditions: [
|
||
|
{
|
||
|
kind: ConditionKind.EventMatch,
|
||
|
key: "type",
|
||
|
pattern: "org.matrix.msc3381.poll.start",
|
||
|
},
|
||
|
],
|
||
|
actions: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||
|
rule_id: ".org.matrix.msc3930.rule.poll_start",
|
||
|
default: true,
|
||
|
enabled: true,
|
||
|
} as IPushRule;
|
||
|
|
||
|
beforeEach(() => {
|
||
|
mockClient.getAccountData.mockImplementation((eventType: string) =>
|
||
|
eventType === EventType.PushRules ? pushRulesEvent : undefined,
|
||
|
);
|
||
|
setPushRules([]);
|
||
|
// stub out error logger to avoid littering console
|
||
|
jest.spyOn(logger, "error")
|
||
|
.mockClear()
|
||
|
.mockImplementation(() => {});
|
||
|
|
||
|
mockClient.setPushRuleActions.mockClear();
|
||
|
mockClient.setPushRuleEnabled.mockClear();
|
||
|
});
|
||
|
|
||
|
const setPushRules = (rules: IPushRule[] = []): void => {
|
||
|
const pushRules = {
|
||
|
global: {
|
||
|
underride: [...rules],
|
||
|
},
|
||
|
};
|
||
|
|
||
|
mockClient.pushRules = pushRules;
|
||
|
};
|
||
|
|
||
|
describe("on mount", () => {
|
||
|
it("handles when user has no push rules event in account data", () => {
|
||
|
mockClient.getAccountData.mockReturnValue(undefined);
|
||
|
getComponent();
|
||
|
|
||
|
expect(mockClient.getAccountData).toHaveBeenCalledWith(EventType.PushRules);
|
||
|
expect(logger.error).not.toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it("handles when user doesnt have a push rule defined in vector definitions", () => {
|
||
|
// synced push rules uses VectorPushRulesDefinitions
|
||
|
// rules defined there may not exist in m.push_rules
|
||
|
// mock push rules with group rule, but missing oneToOne rule
|
||
|
setPushRules([pollStartOneToOne, groupRule, pollStartGroup]);
|
||
|
|
||
|
getComponent();
|
||
|
|
||
|
// just called once for one-to-one
|
||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(1);
|
||
|
// set to match primary rule (groupRule)
|
||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||
|
"global",
|
||
|
"underride",
|
||
|
pollStartGroup.rule_id,
|
||
|
StandardActions.ACTION_NOTIFY,
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it("updates all mismatched rules from synced rules", () => {
|
||
|
setPushRules([
|
||
|
// poll 1-1 rules are synced with oneToOneRule
|
||
|
oneToOneRule, // on
|
||
|
pollStartOneToOne, // on
|
||
|
pollEndOneToOne, // loud
|
||
|
// poll group rules are synced with groupRule
|
||
|
groupRule, // on
|
||
|
pollStartGroup, // loud
|
||
|
]);
|
||
|
|
||
|
getComponent();
|
||
|
|
||
|
// only called for rules not in sync with their primary rule
|
||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(2);
|
||
|
// set to match primary rule
|
||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||
|
"global",
|
||
|
"underride",
|
||
|
pollStartGroup.rule_id,
|
||
|
StandardActions.ACTION_NOTIFY,
|
||
|
);
|
||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||
|
"global",
|
||
|
"underride",
|
||
|
pollEndOneToOne.rule_id,
|
||
|
StandardActions.ACTION_NOTIFY,
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it("updates all mismatched rules from synced rules when primary rule is disabled", async () => {
|
||
|
setPushRules([
|
||
|
// poll 1-1 rules are synced with oneToOneRule
|
||
|
oneToOneRuleDisabled, // off
|
||
|
pollStartOneToOne, // on
|
||
|
pollEndOneToOne, // loud
|
||
|
// poll group rules are synced with groupRule
|
||
|
groupRule, // on
|
||
|
pollStartGroup, // loud
|
||
|
]);
|
||
|
|
||
|
getComponent();
|
||
|
|
||
|
await flushPromises();
|
||
|
|
||
|
// set to match primary rule
|
||
|
expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith(
|
||
|
"global",
|
||
|
PushRuleKind.Underride,
|
||
|
pollStartOneToOne.rule_id,
|
||
|
false,
|
||
|
);
|
||
|
expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith(
|
||
|
"global",
|
||
|
PushRuleKind.Underride,
|
||
|
pollEndOneToOne.rule_id,
|
||
|
false,
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it("catches and logs errors while updating a rule", async () => {
|
||
|
mockClient.setPushRuleActions.mockRejectedValueOnce("oups").mockResolvedValueOnce({});
|
||
|
|
||
|
setPushRules([
|
||
|
// poll 1-1 rules are synced with oneToOneRule
|
||
|
oneToOneRule, // on
|
||
|
pollStartOneToOne, // on
|
||
|
pollEndOneToOne, // loud
|
||
|
// poll group rules are synced with groupRule
|
||
|
groupRule, // on
|
||
|
pollStartGroup, // loud
|
||
|
]);
|
||
|
|
||
|
getComponent();
|
||
|
await flushPromises();
|
||
|
|
||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(2);
|
||
|
// both calls made
|
||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||
|
"global",
|
||
|
"underride",
|
||
|
pollStartGroup.rule_id,
|
||
|
StandardActions.ACTION_NOTIFY,
|
||
|
);
|
||
|
// second primary rule still updated after first rule failed
|
||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||
|
"global",
|
||
|
"underride",
|
||
|
pollEndOneToOne.rule_id,
|
||
|
StandardActions.ACTION_NOTIFY,
|
||
|
);
|
||
|
expect(logger.error).toHaveBeenCalledWith(
|
||
|
"Failed to fully synchronise push rules for .m.rule.room_one_to_one",
|
||
|
"oups",
|
||
|
);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe("on changes to account_data", () => {
|
||
|
it("ignores other account data events", () => {
|
||
|
// setup a push rule state with mismatched rules
|
||
|
setPushRules([
|
||
|
// poll 1-1 rules are synced with oneToOneRule
|
||
|
oneToOneRule, // on
|
||
|
pollEndOneToOne, // loud
|
||
|
]);
|
||
|
|
||
|
getComponent();
|
||
|
|
||
|
mockClient.setPushRuleActions.mockClear();
|
||
|
|
||
|
const someOtherAccountData = new MatrixEvent({ type: "my-test-account-data " });
|
||
|
mockClient.emit(ClientEvent.AccountData, someOtherAccountData);
|
||
|
|
||
|
// didnt check rule sync
|
||
|
expect(mockClient.setPushRuleActions).not.toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
it("updates all mismatched rules from synced rules on a change to push rules account data", () => {
|
||
|
// setup a push rule state with mismatched rules
|
||
|
setPushRules([
|
||
|
// poll 1-1 rules are synced with oneToOneRule
|
||
|
oneToOneRule, // on
|
||
|
pollEndOneToOne, // loud
|
||
|
]);
|
||
|
|
||
|
getComponent();
|
||
|
|
||
|
mockClient.setPushRuleActions.mockClear();
|
||
|
|
||
|
mockClient.emit(ClientEvent.AccountData, pushRulesEvent);
|
||
|
|
||
|
// set to match primary rule
|
||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||
|
"global",
|
||
|
"underride",
|
||
|
pollEndOneToOne.rule_id,
|
||
|
StandardActions.ACTION_NOTIFY,
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it("updates all mismatched rules from synced rules on a change to push rules account data when primary rule is disabled", async () => {
|
||
|
// setup a push rule state with mismatched rules
|
||
|
setPushRules([
|
||
|
// poll 1-1 rules are synced with oneToOneRule
|
||
|
oneToOneRuleDisabled, // off
|
||
|
pollEndOneToOne, // loud
|
||
|
]);
|
||
|
|
||
|
getComponent();
|
||
|
|
||
|
await flushPromises();
|
||
|
|
||
|
mockClient.setPushRuleEnabled.mockClear();
|
||
|
|
||
|
mockClient.emit(ClientEvent.AccountData, pushRulesEvent);
|
||
|
|
||
|
// set to match primary rule
|
||
|
expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith(
|
||
|
"global",
|
||
|
"underride",
|
||
|
pollEndOneToOne.rule_id,
|
||
|
false,
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it("stops listening to account data events on unmount", () => {
|
||
|
// setup a push rule state with mismatched rules
|
||
|
setPushRules([
|
||
|
// poll 1-1 rules are synced with oneToOneRule
|
||
|
oneToOneRule, // on
|
||
|
pollEndOneToOne, // loud
|
||
|
]);
|
||
|
|
||
|
const { unmount } = getComponent();
|
||
|
|
||
|
mockClient.setPushRuleActions.mockClear();
|
||
|
|
||
|
unmount();
|
||
|
|
||
|
mockClient.emit(ClientEvent.AccountData, pushRulesEvent);
|
||
|
|
||
|
// not called
|
||
|
expect(mockClient.setPushRuleActions).not.toHaveBeenCalled();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it("should fire FocusMessageSearch on Ctrl+F when enabled", async () => {
|
||
|
jest.spyOn(defaultDispatcher, "fire");
|
||
|
await SettingsStore.setValue("ctrlFForSearch", null, SettingLevel.DEVICE, true);
|
||
|
|
||
|
getComponent();
|
||
|
await userEvent.keyboard("{Control>}f{/Control}");
|
||
|
expect(defaultDispatcher.fire).toHaveBeenCalledWith(Action.FocusMessageSearch);
|
||
|
});
|
||
|
|
||
|
it("should go home on home shortcut", async () => {
|
||
|
jest.spyOn(defaultDispatcher, "dispatch");
|
||
|
|
||
|
getComponent();
|
||
|
await userEvent.keyboard("{Control>}{Alt>}h</Alt>{/Control}");
|
||
|
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.ViewHomePage });
|
||
|
});
|
||
|
|
||
|
it("should ignore home shortcut if dialogs are open", async () => {
|
||
|
jest.spyOn(defaultDispatcher, "dispatch");
|
||
|
jest.spyOn(Modal, "hasDialogs").mockReturnValue(true);
|
||
|
|
||
|
getComponent();
|
||
|
|
||
|
await userEvent.keyboard("{Control>}{Alt>}h</Alt>{/Control}");
|
||
|
expect(defaultDispatcher.dispatch).not.toHaveBeenCalledWith({ action: Action.ViewHomePage });
|
||
|
});
|
||
|
|
||
|
describe("timezone updates", () => {
|
||
|
const userTimezone = "Europe/London";
|
||
|
const originalController = SETTINGS["userTimezonePublish"].controller;
|
||
|
|
||
|
beforeEach(async () => {
|
||
|
SETTINGS["userTimezonePublish"].controller = undefined;
|
||
|
await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, false);
|
||
|
await SettingsStore.setValue("userTimezone", null, SettingLevel.DEVICE, userTimezone);
|
||
|
});
|
||
|
|
||
|
afterEach(() => {
|
||
|
SETTINGS["userTimezonePublish"].controller = originalController;
|
||
|
});
|
||
|
|
||
|
it("does not update the timezone when userTimezonePublish is off", async () => {
|
||
|
getComponent();
|
||
|
await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, false);
|
||
|
expect(mockClient.deleteExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz");
|
||
|
expect(mockClient.setExtendedProfileProperty).not.toHaveBeenCalled();
|
||
|
});
|
||
|
it("should set the user timezone when userTimezonePublish is enabled", async () => {
|
||
|
getComponent();
|
||
|
await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, true);
|
||
|
expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz", userTimezone);
|
||
|
});
|
||
|
|
||
|
it("should set the user timezone when the timezone is changed", async () => {
|
||
|
const newTimezone = "Europe/Paris";
|
||
|
getComponent();
|
||
|
await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, true);
|
||
|
expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz", userTimezone);
|
||
|
await SettingsStore.setValue("userTimezone", null, SettingLevel.DEVICE, newTimezone);
|
||
|
expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz", newTimezone);
|
||
|
});
|
||
|
|
||
|
it("should clear the timezone when the publish feature is turned off", async () => {
|
||
|
getComponent();
|
||
|
await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, true);
|
||
|
expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz", userTimezone);
|
||
|
await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, false);
|
||
|
expect(mockClient.deleteExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz");
|
||
|
});
|
||
|
});
|
||
|
});
|