Merge matrix-react-sdk into element-web

Merge remote-tracking branch 'repomerge/t3chguy/repomerge' into t3chguy/repo-merge

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-10-15 14:57:26 +01:00
commit f0ee7f7905
No known key found for this signature in database
GPG key ID: A2B008A5F49F5D0D
3265 changed files with 484599 additions and 699 deletions

View file

@ -0,0 +1,142 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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 { ClientEvent, MatrixClient, Room, SyncState } from "matrix-js-sdk/src/matrix";
import BasePlatform from "../../../src/BasePlatform";
import SdkConfig from "../../../src/SdkConfig";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import SettingsStore from "../../../src/settings/SettingsStore";
import { mkStubRoom, mockPlatformPeg, stubClient } from "../../test-utils";
const TEST_DATA = [
{
name: "Electron.showTrayIcon",
level: SettingLevel.PLATFORM,
value: true,
},
];
/**
* An existing setting that has {@link IBaseSetting#supportedLevelsAreOrdered} set to true.
*/
const SETTING_NAME_WITH_CONFIG_OVERRIDE = "feature_msc3531_hide_messages_pending_moderation";
describe("SettingsStore", () => {
let platformSettings: Record<string, any>;
beforeAll(() => {
jest.clearAllMocks();
platformSettings = {};
mockPlatformPeg({
isLevelSupported: jest.fn().mockReturnValue(true),
supportsSetting: jest.fn().mockReturnValue(true),
setSettingValue: jest.fn().mockImplementation((settingName: string, value: any) => {
platformSettings[settingName] = value;
}),
getSettingValue: jest.fn().mockImplementation((settingName: string) => {
return platformSettings[settingName];
}),
reload: jest.fn(),
} as unknown as BasePlatform);
TEST_DATA.forEach((d) => {
SettingsStore.setValue(d.name, null, d.level, d.value);
});
});
beforeEach(() => {
SdkConfig.reset();
});
describe("getValueAt", () => {
TEST_DATA.forEach((d) => {
it(`should return the value "${d.level}"."${d.name}"`, () => {
expect(SettingsStore.getValueAt(d.level, d.name)).toBe(d.value);
// regression test #22545
expect(SettingsStore.getValueAt(d.level, d.name)).toBe(d.value);
});
});
it(`supportedLevelsAreOrdered correctly overrides setting`, async () => {
SdkConfig.put({
features: {
[SETTING_NAME_WITH_CONFIG_OVERRIDE]: false,
},
});
await SettingsStore.setValue(SETTING_NAME_WITH_CONFIG_OVERRIDE, null, SettingLevel.DEVICE, true);
expect(SettingsStore.getValue(SETTING_NAME_WITH_CONFIG_OVERRIDE)).toBe(false);
});
it(`supportedLevelsAreOrdered doesn't incorrectly override setting`, async () => {
await SettingsStore.setValue(SETTING_NAME_WITH_CONFIG_OVERRIDE, null, SettingLevel.DEVICE, true);
expect(SettingsStore.getValueAt(SettingLevel.DEVICE, SETTING_NAME_WITH_CONFIG_OVERRIDE)).toBe(true);
});
});
describe("runMigrations", () => {
let client: MatrixClient;
let room: Room;
let localStorageSetItemSpy: jest.SpyInstance;
let localStorageSetPromise: Promise<void>;
beforeEach(() => {
client = stubClient();
room = mkStubRoom("!room:example.org", "Room", client);
room.getAccountData = jest.fn().mockReturnValue({
getContent: jest.fn().mockReturnValue({
urlPreviewsEnabled_e2ee: true,
}),
});
client.getRooms = jest.fn().mockReturnValue([room]);
client.getRoom = jest.fn().mockReturnValue(room);
localStorageSetPromise = new Promise((resolve) => {
localStorageSetItemSpy = jest
.spyOn(localStorage.__proto__, "setItem")
.mockImplementation(() => resolve());
});
});
afterEach(() => {
jest.restoreAllMocks();
});
it("migrates URL previews setting for e2ee rooms", async () => {
SettingsStore.runMigrations(false);
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
expect(room.getAccountData).toHaveBeenCalled();
await localStorageSetPromise;
expect(localStorageSetItemSpy!).toHaveBeenCalledWith(
`mx_setting_urlPreviewsEnabled_e2ee_${room.roomId}`,
JSON.stringify({ value: true }),
);
});
it("does not migrate e2ee URL previews on a fresh login", async () => {
SettingsStore.runMigrations(true);
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
expect(room.getAccountData).not.toHaveBeenCalled();
});
it("does not migrate if the device is flagged as migrated", async () => {
jest.spyOn(localStorage.__proto__, "getItem").mockImplementation((key: unknown): string | undefined => {
if (key === "url_previews_e2ee_migration_done") return JSON.stringify({ value: true });
return undefined;
});
SettingsStore.runMigrations(false);
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
expect(room.getAccountData).not.toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,27 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024 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 PosthogTrackers from "../../../../src/PosthogTrackers";
import AnalyticsController from "../../../../src/settings/controllers/AnalyticsController";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
describe("AnalyticsController", () => {
afterEach(() => {
jest.restoreAllMocks();
});
it("Tracks a Posthog interaction on change", () => {
const trackInteractionSpy = jest.spyOn(PosthogTrackers, "trackInteraction");
const controller = new AnalyticsController("WebSettingsNotificationsTACOnlyNotificationsToggle");
controller.onChange(SettingLevel.DEVICE, null, false);
expect(trackInteractionSpy).toHaveBeenCalledWith("WebSettingsNotificationsTACOnlyNotificationsToggle");
});
});

View file

@ -0,0 +1,33 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { AllDevicesIsolationMode, OnlySignedDevicesIsolationMode } from "matrix-js-sdk/src/crypto-api";
import { stubClient } from "../../../test-utils";
import DeviceIsolationModeController from "../../../../src/settings/controllers/DeviceIsolationModeController.ts";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
describe("DeviceIsolationModeController", () => {
afterEach(() => {
jest.resetAllMocks();
});
describe("tracks enabling and disabling", () => {
it("on sets signed device isolation mode", () => {
const cli = stubClient();
const controller = new DeviceIsolationModeController();
controller.onChange(SettingLevel.DEVICE, "", true);
expect(cli.getCrypto()?.setDeviceIsolationMode).toHaveBeenCalledWith(new OnlySignedDevicesIsolationMode());
});
it("off sets all device isolation mode", () => {
const cli = stubClient();
const controller = new DeviceIsolationModeController();
controller.onChange(SettingLevel.DEVICE, "", false);
expect(cli.getCrypto()?.setDeviceIsolationMode).toHaveBeenCalledWith(new AllDevicesIsolationMode(false));
});
});
});

View file

@ -0,0 +1,57 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import fetchMockJest from "fetch-mock-jest";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import FallbackIceServerController from "../../../../src/settings/controllers/FallbackIceServerController.ts";
import MatrixClientBackedController from "../../../../src/settings/controllers/MatrixClientBackedController.ts";
import SettingsStore from "../../../../src/settings/SettingsStore.ts";
describe("FallbackIceServerController", () => {
beforeEach(() => {
fetchMockJest.get("https://matrix.org/_matrix/client/versions", { versions: ["v1.4"] });
});
afterEach(() => {
jest.restoreAllMocks();
});
it("should update MatrixClient's state when the setting is updated", async () => {
const client = new MatrixClient({
baseUrl: "https://matrix.org",
userId: "@alice:matrix.org",
accessToken: "token",
});
MatrixClientBackedController.matrixClient = client;
expect(client.isFallbackICEServerAllowed()).toBeFalsy();
await SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, true);
expect(client.isFallbackICEServerAllowed()).toBeTruthy();
});
it("should force the setting to be disabled if disable_fallback_ice=true", async () => {
const controller = new FallbackIceServerController();
const client = new MatrixClient({
baseUrl: "https://matrix.org",
userId: "@alice:matrix.org",
accessToken: "token",
});
MatrixClientBackedController.matrixClient = client;
expect(controller.settingDisabled).toBeFalsy();
client["clientWellKnown"] = {
"io.element.voip": {
disable_fallback_ice: true,
},
};
client.emit(ClientEvent.ClientWellKnown, client["clientWellKnown"]);
expect(controller.settingDisabled).toBeTruthy();
});
});

View file

@ -0,0 +1,24 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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 { Action } from "../../../../src/dispatcher/actions";
import dis from "../../../../src/dispatcher/dispatcher";
import FontSizeController from "../../../../src/settings/controllers/FontSizeController";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
const dispatchSpy = jest.spyOn(dis, "fire");
describe("FontSizeController", () => {
it("dispatches a font size action on change", () => {
const controller = new FontSizeController();
controller.onChange(SettingLevel.ACCOUNT, "$room:server", 12);
expect(dispatchSpy).toHaveBeenCalledWith(Action.MigrateBaseFontSize);
});
});

View file

@ -0,0 +1,81 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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 IncompatibleController from "../../../../src/settings/controllers/IncompatibleController";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import SettingsStore from "../../../../src/settings/SettingsStore";
describe("IncompatibleController", () => {
const settingsGetValueSpy = jest.spyOn(SettingsStore, "getValue");
beforeEach(() => {
settingsGetValueSpy.mockClear();
});
describe("incompatibleSetting", () => {
describe("when incompatibleValue is not set", () => {
it("returns true when setting value is true", () => {
// no incompatible value set, defaulted to true
const controller = new IncompatibleController("feature_spotlight", { key: null });
settingsGetValueSpy.mockReturnValue(true);
// true === true
expect(controller.incompatibleSetting).toBe(true);
expect(controller.settingDisabled).toEqual(true);
expect(settingsGetValueSpy).toHaveBeenCalledWith("feature_spotlight");
});
it("returns false when setting value is not true", () => {
// no incompatible value set, defaulted to true
const controller = new IncompatibleController("feature_spotlight", { key: null });
settingsGetValueSpy.mockReturnValue("test");
expect(controller.incompatibleSetting).toBe(false);
});
});
describe("when incompatibleValue is set to a value", () => {
it("returns true when setting value matches incompatible value", () => {
const controller = new IncompatibleController("feature_spotlight", { key: null }, "test");
settingsGetValueSpy.mockReturnValue("test");
expect(controller.incompatibleSetting).toBe(true);
});
it("returns false when setting value is not true", () => {
const controller = new IncompatibleController("feature_spotlight", { key: null }, "test");
settingsGetValueSpy.mockReturnValue("not test");
expect(controller.incompatibleSetting).toBe(false);
});
});
describe("when incompatibleValue is set to a function", () => {
it("returns result from incompatibleValue function", () => {
const incompatibleValueFn = jest.fn().mockReturnValue(false);
const controller = new IncompatibleController("feature_spotlight", { key: null }, incompatibleValueFn);
settingsGetValueSpy.mockReturnValue("test");
expect(controller.incompatibleSetting).toBe(false);
expect(incompatibleValueFn).toHaveBeenCalledWith("test");
});
});
});
describe("getValueOverride()", () => {
it("returns forced value when setting is incompatible", () => {
settingsGetValueSpy.mockReturnValue(true);
const controller = new IncompatibleController("feature_spotlight", { key: null });
expect(
controller.getValueOverride(SettingLevel.ACCOUNT, "$room:server", true, SettingLevel.ACCOUNT),
).toEqual({ key: null });
});
it("returns null when setting is not incompatible", () => {
settingsGetValueSpy.mockReturnValue(false);
const controller = new IncompatibleController("feature_spotlight", { key: null });
expect(
controller.getValueOverride(SettingLevel.ACCOUNT, "$room:server", true, SettingLevel.ACCOUNT),
).toEqual(null);
});
});
});

View file

@ -0,0 +1,141 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 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 { defer } from "matrix-js-sdk/src/utils";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import ServerSupportUnstableFeatureController from "../../../../src/settings/controllers/ServerSupportUnstableFeatureController";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import { LabGroup, SETTINGS } from "../../../../src/settings/Settings";
import { stubClient } from "../../../test-utils";
import { WatchManager } from "../../../../src/settings/WatchManager";
import MatrixClientBackedController from "../../../../src/settings/controllers/MatrixClientBackedController";
import { TranslationKey } from "../../../../src/languageHandler";
describe("ServerSupportUnstableFeatureController", () => {
const watchers = new WatchManager();
const setting = "setting_name";
async function prepareSetting(
cli: MatrixClient,
controller: ServerSupportUnstableFeatureController,
): Promise<void> {
SETTINGS[setting] = {
isFeature: true,
labsGroup: LabGroup.Messaging,
displayName: "name of some kind" as TranslationKey,
supportedLevels: [SettingLevel.DEVICE, SettingLevel.CONFIG],
default: false,
controller,
};
const deferred = defer<any>();
watchers.watchSetting(setting, null, deferred.resolve);
MatrixClientBackedController.matrixClient = cli;
await deferred.promise;
}
describe("getValueOverride()", () => {
it("should return forced value is setting is disabled", async () => {
const cli = stubClient();
cli.doesServerSupportUnstableFeature = jest.fn(async () => false);
const controller = new ServerSupportUnstableFeatureController(
setting,
watchers,
[["feature"]],
undefined,
undefined,
"other_value",
);
await prepareSetting(cli, controller);
expect(controller.getValueOverride(SettingLevel.DEVICE, null, true, SettingLevel.ACCOUNT)).toEqual(
"other_value",
);
});
it("should pass through to the handler if setting is not disabled", async () => {
const cli = stubClient();
cli.doesServerSupportUnstableFeature = jest.fn(async () => true);
const controller = new ServerSupportUnstableFeatureController(
setting,
watchers,
[["feature"]],
"other_value",
);
await prepareSetting(cli, controller);
expect(controller.getValueOverride(SettingLevel.DEVICE, null, true, SettingLevel.ACCOUNT)).toEqual(null);
});
});
describe("settingDisabled()", () => {
it("considered disabled if there is no matrix client", () => {
const controller = new ServerSupportUnstableFeatureController(setting, watchers, [["org.matrix.msc3030"]]);
expect(controller.settingDisabled).toEqual(true);
});
it("considered disabled if not all required features in the only feature group are supported", async () => {
const cli = stubClient();
cli.doesServerSupportUnstableFeature = jest.fn(async (featureName) => {
return featureName === "org.matrix.msc3827.stable";
});
const controller = new ServerSupportUnstableFeatureController(setting, watchers, [
["org.matrix.msc3827.stable", "org.matrix.msc3030"],
]);
await prepareSetting(cli, controller);
expect(controller.settingDisabled).toEqual(true);
});
it("considered enabled if all required features in the only feature group are supported", async () => {
const cli = stubClient();
cli.doesServerSupportUnstableFeature = jest.fn(async (featureName) => {
return featureName === "org.matrix.msc3827.stable" || featureName === "org.matrix.msc3030";
});
const controller = new ServerSupportUnstableFeatureController(setting, watchers, [
["org.matrix.msc3827.stable", "org.matrix.msc3030"],
]);
await prepareSetting(cli, controller);
expect(controller.settingDisabled).toEqual(false);
});
it("considered enabled if all required features in one of the feature groups are supported", async () => {
const cli = stubClient();
cli.doesServerSupportUnstableFeature = jest.fn(async (featureName) => {
return featureName === "org.matrix.msc3827.stable" || featureName === "org.matrix.msc3030";
});
const controller = new ServerSupportUnstableFeatureController(setting, watchers, [
["foo-unsupported", "bar-unsupported"],
["org.matrix.msc3827.stable", "org.matrix.msc3030"],
]);
await prepareSetting(cli, controller);
expect(controller.settingDisabled).toEqual(false);
});
it("considered disabled if not all required features in one of the feature groups are supported", async () => {
const cli = stubClient();
cli.doesServerSupportUnstableFeature = jest.fn(async (featureName) => {
return featureName === "org.matrix.msc3827.stable";
});
const controller = new ServerSupportUnstableFeatureController(setting, watchers, [
["foo-unsupported", "bar-unsupported"],
["org.matrix.msc3827.stable", "org.matrix.msc3030"],
]);
await prepareSetting(cli, controller);
expect(controller.settingDisabled).toEqual(true);
});
});
});

View file

@ -0,0 +1,36 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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 { Action } from "../../../../src/dispatcher/actions";
import dis from "../../../../src/dispatcher/dispatcher";
import SystemFontController from "../../../../src/settings/controllers/SystemFontController";
import SettingsStore from "../../../../src/settings/SettingsStore";
const dispatchSpy = jest.spyOn(dis, "dispatch");
describe("SystemFontController", () => {
it("dispatches a system font update action on change", () => {
const controller = new SystemFontController();
const getValueSpy = jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => {
if (settingName === "useBundledEmojiFont") return false;
if (settingName === "useSystemFont") return true;
if (settingName === "systemFont") return "Comic Sans MS";
});
controller.onChange();
expect(dispatchSpy).toHaveBeenCalledWith({
action: Action.UpdateSystemFont,
useBundledEmojiFont: false,
useSystemFont: true,
font: "Comic Sans MS",
});
expect(getValueSpy).toHaveBeenCalledWith("useSystemFont");
});
});

View file

@ -0,0 +1,60 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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 ThemeController from "../../../../src/settings/controllers/ThemeController";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { DEFAULT_THEME } from "../../../../src/theme";
describe("ThemeController", () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue([]);
afterEach(() => {
// reset
ThemeController.isLogin = false;
});
it("returns null when calculatedValue is falsy", () => {
const controller = new ThemeController();
expect(
controller.getValueOverride(
SettingLevel.ACCOUNT,
"$room:server",
undefined /* calculatedValue */,
SettingLevel.ACCOUNT,
),
).toEqual(null);
});
it("returns light when login flag is set", () => {
const controller = new ThemeController();
ThemeController.isLogin = true;
expect(controller.getValueOverride(SettingLevel.ACCOUNT, "$room:server", "dark", SettingLevel.ACCOUNT)).toEqual(
"light",
);
});
it("returns default theme when value is not a valid theme", () => {
const controller = new ThemeController();
expect(
controller.getValueOverride(SettingLevel.ACCOUNT, "$room:server", "my-test-theme", SettingLevel.ACCOUNT),
).toEqual(DEFAULT_THEME);
});
it("returns null when value is a valid theme", () => {
const controller = new ThemeController();
expect(controller.getValueOverride(SettingLevel.ACCOUNT, "$room:server", "dark", SettingLevel.ACCOUNT)).toEqual(
null,
);
});
});

View file

@ -0,0 +1,38 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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 { ImageSize, suggestedSize } from "../../../../src/settings/enums/ImageSize";
describe("ImageSize", () => {
describe("suggestedSize", () => {
it("constrains width", () => {
const size = suggestedSize(ImageSize.Normal, { w: 648, h: 162 });
expect(size).toStrictEqual({ w: 324, h: 81 });
});
it("constrains height", () => {
const size = suggestedSize(ImageSize.Normal, { w: 162, h: 648 });
expect(size).toStrictEqual({ w: 81, h: 324 });
});
it("constrains width in large mode", () => {
const size = suggestedSize(ImageSize.Large, { w: 2400, h: 1200 });
expect(size).toStrictEqual({ w: 800, h: 400 });
});
it("returns max values if content size is not specified", () => {
const size = suggestedSize(ImageSize.Normal, {});
expect(size).toStrictEqual({ w: 324, h: 324 });
});
it("returns integer values", () => {
const size = suggestedSize(ImageSize.Normal, { w: 642, h: 350 }); // does not divide evenly
expect(size).toStrictEqual({ w: 324, h: 176 });
});
it("returns integer values for portrait images", () => {
const size = suggestedSize(ImageSize.Normal, { w: 720, h: 1280 });
expect(size).toStrictEqual({ w: 182, h: 324 });
});
});
});

View file

@ -0,0 +1,74 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 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 { mocked } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import DeviceSettingsHandler from "../../../../src/settings/handlers/DeviceSettingsHandler";
import { CallbackFn, WatchManager } from "../../../../src/settings/WatchManager";
import { stubClient } from "../../../test-utils/test-utils";
describe("DeviceSettingsHandler", () => {
const ROOM_ID_IS_UNUSED = "";
const unknownSettingKey = "unknown_setting";
const featureKey = "my_feature";
let watchers: WatchManager;
let handler: DeviceSettingsHandler;
let settingListener: CallbackFn;
beforeEach(() => {
watchers = new WatchManager();
handler = new DeviceSettingsHandler([featureKey], watchers);
settingListener = jest.fn();
});
afterEach(() => {
watchers.unwatchSetting(settingListener);
});
it("Returns undefined for an unknown setting", () => {
expect(handler.getValue(unknownSettingKey, ROOM_ID_IS_UNUSED)).toBeUndefined();
});
it("Returns the value for a disabled feature", () => {
handler.setValue(featureKey, ROOM_ID_IS_UNUSED, false);
expect(handler.getValue(featureKey, ROOM_ID_IS_UNUSED)).toBe(false);
});
it("Returns the value for an enabled feature", () => {
handler.setValue(featureKey, ROOM_ID_IS_UNUSED, true);
expect(handler.getValue(featureKey, ROOM_ID_IS_UNUSED)).toBe(true);
});
describe("If I am a guest", () => {
let client: MatrixClient;
beforeEach(() => {
client = stubClient();
mocked(client.isGuest).mockReturnValue(true);
});
afterEach(() => {
MatrixClientPeg.get = () => null;
MatrixClientPeg.safeGet = () => new MatrixClient({ baseUrl: "foobar" });
});
it("Returns the value for a disabled feature", () => {
handler.setValue(featureKey, ROOM_ID_IS_UNUSED, false);
expect(handler.getValue(featureKey, ROOM_ID_IS_UNUSED)).toBe(false);
});
it("Returns the value for an enabled feature", () => {
handler.setValue(featureKey, ROOM_ID_IS_UNUSED, true);
expect(handler.getValue(featureKey, ROOM_ID_IS_UNUSED)).toBe(true);
});
});
});

View file

@ -0,0 +1,55 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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 RoomDeviceSettingsHandler from "../../../../src/settings/handlers/RoomDeviceSettingsHandler";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import { CallbackFn, WatchManager } from "../../../../src/settings/WatchManager";
describe("RoomDeviceSettingsHandler", () => {
const roomId = "!room:example.com";
const value = "test value";
const testSettings = [
"RightPanel.phases",
// special case in RoomDeviceSettingsHandler
"blacklistUnverifiedDevices",
];
let watchers: WatchManager;
let handler: RoomDeviceSettingsHandler;
let settingListener: CallbackFn;
beforeEach(() => {
watchers = new WatchManager();
handler = new RoomDeviceSettingsHandler(watchers);
settingListener = jest.fn();
});
afterEach(() => {
watchers.unwatchSetting(settingListener);
});
it.each(testSettings)("should write/read/clear the value for »%s«", (setting: string): void => {
// initial value should be null
watchers.watchSetting(setting, roomId, settingListener);
expect(handler.getValue(setting, roomId)).toBeNull();
// set and read value
handler.setValue(setting, roomId, value);
expect(settingListener).toHaveBeenCalledWith(roomId, SettingLevel.ROOM_DEVICE, value);
expect(handler.getValue(setting, roomId)).toEqual(value);
// clear value
handler.setValue(setting, roomId, null);
expect(settingListener).toHaveBeenCalledWith(roomId, SettingLevel.ROOM_DEVICE, null);
expect(handler.getValue(setting, roomId)).toBeNull();
});
it("canSetValue should return true", () => {
expect(handler.canSetValue("test setting", roomId)).toBe(true);
});
});

View file

@ -0,0 +1,159 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 r00ster91 <r00ster91@proton.me>
Copyright 2022 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 { sleep } from "matrix-js-sdk/src/utils";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import { FontWatcher } from "../../../../src/settings/watchers/FontWatcher";
import { Action } from "../../../../src/dispatcher/actions";
import { untilDispatch } from "../../../test-utils";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
async function setSystemFont(font: string | false): Promise<void> {
await SettingsStore.setValue("systemFont", null, SettingLevel.DEVICE, font || "");
await SettingsStore.setValue("useSystemFont", null, SettingLevel.DEVICE, !!font);
await untilDispatch(Action.UpdateSystemFont);
await sleep(1); // await the FontWatcher doing its action
}
async function setUseBundledEmojiFont(use: boolean): Promise<void> {
await SettingsStore.setValue("useBundledEmojiFont", null, SettingLevel.DEVICE, use);
await untilDispatch(Action.UpdateSystemFont);
await sleep(1); // await the FontWatcher doing its action
}
const getFontFamily = () => {
return document.body.style.getPropertyValue(FontWatcher.FONT_FAMILY_CUSTOM_PROPERTY);
};
const getEmojiFontFamily = () => {
return document.body.style.getPropertyValue(FontWatcher.EMOJI_FONT_FAMILY_CUSTOM_PROPERTY);
};
describe("FontWatcher", function () {
it("should load font on start()", async () => {
const watcher = new FontWatcher();
await setSystemFont("Font Name");
expect(getFontFamily()).toMatchInlineSnapshot(`""`);
await watcher.start();
expect(getFontFamily()).toMatchInlineSnapshot(`""Font Name", Twemoji"`);
});
it("should load font on Action.OnLoggedIn", async () => {
await setSystemFont("Font Name");
await new FontWatcher().start();
document.body.style.removeProperty(FontWatcher.FONT_FAMILY_CUSTOM_PROPERTY); // clear the fontFamily which was by start which we tested already
defaultDispatcher.fire(Action.OnLoggedIn, true);
expect(getFontFamily()).toMatchInlineSnapshot(`""Font Name", Twemoji"`);
});
it("should reset font on Action.OnLoggedOut", async () => {
await setSystemFont("Font Name");
const watcher = new FontWatcher();
await watcher.start();
expect(getFontFamily()).toMatchInlineSnapshot(`""Font Name", Twemoji"`);
defaultDispatcher.fire(Action.OnLoggedOut, true);
expect(getFontFamily()).toMatchInlineSnapshot(`""`);
});
describe("Sets font as expected", () => {
let fontWatcher: FontWatcher;
beforeEach(async () => {
fontWatcher = new FontWatcher();
await fontWatcher.start();
});
afterEach(() => {
fontWatcher.stop();
});
it("encloses the fonts by double quotes and sets them as the system font", async () => {
await setSystemFont("Fira Sans Thin, Commodore 64");
expect(getFontFamily()).toMatchInlineSnapshot(`""Fira Sans Thin","Commodore 64", Twemoji"`);
});
it("does not add double quotes if already present and sets the font as the system font", async () => {
await setSystemFont(`"Commodore 64"`);
expect(getFontFamily()).toMatchInlineSnapshot(`""Commodore 64", Twemoji"`);
});
it("trims whitespace, encloses the fonts by double quotes, and sets them as the system font", async () => {
await setSystemFont(` Fira Code , "Commodore 64" `);
expect(getFontFamily()).toMatchInlineSnapshot(`""Fira Code","Commodore 64", Twemoji"`);
});
});
describe("Sets bundled emoji font as expected", () => {
let fontWatcher: FontWatcher;
beforeEach(async () => {
await setSystemFont(false);
fontWatcher = new FontWatcher();
await fontWatcher.start();
});
afterEach(() => {
fontWatcher.stop();
});
it("by default adds Twemoji font", async () => {
expect(getEmojiFontFamily()).toMatchInlineSnapshot(`"Twemoji"`);
});
it("does not add Twemoji font when disabled", async () => {
await setUseBundledEmojiFont(false);
expect(getEmojiFontFamily()).toMatchInlineSnapshot(`""`);
});
it("works in conjunction with useSystemFont", async () => {
await setSystemFont(`"Commodore 64"`);
await setUseBundledEmojiFont(true);
expect(getFontFamily()).toMatchInlineSnapshot(`""Commodore 64", Twemoji"`);
});
});
describe("Migrates baseFontSize", () => {
let watcher: FontWatcher | undefined;
beforeEach(() => {
document.documentElement.style.fontSize = "14px";
watcher = new FontWatcher();
});
afterEach(() => {
watcher!.stop();
});
it("should not run the migration", async () => {
await watcher!.start();
expect(SettingsStore.getValue("fontSizeDelta")).toBe(0);
});
it("should migrate from V1 font size to V3", async () => {
await SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, 13);
await watcher!.start();
// 13px (V1 font size) + 5px (V1 offset) + 1px (root font size increase) - 14px (default browser font size) = 5px
expect(SettingsStore.getValue("fontSizeDelta")).toBe(5);
// baseFontSize should be cleared
expect(SettingsStore.getValue("baseFontSize")).toBe(0);
});
it("should migrate from V2 font size to V3 using browser font size", async () => {
await SettingsStore.setValue("baseFontSizeV2", null, SettingLevel.DEVICE, 18);
await watcher!.start();
// 18px - 14px (default browser font size) = 2px
expect(SettingsStore.getValue("fontSizeDelta")).toBe(4);
// baseFontSize should be cleared
expect(SettingsStore.getValue("baseFontSizeV2")).toBe(0);
});
it("should migrate from V2 font size to V3 using fallback font size", async () => {
document.documentElement.style.fontSize = "";
await SettingsStore.setValue("baseFontSizeV2", null, SettingLevel.DEVICE, 18);
await watcher!.start();
// 18px - 16px (fallback) = 2px
expect(SettingsStore.getValue("fontSizeDelta")).toBe(2);
// baseFontSize should be cleared
expect(SettingsStore.getValue("baseFontSizeV2")).toBe(0);
});
});
});

View file

@ -0,0 +1,187 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 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 SettingsStore from "../../../../src/settings/SettingsStore";
import ThemeWatcher from "../../../../src/settings/watchers/ThemeWatcher";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
function makeMatchMedia(values: any) {
class FakeMediaQueryList {
matches: false;
media?: null;
onchange?: null;
addListener() {}
removeListener() {}
addEventListener() {}
removeEventListener() {}
dispatchEvent() {
return true;
}
constructor(query: string) {
this.matches = values[query];
}
}
return function matchMedia(query: string) {
return new FakeMediaQueryList(query) as unknown as MediaQueryList;
};
}
function makeGetValue(values: any) {
return function getValue<T = any>(settingName: string, _roomId: string | null = null, _excludeDefault = false): T {
return values[settingName];
};
}
function makeGetValueAt(values: any) {
return function getValueAt(
_level: SettingLevel,
settingName: string,
_roomId: string | null = null,
_explicit = false,
_excludeDefault = false,
): any {
return values[settingName];
};
}
describe("ThemeWatcher", function () {
it("should choose a light theme by default", () => {
// Given no system settings
global.matchMedia = makeMatchMedia({});
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it("should choose default theme if system settings are inconclusive", () => {
// Given no system settings but we asked to use them
global.matchMedia = makeMatchMedia({});
SettingsStore.getValue = makeGetValue({
use_system_theme: true,
theme: "light",
});
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it("should choose a dark theme if that is selected", () => {
// Given system says light high contrast but theme is set to dark
global.matchMedia = makeMatchMedia({
"(prefers-contrast: more)": true,
"(prefers-color-scheme: light)": true,
});
SettingsStore.getValueAt = makeGetValueAt({ theme: "dark" });
// Then getEffectiveTheme returns dark
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("dark");
});
it("should choose a light theme if that is selected", () => {
// Given system settings say dark high contrast but theme set to light
global.matchMedia = makeMatchMedia({
"(prefers-contrast: more)": true,
"(prefers-color-scheme: dark)": true,
});
SettingsStore.getValueAt = makeGetValueAt({ theme: "light" });
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it("should choose a light-high-contrast theme if that is selected", () => {
// Given system settings say dark and theme set to light-high-contrast
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: dark)": true });
SettingsStore.getValueAt = makeGetValueAt({ theme: "light-high-contrast" });
// Then getEffectiveTheme returns light-high-contrast
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light-high-contrast");
});
it("should choose a light theme if system prefers it (via default)", () => {
// Given system prefers lightness, even though we did not
// click "Use system theme" or choose a theme explicitly
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: light)": true });
SettingsStore.getValueAt = makeGetValueAt({});
SettingsStore.getValue = makeGetValue({ use_system_theme: true });
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it("should choose a dark theme if system prefers it (via default)", () => {
// Given system prefers darkness, even though we did not
// click "Use system theme" or choose a theme explicitly
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: dark)": true });
SettingsStore.getValueAt = makeGetValueAt({});
SettingsStore.getValue = makeGetValue({ use_system_theme: true });
// Then getEffectiveTheme returns dark
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("dark");
});
it("should choose a light theme if system prefers it (explicit)", () => {
// Given system prefers lightness
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: light)": true });
SettingsStore.getValueAt = makeGetValueAt({ use_system_theme: true });
SettingsStore.getValue = makeGetValue({ use_system_theme: true });
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it("should choose a dark theme if system prefers it (explicit)", () => {
// Given system prefers darkness
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: dark)": true });
SettingsStore.getValueAt = makeGetValueAt({ use_system_theme: true });
SettingsStore.getValue = makeGetValue({ use_system_theme: true });
// Then getEffectiveTheme returns dark
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("dark");
});
it("should choose a high-contrast theme if system prefers it", () => {
// Given system prefers high contrast and light
global.matchMedia = makeMatchMedia({
"(prefers-contrast: more)": true,
"(prefers-color-scheme: light)": true,
});
SettingsStore.getValueAt = makeGetValueAt({ use_system_theme: true });
SettingsStore.getValue = makeGetValue({ use_system_theme: true });
// Then getEffectiveTheme returns light-high-contrast
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light-high-contrast");
});
it("should not choose a high-contrast theme if not available", () => {
// Given system prefers high contrast and dark, but we don't (yet)
// have a high-contrast dark theme
global.matchMedia = makeMatchMedia({
"(prefers-contrast: more)": true,
"(prefers-color-scheme: dark)": true,
});
SettingsStore.getValueAt = makeGetValueAt({ use_system_theme: true });
SettingsStore.getValue = makeGetValue({ use_system_theme: true });
// Then getEffectiveTheme returns dark
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("dark");
});
});