Migrate batch of tests to vitest (#34121)
* Migrate batch of tests tro vitest * Iterate * Migrate another test * Migrate batch of tests to vitest * Iterate
This commit is contained in:
@@ -17,7 +17,10 @@ const adapter = {
|
||||
fn: isJest ? (jest.fn as unknown as typeof vi.fn) : vi.fn,
|
||||
spyOn: isJest ? (jest.spyOn as unknown as typeof vi.spyOn) : vi.spyOn,
|
||||
mocked: isJest ? (jestMocked as typeof vi.mocked) : vi.mocked,
|
||||
} as Pick<typeof vi, "fn" | "spyOn" | "mocked">;
|
||||
advanceTimersByTime: isJest
|
||||
? (jest.advanceTimersByTime as unknown as typeof vi.advanceTimersByTime)
|
||||
: vi.advanceTimersByTime,
|
||||
} as Pick<typeof vi, "fn" | "spyOn" | "mocked" | "advanceTimersByTime">;
|
||||
|
||||
const mocked = adapter.mocked;
|
||||
export { adapter as vi, mocked };
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { stubClient } from "../test-utils";
|
||||
import { statusCommand } from "../../src/slash-commands/status";
|
||||
import { UserFriendlyError } from "../../src/languageHandler";
|
||||
|
||||
describe("/status", () => {
|
||||
const roomId = "!room:example.com";
|
||||
|
||||
let client: ReturnType<typeof stubClient>;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
});
|
||||
|
||||
function run(args?: string) {
|
||||
return statusCommand.run(client, roomId, null, args);
|
||||
}
|
||||
|
||||
it("should reject if no args provided", () => {
|
||||
const result = run(undefined);
|
||||
expect(result.error).toBeInstanceOf(UserFriendlyError);
|
||||
expect((result.error as UserFriendlyError).message).toBe(
|
||||
"No arguments provided. You should supply an emoij and an optional text component.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should reject if no text is provided after the emoji", () => {
|
||||
const result = run("🎉");
|
||||
expect(result.error).toBeInstanceOf(UserFriendlyError);
|
||||
expect((result.error as UserFriendlyError).message).toBe("You did not provide any status text");
|
||||
});
|
||||
|
||||
it("should reject if the emoji field has more than one grapheme segment", () => {
|
||||
const result = run("ab hello");
|
||||
expect(result.error).toBeInstanceOf(UserFriendlyError);
|
||||
expect((result.error as UserFriendlyError).message).toBe("The first argument must be an emoji");
|
||||
});
|
||||
|
||||
it("should reject if the status text exceeds the maximum byte length", () => {
|
||||
const longText = "a".repeat(257);
|
||||
const result = run(`🎉 ${longText}`);
|
||||
expect(result.error).toBeInstanceOf(UserFriendlyError);
|
||||
expect((result.error as UserFriendlyError).message).toBe("The text you provided was too long.");
|
||||
});
|
||||
|
||||
it("should set the extended profile property on success", async () => {
|
||||
const result = run("🎉 Having a great day");
|
||||
expect(result.error).toBeUndefined();
|
||||
await result.promise;
|
||||
expect(client.setExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.status", {
|
||||
emoji: "🎉",
|
||||
text: "Having a great day",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import { type ActionPayload } from "../../src/dispatcher/payloads";
|
||||
import defaultDispatcher from "../../src/dispatcher/dispatcher";
|
||||
import { type DispatcherAction } from "../../src/dispatcher/actions";
|
||||
import Modal from "../../src/Modal";
|
||||
import { vi } from "../setup/adapter.ts";
|
||||
|
||||
export const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise((r) => e.once(k, r));
|
||||
|
||||
@@ -128,7 +129,7 @@ export const flushPromises = () => act(async () => await new Promise<void>((reso
|
||||
// https://gist.github.com/apieceofbart/e6dea8d884d29cf88cdb54ef14ddbcc4?permalink_comment_id=4018174#gistcomment-4018174
|
||||
export const flushPromisesWithFakeTimers = async (): Promise<void> => {
|
||||
const promise = new Promise((resolve) => process.nextTick(resolve));
|
||||
jest.advanceTimersByTime(1);
|
||||
vi.advanceTimersByTime(1);
|
||||
await promise;
|
||||
};
|
||||
|
||||
@@ -165,8 +166,8 @@ export function waitForUpdate(inst: React.Component, updates = 1): Promise<void>
|
||||
* that also checks timestamps
|
||||
*/
|
||||
export const advanceDateAndTime = (ms: number) => {
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(Date.now() + ms);
|
||||
jest.advanceTimersByTime(ms);
|
||||
vi.spyOn(global.Date, "now").mockReturnValue(Date.now() + ms);
|
||||
vi.advanceTimersByTime(ms);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -199,8 +200,8 @@ export const clearAllModals = async (): Promise<void> => {
|
||||
export function useMockMediaDevices(): void {
|
||||
// @ts-ignore assignment of a thing that isn't a `MediaDevices` to read-only property
|
||||
navigator["mediaDevices"] = {
|
||||
enumerateDevices: jest.fn().mockResolvedValue([]),
|
||||
getUserMedia: jest.fn(),
|
||||
enumerateDevices: vi.fn().mockResolvedValue([]),
|
||||
getUserMedia: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -234,7 +235,7 @@ export function resetJsDomAfterEach(): void {
|
||||
|
||||
// intercept setTimeout and setInterval, and clear them at the end.
|
||||
//
|
||||
// *Don't* use jest.spyOn for this because it makes the DOM testing library think we are using fake timers.
|
||||
// *Don't* use vi.spyOn for this because it makes the DOM testing library think we are using fake timers.
|
||||
//
|
||||
["setTimeout", "setInterval"].forEach((name) => {
|
||||
const originalFn = window[name as keyof Window];
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import fetchMock from "@fetch-mock/jest";
|
||||
|
||||
import { advanceDateAndTime, createTestClient, stubClient } from "../test-utils";
|
||||
import { type IMatrixClientPeg, MatrixClientPeg as peg } from "../../src/MatrixClientPeg";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
const PegClass = Object.getPrototypeOf(peg).constructor;
|
||||
|
||||
describe("MatrixClientPeg", () => {
|
||||
beforeEach(() => {
|
||||
// stub out Logger.log which gets called a lot and clutters up the test output
|
||||
jest.spyOn(logger, "log").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
jest.restoreAllMocks();
|
||||
|
||||
// some of the tests assign `MatrixClientPeg.matrixClient`: clear it, to prevent leakage between tests
|
||||
peg.unset();
|
||||
});
|
||||
|
||||
it("setJustRegisteredUserId", () => {
|
||||
stubClient();
|
||||
(peg as any).matrixClient = peg.get();
|
||||
peg.setJustRegisteredUserId("@userId:matrix.org");
|
||||
expect(peg.safeGet().credentials.userId).toBe("@userId:matrix.org");
|
||||
expect(peg.currentUserIsJustRegistered()).toBe(true);
|
||||
expect(peg.userRegisteredWithinLastHours(0)).toBe(false);
|
||||
expect(peg.userRegisteredWithinLastHours(1)).toBe(true);
|
||||
expect(peg.userRegisteredWithinLastHours(24)).toBe(true);
|
||||
advanceDateAndTime(1 * 60 * 60 * 1000 + 1);
|
||||
expect(peg.userRegisteredWithinLastHours(0)).toBe(false);
|
||||
expect(peg.userRegisteredWithinLastHours(1)).toBe(false);
|
||||
expect(peg.userRegisteredWithinLastHours(24)).toBe(true);
|
||||
advanceDateAndTime(24 * 60 * 60 * 1000);
|
||||
expect(peg.userRegisteredWithinLastHours(0)).toBe(false);
|
||||
expect(peg.userRegisteredWithinLastHours(1)).toBe(false);
|
||||
expect(peg.userRegisteredWithinLastHours(24)).toBe(false);
|
||||
});
|
||||
|
||||
it("setJustRegisteredUserId(null)", () => {
|
||||
stubClient();
|
||||
(peg as any).matrixClient = peg.get();
|
||||
peg.setJustRegisteredUserId(null);
|
||||
expect(peg.currentUserIsJustRegistered()).toBe(false);
|
||||
expect(peg.userRegisteredWithinLastHours(0)).toBe(false);
|
||||
expect(peg.userRegisteredWithinLastHours(1)).toBe(false);
|
||||
expect(peg.userRegisteredWithinLastHours(24)).toBe(false);
|
||||
advanceDateAndTime(1 * 60 * 60 * 1000 + 1);
|
||||
expect(peg.userRegisteredWithinLastHours(0)).toBe(false);
|
||||
expect(peg.userRegisteredWithinLastHours(1)).toBe(false);
|
||||
expect(peg.userRegisteredWithinLastHours(24)).toBe(false);
|
||||
});
|
||||
|
||||
describe(".start", () => {
|
||||
let testPeg: IMatrixClientPeg;
|
||||
|
||||
beforeEach(() => {
|
||||
// instantiate a MatrixClientPegClass instance, with a new MatrixClient
|
||||
testPeg = new PegClass();
|
||||
fetchMock.get("http://example.com/_matrix/client/versions", {});
|
||||
|
||||
const mockClient = createTestClient();
|
||||
mockClient.initRustCrypto = jest.fn();
|
||||
mockClient.startClient = jest.fn();
|
||||
testPeg.set(mockClient as unknown as MatrixClient);
|
||||
});
|
||||
|
||||
it("should initialise the rust crypto library by default", async () => {
|
||||
const mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined);
|
||||
|
||||
const cryptoStoreKey = new Uint8Array([1, 2, 3, 4]);
|
||||
await testPeg.start({ rustCryptoStoreKey: cryptoStoreKey });
|
||||
expect(mockInitRustCrypto).toHaveBeenCalledWith({ storageKey: cryptoStoreKey });
|
||||
});
|
||||
|
||||
it("should try to start dehydration if dehydration is enabled", async () => {
|
||||
const mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined);
|
||||
const mockStartDehydration = jest.fn();
|
||||
jest.spyOn(testPeg.safeGet(), "getCrypto").mockReturnValue({
|
||||
isDehydrationSupported: jest.fn().mockResolvedValue(true),
|
||||
startDehydration: mockStartDehydration,
|
||||
setDeviceIsolationMode: jest.fn(),
|
||||
} as any);
|
||||
jest.spyOn(testPeg.safeGet(), "waitForClientWellKnown").mockResolvedValue({
|
||||
"m.homeserver": {
|
||||
base_url: "http://example.com",
|
||||
},
|
||||
"org.matrix.msc3814": true,
|
||||
} as any);
|
||||
|
||||
const cryptoStoreKey = new Uint8Array([1, 2, 3, 4]);
|
||||
await testPeg.start({ rustCryptoStoreKey: cryptoStoreKey });
|
||||
expect(mockInitRustCrypto).toHaveBeenCalledWith({ storageKey: cryptoStoreKey });
|
||||
expect(mockStartDehydration).toHaveBeenCalledWith({ onlyIfKeyCached: true, rehydrate: false });
|
||||
});
|
||||
|
||||
it("Should migrate existing login", async () => {
|
||||
const mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined);
|
||||
|
||||
await testPeg.start();
|
||||
expect(mockInitRustCrypto).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { SettingLevel } from "../../src/settings/SettingLevel";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import { stubClient } from "../test-utils";
|
||||
import MediaDeviceHandler from "../../src/MediaDeviceHandler";
|
||||
import SettingsStore from "../../src/settings/SettingsStore";
|
||||
|
||||
jest.mock("../../src/settings/SettingsStore");
|
||||
|
||||
const SettingsStoreMock = mocked(SettingsStore);
|
||||
|
||||
describe("MediaDeviceHandler", () => {
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("sets audio settings", async () => {
|
||||
const expectedAudioSettings = new Map<string, boolean>([
|
||||
["webrtc_audio_autoGainControl", false],
|
||||
["webrtc_audio_echoCancellation", true],
|
||||
["webrtc_audio_noiseSuppression", false],
|
||||
]);
|
||||
|
||||
SettingsStoreMock.getValue.mockImplementation((settingName): any => {
|
||||
return expectedAudioSettings.get(settingName);
|
||||
});
|
||||
|
||||
await MediaDeviceHandler.setAudioAutoGainControl(false);
|
||||
await MediaDeviceHandler.setAudioEchoCancellation(true);
|
||||
await MediaDeviceHandler.setAudioNoiseSuppression(false);
|
||||
|
||||
expectedAudioSettings.forEach((value, key) => {
|
||||
expect(SettingsStoreMock.setValue).toHaveBeenCalledWith(key, null, SettingLevel.DEVICE, value);
|
||||
});
|
||||
|
||||
expect(MatrixClientPeg.safeGet().getMediaHandler().setAudioSettings).toHaveBeenCalledWith({
|
||||
autoGainControl: false,
|
||||
echoCancellation: true,
|
||||
noiseSuppression: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,262 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type IResultRoomEvents } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import eventSearch from "../../src/Searching";
|
||||
import EventIndexPeg from "../../src/indexing/EventIndexPeg";
|
||||
import { createTestClient } from "../test-utils";
|
||||
|
||||
describe("Searching", () => {
|
||||
const mockClient = createTestClient();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("localSearch", () => {
|
||||
it("removes state_key: null from search results", async () => {
|
||||
// Mock search results from Seshat that include state_key: null
|
||||
const mockSearchResults: IResultRoomEvents = {
|
||||
count: 2,
|
||||
results: [
|
||||
{
|
||||
rank: 1,
|
||||
result: {
|
||||
event_id: "$event1",
|
||||
room_id: "!room:example.org",
|
||||
sender: "@user:example.org",
|
||||
type: "m.room.message",
|
||||
origin_server_ts: 1234567890,
|
||||
content: { body: "test message 1", msgtype: "m.text" },
|
||||
// Seshat incorrectly includes state_key: null for non-state events
|
||||
state_key: null,
|
||||
} as any,
|
||||
context: {
|
||||
events_before: [
|
||||
{
|
||||
event_id: "$before1",
|
||||
room_id: "!room:example.org",
|
||||
sender: "@user:example.org",
|
||||
type: "m.room.message",
|
||||
origin_server_ts: 1234567889,
|
||||
content: { body: "before message", msgtype: "m.text" },
|
||||
state_key: null,
|
||||
} as any,
|
||||
],
|
||||
events_after: [
|
||||
{
|
||||
event_id: "$after1",
|
||||
room_id: "!room:example.org",
|
||||
sender: "@user:example.org",
|
||||
type: "m.room.message",
|
||||
origin_server_ts: 1234567891,
|
||||
content: { body: "after message", msgtype: "m.text" },
|
||||
state_key: null,
|
||||
} as any,
|
||||
],
|
||||
profile_info: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
result: {
|
||||
event_id: "$event2",
|
||||
room_id: "!room:example.org",
|
||||
sender: "@user:example.org",
|
||||
type: "m.room.message",
|
||||
origin_server_ts: 1234567880,
|
||||
content: { body: "test message 2", msgtype: "m.text" },
|
||||
state_key: null,
|
||||
} as any,
|
||||
context: {
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
profile_info: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
highlights: ["test"],
|
||||
};
|
||||
|
||||
// Mock EventIndex.search to return results with state_key: null
|
||||
const mockEventIndex = {
|
||||
search: jest.fn().mockResolvedValue(mockSearchResults),
|
||||
};
|
||||
jest.spyOn(EventIndexPeg, "get").mockReturnValue(mockEventIndex as any);
|
||||
|
||||
// Mock crypto to indicate room is encrypted
|
||||
jest.spyOn(mockClient, "getCrypto").mockReturnValue({
|
||||
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true),
|
||||
} as any);
|
||||
|
||||
// Perform search in an encrypted room
|
||||
const roomId = "!room:example.org";
|
||||
await eventSearch(mockClient, "test", roomId);
|
||||
|
||||
// Verify that state_key: null was removed from the search arguments passed to search
|
||||
expect(mockEventIndex.search).toHaveBeenCalled();
|
||||
|
||||
// Get the mock search results that were passed to processRoomEventsSearch
|
||||
// The state_key should have been deleted from the original results object
|
||||
const mainEventResult = mockSearchResults.results![0].result as unknown as Record<string, unknown>;
|
||||
expect(mainEventResult.state_key).toBeUndefined();
|
||||
|
||||
const beforeEvent = mockSearchResults.results![0].context!.events_before![0] as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(beforeEvent.state_key).toBeUndefined();
|
||||
|
||||
const afterEvent = mockSearchResults.results![0].context!.events_after![0] as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(afterEvent.state_key).toBeUndefined();
|
||||
|
||||
const secondResult = mockSearchResults.results![1].result as unknown as Record<string, unknown>;
|
||||
expect(secondResult.state_key).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not modify events without state_key: null", async () => {
|
||||
const mockSearchResults: IResultRoomEvents = {
|
||||
count: 1,
|
||||
results: [
|
||||
{
|
||||
rank: 1,
|
||||
result: {
|
||||
event_id: "$event1",
|
||||
room_id: "!room:example.org",
|
||||
sender: "@user:example.org",
|
||||
type: "m.room.message",
|
||||
origin_server_ts: 1234567890,
|
||||
content: { body: "test message", msgtype: "m.text" },
|
||||
// No state_key property at all (correct behavior)
|
||||
} as any,
|
||||
context: {
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
profile_info: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
highlights: ["test"],
|
||||
};
|
||||
|
||||
const mockEventIndex = {
|
||||
search: jest.fn().mockResolvedValue(mockSearchResults),
|
||||
};
|
||||
jest.spyOn(EventIndexPeg, "get").mockReturnValue(mockEventIndex as any);
|
||||
|
||||
jest.spyOn(mockClient, "getCrypto").mockReturnValue({
|
||||
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true),
|
||||
} as any);
|
||||
|
||||
const roomId = "!room:example.org";
|
||||
await eventSearch(mockClient, "test", roomId);
|
||||
|
||||
// Verify state_key is still undefined (not accidentally set to something)
|
||||
const eventResult = mockSearchResults.results![0].result as unknown as Record<string, unknown>;
|
||||
expect("state_key" in eventResult).toBe(false);
|
||||
});
|
||||
|
||||
it("handles missing context fields and empty result sets", async () => {
|
||||
const mockSearchResults: IResultRoomEvents = {
|
||||
count: 3,
|
||||
results: [
|
||||
{
|
||||
rank: 1,
|
||||
result: {
|
||||
event_id: "$event1",
|
||||
room_id: "!room:example.org",
|
||||
sender: "@user:example.org",
|
||||
type: "m.room.message",
|
||||
origin_server_ts: 1234567890,
|
||||
content: { body: "test message", msgtype: "m.text" },
|
||||
state_key: null,
|
||||
} as any,
|
||||
context: {
|
||||
events_before: [{ event_id: "$before1", state_key: "not-null" } as any],
|
||||
events_after: [{ event_id: "$after1", state_key: "not-null" } as any],
|
||||
profile_info: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
result: {
|
||||
event_id: "$event2",
|
||||
room_id: "!room:example.org",
|
||||
sender: "@user:example.org",
|
||||
type: "m.room.message",
|
||||
origin_server_ts: 1234567891,
|
||||
content: { body: "test message 2", msgtype: "m.text" },
|
||||
state_key: null,
|
||||
} as any,
|
||||
context: {
|
||||
profile_info: {},
|
||||
} as any,
|
||||
},
|
||||
{
|
||||
rank: 3,
|
||||
result: {
|
||||
event_id: "$event3",
|
||||
room_id: "!room:example.org",
|
||||
sender: "@user:example.org",
|
||||
type: "m.room.message",
|
||||
origin_server_ts: 1234567892,
|
||||
content: { body: "test message 3", msgtype: "m.text" },
|
||||
state_key: null,
|
||||
} as any,
|
||||
context: undefined as any,
|
||||
},
|
||||
],
|
||||
highlights: ["test"],
|
||||
};
|
||||
|
||||
const mockEventIndex = {
|
||||
search: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(mockSearchResults)
|
||||
.mockResolvedValueOnce({ count: 0, highlights: ["test"] } as IResultRoomEvents),
|
||||
};
|
||||
jest.spyOn(EventIndexPeg, "get").mockReturnValue(mockEventIndex as any);
|
||||
|
||||
jest.spyOn(mockClient, "getCrypto").mockReturnValue({
|
||||
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true),
|
||||
} as any);
|
||||
|
||||
const roomId = "!room:example.org";
|
||||
await eventSearch(mockClient, "test", roomId);
|
||||
await eventSearch(mockClient, "test", roomId);
|
||||
|
||||
const firstMainEvent = mockSearchResults.results![0].result as unknown as Record<string, unknown>;
|
||||
expect(firstMainEvent.state_key).toBeUndefined();
|
||||
|
||||
const beforeEvent = mockSearchResults.results![0].context!.events_before![0] as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(beforeEvent.state_key).toBe("not-null");
|
||||
|
||||
const afterEvent = mockSearchResults.results![0].context!.events_after![0] as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(afterEvent.state_key).toBe("not-null");
|
||||
|
||||
const secondMainEvent = mockSearchResults.results![1].result as unknown as Record<string, unknown>;
|
||||
expect(secondMainEvent.state_key).toBeUndefined();
|
||||
|
||||
const thirdMainEvent = mockSearchResults.results![2].result as unknown as Record<string, unknown>;
|
||||
expect(thirdMainEvent.state_key).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,243 +0,0 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type SlidingSync, SlidingSyncEvent, SlidingSyncState } from "matrix-js-sdk/src/sliding-sync";
|
||||
import { mocked } from "jest-mock";
|
||||
import { ClientEvent, type MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import fetchMock from "@fetch-mock/jest";
|
||||
import EventEmitter from "events";
|
||||
import { waitFor } from "jest-matrix-react";
|
||||
|
||||
import { SlidingSyncManager } from "../../src/SlidingSyncManager";
|
||||
import { mkStubRoom, stubClient } from "../test-utils";
|
||||
|
||||
class MockSlidingSync extends EventEmitter {
|
||||
lists = {};
|
||||
listModifiedCount = 0;
|
||||
terminated = false;
|
||||
needsResend = false;
|
||||
modifyRoomSubscriptions = jest.fn();
|
||||
getRoomSubscriptions = jest.fn();
|
||||
useCustomSubscription = jest.fn();
|
||||
getListParams = jest.fn();
|
||||
setList = jest.fn();
|
||||
setListRanges = jest.fn();
|
||||
getListData = jest.fn();
|
||||
extensions = jest.fn();
|
||||
desiredRoomSubscriptions = jest.fn();
|
||||
}
|
||||
|
||||
describe("SlidingSyncManager", () => {
|
||||
let manager: SlidingSyncManager;
|
||||
let slidingSync: SlidingSync;
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
slidingSync = new MockSlidingSync() as unknown as SlidingSync;
|
||||
manager = new SlidingSyncManager();
|
||||
client = stubClient();
|
||||
// by default the client has no rooms: stubClient magically makes rooms annoyingly.
|
||||
mocked(client.getRoom).mockReturnValue(null);
|
||||
(manager as any).configure(client, "invalid");
|
||||
manager.slidingSync = slidingSync;
|
||||
fetchMock.get("https://proxy/client/server.json", {});
|
||||
});
|
||||
|
||||
describe("setRoomVisible", () => {
|
||||
it("adds a subscription for the room", async () => {
|
||||
const roomId = "!room:id";
|
||||
mocked(client.getRoom).mockReturnValue(mkStubRoom(roomId, "foo", client));
|
||||
const subs = new Set<string>();
|
||||
mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs);
|
||||
await manager.setRoomVisible(roomId);
|
||||
expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set<string>([roomId]));
|
||||
});
|
||||
|
||||
it("adds a custom subscription for a lazy-loadable room", async () => {
|
||||
const roomId = "!lazy:id";
|
||||
const room = new Room(roomId, client, client.getUserId()!);
|
||||
room.getLiveTimeline().initialiseState([
|
||||
new MatrixEvent({
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
event_id: "$abc123",
|
||||
sender: client.getUserId()!,
|
||||
content: {
|
||||
creator: client.getUserId()!,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
mocked(client.getRoom).mockImplementation((r: string): Room | null => {
|
||||
if (roomId === r) {
|
||||
return room;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const subs = new Set<string>();
|
||||
mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs);
|
||||
await manager.setRoomVisible(roomId);
|
||||
expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set<string>([roomId]));
|
||||
// we aren't prescriptive about what the sub name is.
|
||||
expect(slidingSync.useCustomSubscription).toHaveBeenCalledWith(roomId, expect.anything());
|
||||
});
|
||||
|
||||
it("waits if the room is not yet known", async () => {
|
||||
const roomId = "!room:id";
|
||||
mocked(client.getRoom).mockReturnValue(null);
|
||||
const subs = new Set<string>();
|
||||
mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs);
|
||||
|
||||
const setVisibleDone = jest.fn();
|
||||
manager.setRoomVisible(roomId).then(setVisibleDone);
|
||||
|
||||
await waitFor(() => expect(client.getRoom).toHaveBeenCalledWith(roomId));
|
||||
|
||||
expect(setVisibleDone).not.toHaveBeenCalled();
|
||||
|
||||
const stubRoom = mkStubRoom(roomId, "foo", client);
|
||||
mocked(client.getRoom).mockReturnValue(stubRoom);
|
||||
client.emit(ClientEvent.Room, stubRoom);
|
||||
|
||||
await waitFor(() => expect(setVisibleDone).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureListRegistered", () => {
|
||||
it("creates a new list based on the key", async () => {
|
||||
const listKey = "key";
|
||||
mocked(slidingSync.getListParams).mockReturnValue(null);
|
||||
await manager.ensureListRegistered(listKey, {
|
||||
sort: ["by_recency"],
|
||||
});
|
||||
expect(slidingSync.setList).toHaveBeenCalledWith(
|
||||
listKey,
|
||||
expect.objectContaining({
|
||||
sort: ["by_recency"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("updates an existing list based on the key", async () => {
|
||||
const listKey = "key";
|
||||
mocked(slidingSync.getListParams).mockReturnValue({
|
||||
ranges: [[0, 42]],
|
||||
});
|
||||
await manager.ensureListRegistered(listKey, {
|
||||
sort: ["by_recency"],
|
||||
});
|
||||
expect(slidingSync.setList).toHaveBeenCalledWith(
|
||||
listKey,
|
||||
expect.objectContaining({
|
||||
sort: ["by_recency"],
|
||||
ranges: [[0, 42]],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("updates ranges on an existing list based on the key if there's no other changes", async () => {
|
||||
const listKey = "key";
|
||||
mocked(slidingSync.getListParams).mockReturnValue({
|
||||
ranges: [[0, 42]],
|
||||
});
|
||||
await manager.ensureListRegistered(listKey, {
|
||||
ranges: [[0, 52]],
|
||||
});
|
||||
expect(slidingSync.setList).not.toHaveBeenCalled();
|
||||
expect(slidingSync.setListRanges).toHaveBeenCalledWith(listKey, [[0, 52]]);
|
||||
});
|
||||
|
||||
it("no-ops for idential changes", async () => {
|
||||
const listKey = "key";
|
||||
mocked(slidingSync.getListParams).mockReturnValue({
|
||||
ranges: [[0, 42]],
|
||||
sort: ["by_recency"],
|
||||
});
|
||||
await manager.ensureListRegistered(listKey, {
|
||||
ranges: [[0, 42]],
|
||||
sort: ["by_recency"],
|
||||
});
|
||||
expect(slidingSync.setList).not.toHaveBeenCalled();
|
||||
expect(slidingSync.setListRanges).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("startSpidering", () => {
|
||||
it("requests in expanding batchSizes", async () => {
|
||||
const gapMs = 1;
|
||||
const batchSize = 10;
|
||||
mocked(slidingSync.getListData).mockImplementation((key) => {
|
||||
return {
|
||||
joinedCount: 64,
|
||||
roomIndexToRoomId: {},
|
||||
};
|
||||
});
|
||||
await (manager as any).startSpidering(slidingSync, batchSize, gapMs);
|
||||
|
||||
// we expect calls for 10,19 -> 20,29 -> 30,39 -> 40,49 -> 50,59 -> 60,69
|
||||
const wantWindows = [
|
||||
[0, 10],
|
||||
[0, 20],
|
||||
[0, 30],
|
||||
[0, 40],
|
||||
[0, 50],
|
||||
[0, 60],
|
||||
[0, 70],
|
||||
];
|
||||
|
||||
for (let i = 1; i < wantWindows.length; ++i) {
|
||||
// each time we emit, it should expand the range of all 5 lists by 10 until
|
||||
// they all include all the rooms (64), which is 6 emits.
|
||||
slidingSync.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, null, undefined);
|
||||
await waitFor(() => expect(slidingSync.getListData).toHaveBeenCalledTimes(i * 5));
|
||||
expect(slidingSync.setListRanges).toHaveBeenCalledTimes(i * 5);
|
||||
expect(slidingSync.setListRanges).toHaveBeenCalledWith("spaces", [wantWindows[i]]);
|
||||
}
|
||||
});
|
||||
it("handles accounts with zero rooms", async () => {
|
||||
const gapMs = 1;
|
||||
const batchSize = 10;
|
||||
mocked(slidingSync.getListData).mockImplementation((key) => {
|
||||
return {
|
||||
joinedCount: 0,
|
||||
roomIndexToRoomId: {},
|
||||
};
|
||||
});
|
||||
await (manager as any).startSpidering(slidingSync, batchSize, gapMs);
|
||||
slidingSync.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, null, undefined);
|
||||
await waitFor(() => expect(slidingSync.getListData).toHaveBeenCalledTimes(5));
|
||||
// should not have needed to expand the range
|
||||
expect(slidingSync.setListRanges).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe("checkSupport", () => {
|
||||
beforeEach(() => {
|
||||
SlidingSyncManager.serverSupportsSlidingSync = false;
|
||||
});
|
||||
it("shorts out if the server has 'native' sliding sync support", async () => {
|
||||
jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(true);
|
||||
expect(SlidingSyncManager.serverSupportsSlidingSync).toBeFalsy();
|
||||
await manager.checkSupport(client);
|
||||
expect(SlidingSyncManager.serverSupportsSlidingSync).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe("setup", () => {
|
||||
let untypedManager: any;
|
||||
|
||||
beforeEach(() => {
|
||||
untypedManager = manager;
|
||||
jest.spyOn(untypedManager, "configure");
|
||||
jest.spyOn(untypedManager, "startSpidering");
|
||||
});
|
||||
it("uses the baseUrl", async () => {
|
||||
await manager.setup(client);
|
||||
expect(untypedManager.configure).toHaveBeenCalled();
|
||||
expect(untypedManager.configure).toHaveBeenCalledWith(client, client.baseUrl);
|
||||
expect(untypedManager.startSpidering).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ClientEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import AccountSettingsHandler from "../../../../src/settings/handlers/AccountSettingsHandler.ts";
|
||||
import { WatchManager } from "../../../../src/settings/WatchManager.ts";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
|
||||
describe("AccountSettingsHandler", () => {
|
||||
const watchManager = new WatchManager();
|
||||
const handler = new AccountSettingsHandler(watchManager);
|
||||
|
||||
beforeEach(stubClient);
|
||||
|
||||
it("should notify watchers of recent_emoji on account data update", async () => {
|
||||
const fn = jest.fn();
|
||||
handler.watchers.watchSetting("recent_emoji", null, fn);
|
||||
|
||||
const ev = new MatrixEvent({
|
||||
type: "io.element.recent_emoji",
|
||||
content: {
|
||||
recent_emoji: [["🤒", 1]],
|
||||
},
|
||||
});
|
||||
mocked(handler.client.getAccountData).mockImplementation((eventType) =>
|
||||
eventType === "io.element.recent_emoji" ? ev : undefined,
|
||||
);
|
||||
handler.client.emit(ClientEvent.AccountData, ev);
|
||||
|
||||
expect(fn).toHaveBeenCalledWith(null, "account", [{ emoji: "🤒", total: 1 }]);
|
||||
});
|
||||
|
||||
it("should write value to account data correctly", async () => {
|
||||
void handler.setValue("pseudonymousAnalyticsOptIn", null, true);
|
||||
|
||||
expect(handler.client.setAccountData).toHaveBeenCalledWith("im.vector.analytics", {
|
||||
pseudonymousAnalyticsOptIn: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
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 { type 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
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 { type 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);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,313 +0,0 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`showError should match snapshot 1`] = `
|
||||
<div
|
||||
id="matrixchat"
|
||||
>
|
||||
<div
|
||||
class="mx_ErrorView cpd-theme-light"
|
||||
>
|
||||
<img
|
||||
alt="Element"
|
||||
class="mx_ErrorView_logo"
|
||||
height="160"
|
||||
src="themes/element/img/logos/element-app-logo.png"
|
||||
/>
|
||||
<div
|
||||
class="mx_ErrorView_container"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
>
|
||||
Error title
|
||||
</h1>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-lg-regular_6v6n8_69"
|
||||
>
|
||||
msg1
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-lg-regular_6v6n8_69"
|
||||
>
|
||||
msg2
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`showIncompatibleBrowser should match snapshot 1`] = `
|
||||
<div
|
||||
id="matrixchat"
|
||||
>
|
||||
<div
|
||||
class="mx_ErrorView cpd-theme-light"
|
||||
>
|
||||
<img
|
||||
alt="Element"
|
||||
class="mx_ErrorView_logo"
|
||||
height="160"
|
||||
src="themes/element/img/logos/element-app-logo.png"
|
||||
/>
|
||||
<div
|
||||
class="mx_ErrorView_container"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
>
|
||||
Element does not support this browser
|
||||
</h1>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-lg-regular_6v6n8_69"
|
||||
>
|
||||
Element uses some browser features which are not available in your current browser. If you continue, some features may stop working and there is a risk that you may lose data in the future.
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-lg-regular_6v6n8_69"
|
||||
>
|
||||
<span>
|
||||
For the best experience, use
|
||||
<a
|
||||
href="https://google.com/chrome"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Chrome
|
||||
</a>
|
||||
,
|
||||
<a
|
||||
href="https://firefox.com"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Firefox
|
||||
</a>
|
||||
,
|
||||
<a
|
||||
href="https://microsoft.com/edge"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Edge
|
||||
</a>
|
||||
, or
|
||||
<a
|
||||
href="https://apple.com/safari"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Safari
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_ErrorView_flexContainer mx_ErrorView_buttons"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="secondary"
|
||||
data-size="md"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
|
||||
/>
|
||||
<path
|
||||
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
|
||||
/>
|
||||
</svg>
|
||||
Learn more
|
||||
</button>
|
||||
<button
|
||||
class="_button_1nw83_8"
|
||||
data-kind="primary"
|
||||
data-size="md"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Continue anyway
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_separator_13qwf_8"
|
||||
data-kind="primary"
|
||||
data-orientation="horizontal"
|
||||
role="separator"
|
||||
/>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Use Element Desktop instead
|
||||
</h2>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_ErrorView_flexContainer"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<a
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
href="https://packages.element.io/desktop/install/macos/Element.dmg"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M16.099 2.4a4.1 4.1 0 0 1-1.057 3.073c-.747.863-1.878 1.36-3.07 1.348-.075-1.081.315-2.146 1.085-2.96.78-.825 1.866-1.346 3.042-1.461m3.767 6.54c-1.37.783-2.214 2.163-2.234 3.657.002 1.69 1.092 3.215 2.768 3.873a9.4 9.4 0 0 1-1.44 2.723c-.848 1.178-1.737 2.329-3.149 2.35-.671.015-1.124-.165-1.596-.351-.493-.195-1.006-.398-1.809-.398-.852 0-1.388.21-1.905.412-.447.174-.88.343-1.49.367-1.343.046-2.37-1.258-3.25-2.425-1.756-2.383-3.124-6.716-1.29-9.664.86-1.437 2.471-2.349 4.241-2.402.763-.015 1.494.258 2.135.497.49.183.929.347 1.287.347.315 0 .74-.157 1.237-.34.78-.288 1.737-.64 2.71-.545 1.514.044 2.917.748 3.785 1.9"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Mac
|
||||
</a>
|
||||
<a
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
href="https://packages.element.io/desktop/install/win32/x64/Element%20Setup.exe"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.589 2.4H21.6v9.011h-9.011zM2.4 12.588h9.011v9.011H2.4zM2.4 2.4h9.011v9.011H2.4zm10.189 10.188H21.6v9.011h-9.011z"
|
||||
/>
|
||||
</svg>
|
||||
Windows (64-bit)
|
||||
</a>
|
||||
<a
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
href="https://packages.element.io/desktop/install/win32/arm64/Element%20Setup.exe"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.589 2.4H21.6v9.011h-9.011zM2.4 12.588h9.011v9.011H2.4zM2.4 2.4h9.011v9.011H2.4zm10.189 10.188H21.6v9.011h-9.011z"
|
||||
/>
|
||||
</svg>
|
||||
Windows (ARM 64-bit)
|
||||
</a>
|
||||
<a
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
href="https://element.io/download#linux"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#cpd_LinuxIcon_a)"
|
||||
>
|
||||
<path
|
||||
d="M13.206 20.644a2.7 2.7 0 0 1-1.097.248 2.8 2.8 0 0 1-1.567-.495c-.153.38-.392.723-.7 1a2.8 2.8 0 0 1-1.078.604h6.141a2.8 2.8 0 0 1-.99-.51 2.8 2.8 0 0 1-.709-.847M6.71 16.673a.2.2 0 0 0 .067-.03q.056-.49.13-.94c-.01-.169.017-.338.08-.495.348-1.904.834-3.24 1.504-4.052a.13.13 0 0 1 .18-.018.123.123 0 0 1 .018.174 4 4 0 0 0-.203.272q-.764 1.113-1.199 3.389h.03a.6.6 0 0 1 .166 0c.198.042.508.203.985.67.061-2.72 1.661-4.913 3.642-4.913 1.748 0 3.208 1.715 3.556 3.997a1.7 1.7 0 0 1 .762-.742.8.8 0 0 1 .254-.047 7.5 7.5 0 0 0-.948-2.342 4 4 0 0 0-.2-.272.12.12 0 0 1-.03-.09.12.12 0 0 1 .048-.083.13.13 0 0 1 .092-.028.13.13 0 0 1 .085.045c.508.624.93 1.557 1.245 2.822.678.247.696 1.361.71 2.267 0 .43 0 .877.105.99.104.114.325.09.635-.04.036-.393.053-.742.063-1.036v-1.082c0-2.475-3.266-6.755-3.266-6.755l-.378-3.218c0-2.953-2.733-2.926-2.733-2.926s-2.743-.027-2.743 2.914l-.365 3.23s-3.267 4.277-3.267 6.755v.31s-.012.299 0 .767v.227c.414.154.839.315.976.28m5.388-10.41c.761.068 1.975.247 2.031.613.04.282-.429.822-.523.926-.195.215-.876.92-1.49.92-.615 0-1.296-.705-1.49-.92-.093-.104-.563-.644-.525-.926.046-.374 1.273-.545 1.996-.614"
|
||||
/>
|
||||
<path
|
||||
d="M12.111 8.475c.333 0 .843-.33 1.298-.837.348-.384.475-.666.465-.73-.064-.127-.894-.317-1.778-.401-.856.084-1.687.274-1.75.406.091.275.252.523.467.722.457.51.965.84 1.298.84m.684 10.664c-.292-2.607-.023-3.146.206-3.324a.42.42 0 0 1 .4-.057c.23.11.431.271.587.47.287.304.488.495.674.391.131-.077.32-.495.507-.879.097-.205.196-.425.305-.636-.254-2.33-1.65-4.126-3.355-4.126-1.872 0-3.393 2.166-3.393 4.827v.096c.31.317.68.743 1.133 1.3q.266.328.487.685a2.7 2.7 0 0 1 .29 2.258c.426.312.943.485 1.476.495.341-.002.679-.073.99-.21a2.6 2.6 0 0 1-.16-.46 7 7 0 0 1-.147-.83"
|
||||
/>
|
||||
<path
|
||||
d="M9.651 17.35c-.355-.44-.657-.789-.916-1.071l-.13-.139-.132-.138c-.587-.594-.897-.758-1.06-.785a.2.2 0 0 0-.068 0 .15.15 0 0 0-.114.082v.017a.9.9 0 0 0-.058.406v.327c.034.25-.019.505-.15.723a.4.4 0 0 1-.087.071l-.053.032a.6.6 0 0 1-.107.043H6.75c-.211.037-.554-.084-.988-.248l-.127-.05c-.544-.21-1.179-.452-1.644-.452a.63.63 0 0 0-.553.232c-.34.495.421 1.273 1.036 1.901.378.389.675.693.708.921.059.42-.393.582-.792.725a1.5 1.5 0 0 0-.564.278c-.06.069-.058.116-.045.15.055.191.507.61 2.897 1.285q.398.112.806.178c.49.073.992.01 1.448-.182a2.64 2.64 0 0 0 1.13-.902q.165-.245.271-.522l.05-.121c0-.042.024-.09.037-.131a2.48 2.48 0 0 0-.293-1.98 7 7 0 0 0-.475-.65m9.618-.17c-.212.02-.418.084-.604.186l-.277.117c-.305.113-.61.168-.807-.095a.6.6 0 0 1-.094-.227 3.6 3.6 0 0 1-.056-.592v-.322c0-.72-.026-1.586-.373-1.925a.5.5 0 0 0-.255-.14h-.038a.5.5 0 0 0-.254.035c-.3.12-.56.496-.79.936l-.109.215-.117.248-.104.225c-.223.495-.398.874-.601.99-.376.216-.704-.13-.991-.438a1.5 1.5 0 0 0-.48-.4.2.2 0 0 0-.076-.016q-.048 0-.084.03c-.158.124-.384.69-.112 3.104q.044.4.137.792.052.207.137.404.027.056.056.111c.02.037.033.074.054.111.208.377.51.696.879.928a2.66 2.66 0 0 0 2.504.171c.398-.18.743-.454 1.003-.8a7 7 0 0 0 .45-.675c1.218-2.116 1.241-2.722 1.147-2.896a.15.15 0 0 0-.144-.077"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="cpd_LinuxIcon_a"
|
||||
>
|
||||
<path
|
||||
d="M3.167 2.19H20.5V22H3.165z"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
Linux
|
||||
</a>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Or use our mobile app
|
||||
</h2>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_ErrorView_flexContainer"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-6x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<a
|
||||
href="https://apps.apple.com/app/vector/id1083446067"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="Apple App Store"
|
||||
height="64"
|
||||
src="themes/element/img/download/apple.svg"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=im.vector.app"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="Google Play Store"
|
||||
height="64"
|
||||
src="themes/element/img/download/google.svg"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://f-droid.org/repository/browse/?fdid=im.vector.app"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="F-Droid"
|
||||
height="64"
|
||||
src="themes/element/img/download/fdroid.svg"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* @jest-environment jest-fixed-jsdom
|
||||
* @jest-environment-options {"url": "https://app.element.io/#/room/#room:server"}
|
||||
*/
|
||||
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import fetchMock from "@fetch-mock/jest";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { Crypto } from "@peculiar/webcrypto";
|
||||
|
||||
import { loadApp } from "../../../src/vector/app.tsx";
|
||||
import SdkConfig from "../../../src/SdkConfig.ts";
|
||||
import PlatformPeg from "../../../src/PlatformPeg.ts";
|
||||
import { mockPlatformPeg, unmockPlatformPeg } from "../../test-utils";
|
||||
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
||||
|
||||
const defaultConfig = {
|
||||
default_hs_url: "https://synapse",
|
||||
};
|
||||
const issuer = "https://auth.org/";
|
||||
const webCrypto = new Crypto();
|
||||
|
||||
describe("sso_redirect_options", () => {
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, "crypto", {
|
||||
value: {
|
||||
// Stable stub
|
||||
getRandomValues: (arr: Uint8Array) => {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i] = i;
|
||||
}
|
||||
return arr;
|
||||
},
|
||||
subtle: webCrypto.subtle,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
SdkConfig.reset();
|
||||
mockPlatformPeg({ getDefaultDeviceDisplayName: jest.fn(), startSingleSignOn: jest.fn() });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
unmockPlatformPeg();
|
||||
});
|
||||
|
||||
describe("immediate", () => {
|
||||
beforeEach(() => {
|
||||
SdkConfig.put({
|
||||
...defaultConfig,
|
||||
sso_redirect_options: { immediate: true },
|
||||
// Avoid testing dynamic client registration
|
||||
oidc_static_clients: { [issuer]: { client_id: "12345" } },
|
||||
});
|
||||
// Signal we support v1.1 to pass the minimum js-sdk compatibility bar
|
||||
// Signal we support v1.15 to use stable Native OIDC support
|
||||
fetchMock.get("https://synapse/_matrix/client/versions", { versions: ["v1.1", "v1.15"] });
|
||||
});
|
||||
|
||||
it("should redirect for legacy SSO", async () => {
|
||||
fetchMock.getOnce("https://synapse/_matrix/client/v3/login", {
|
||||
flows: [{ stages: ["m.login.sso"] }],
|
||||
});
|
||||
|
||||
const startSingleSignOnSpy = jest.spyOn(PlatformPeg.get()!, "startSingleSignOn");
|
||||
|
||||
await loadApp({}, jest.fn());
|
||||
expect(startSingleSignOnSpy).toHaveBeenCalledWith(expect.any(MatrixClient), "sso", "/room/#room:server");
|
||||
});
|
||||
|
||||
it("should redirect for native OIDC", async () => {
|
||||
const authConfig = { ...makeDelegatedAuthConfig(issuer), response_modes_supported: ["query", "fragment"] };
|
||||
fetchMock.get("https://synapse/_matrix/client/v1/auth_metadata", authConfig);
|
||||
fetchMock.get(`${authConfig.issuer}.well-known/openid-configuration`, authConfig);
|
||||
fetchMock.get(authConfig.jwks_uri!, { keys: [] });
|
||||
|
||||
const startOidcLoginSpy = jest.spyOn(window.location, "href", "set");
|
||||
|
||||
await loadApp({}, jest.fn());
|
||||
expect(startOidcLoginSpy).toHaveBeenCalledWith(
|
||||
"https://auth.org/auth?client_id=12345&redirect_uri=https%3A%2F%2Fapp.element.io%2F%3Fno_universal_links%3Dtrue&response_type=code&scope=openid+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Aapi%3A*+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Adevice%3AwKpa6hpi3Y&nonce=38QgU2Pomx&state=10000000100040008000100000000000&code_challenge=awE81eIsGff70JahvrTqWRbGKLI10ooyo_Xm1sxuZvU&code_challenge_method=S256&response_mode=fragment",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* @jest-environment jest-fixed-jsdom
|
||||
* @jest-environment-options {"url": "https://app.element.io/?loginToken=123&no_universal_links&something_else=value#/home?state=abc&code=xyz"}
|
||||
*/
|
||||
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import fetchMock from "@fetch-mock/jest";
|
||||
import { waitFor, screen } from "jest-matrix-react";
|
||||
|
||||
import { loadApp, showError, showIncompatibleBrowser } from "../../../src/vector/init.tsx";
|
||||
import SdkConfig from "../../../src/SdkConfig.ts";
|
||||
import MatrixChat from "../../../src/components/structures/MatrixChat.tsx";
|
||||
import { parseAppUrl } from "../../../src/vector/url_utils.ts";
|
||||
|
||||
function setUpMatrixChatDiv() {
|
||||
document.getElementById("matrixchat")?.remove();
|
||||
const div = document.createElement("div");
|
||||
div.id = "matrixchat";
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
describe("showIncompatibleBrowser", () => {
|
||||
beforeEach(setUpMatrixChatDiv);
|
||||
|
||||
it("should match snapshot", async () => {
|
||||
await showIncompatibleBrowser(jest.fn());
|
||||
await screen.findByText("Element does not support this browser");
|
||||
expect(document.getElementById("matrixchat")).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("showError", () => {
|
||||
beforeEach(setUpMatrixChatDiv);
|
||||
|
||||
it("should match snapshot", async () => {
|
||||
await showError("Error title", ["msg1", "msg2"]);
|
||||
await screen.findByText("Error title");
|
||||
expect(document.getElementById("matrixchat")).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadApp", () => {
|
||||
beforeEach(setUpMatrixChatDiv);
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.get("https://matrix.org/_matrix/client/versions", { versions: ["v1.6"] });
|
||||
SdkConfig.put({ default_server_config: { "m.homeserver": { base_url: "https://matrix.org" } } });
|
||||
});
|
||||
|
||||
it("should set window.matrixChat to the MatrixChat instance", async () => {
|
||||
await loadApp({});
|
||||
await waitFor(() => expect(window.matrixChat).toBeInstanceOf(MatrixChat));
|
||||
});
|
||||
|
||||
it("should pass onTokenLoginCompleted which strips searchParams & fragment to MatrixChat", async () => {
|
||||
const spy = jest.spyOn(window.history, "replaceState");
|
||||
|
||||
await loadApp({});
|
||||
await waitFor(() => expect(window.matrixChat).toBeInstanceOf(MatrixChat));
|
||||
window.matrixChat!.props.onTokenLoginCompleted(parseAppUrl(window.location).params, "/home");
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(null, "", "https://app.element.io/?something_else=value#/home");
|
||||
});
|
||||
});
|
||||
@@ -1,531 +0,0 @@
|
||||
/*
|
||||
Copyright 2024-2025 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked, type MockedObject } from "jest-mock";
|
||||
import { waitFor } from "jest-matrix-react";
|
||||
|
||||
import { UpdateCheckStatus } from "../../../../src/BasePlatform";
|
||||
import { Action } from "../../../../src/dispatcher/actions";
|
||||
import dispatcher from "../../../../src/dispatcher/dispatcher";
|
||||
import * as rageshake from "../../../../src/rageshake/rageshake";
|
||||
import { BreadcrumbsStore } from "../../../../src/stores/BreadcrumbsStore";
|
||||
import Modal from "../../../../src/Modal";
|
||||
import DesktopCapturerSourcePicker from "../../../../src/components/views/elements/DesktopCapturerSourcePicker";
|
||||
import ElectronPlatform from "../../../../src/vector/platform/ElectronPlatform";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import ToastStore from "../../../../src/stores/ToastStore.ts";
|
||||
|
||||
jest.mock("../../../../src/rageshake/rageshake", () => ({
|
||||
flush: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("ElectronPlatform", () => {
|
||||
const initialiseValues = jest.fn().mockReturnValue({
|
||||
protocol: "io.element.desktop",
|
||||
sessionId: "session-id",
|
||||
config: { _config: true },
|
||||
supportedSettings: { setting1: false, setting2: true },
|
||||
supportsBadgeOverlay: false,
|
||||
});
|
||||
const defaultUserAgent =
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
|
||||
"(KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36";
|
||||
const mockElectron = {
|
||||
on: jest.fn(),
|
||||
send: jest.fn(),
|
||||
initialise: initialiseValues,
|
||||
setSettingValue: jest.fn().mockResolvedValue(undefined),
|
||||
getSettingValue: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as MockedObject<Electron>;
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
const dispatchFireSpy = jest.spyOn(dispatcher, "fire");
|
||||
const logSpy = jest.spyOn(logger, "log").mockImplementation(() => {});
|
||||
|
||||
const userId = "@alice:server.org";
|
||||
const deviceId = "device-id";
|
||||
|
||||
beforeEach(() => {
|
||||
window.electron = mockElectron;
|
||||
jest.clearAllMocks();
|
||||
Object.defineProperty(window, "navigator", { value: { userAgent: defaultUserAgent }, writable: true });
|
||||
});
|
||||
|
||||
const getElectronEventHandlerCall = (
|
||||
eventType: string,
|
||||
): [type: string, handler: (...args: any) => void] | undefined =>
|
||||
mockElectron.on.mock.calls.find(([type]) => type === eventType);
|
||||
|
||||
it("flushes rageshake before quitting", () => {
|
||||
new ElectronPlatform();
|
||||
const [event, handler] = getElectronEventHandlerCall("before-quit")!;
|
||||
// correct event bound
|
||||
expect(event).toBeTruthy();
|
||||
|
||||
handler();
|
||||
|
||||
expect(logSpy).toHaveBeenCalled();
|
||||
expect(rageshake.flush).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should load config", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await expect(platform.getConfig()).resolves.toEqual({ _config: true });
|
||||
});
|
||||
|
||||
it("should return oidc client state as expected", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await platform.getConfig();
|
||||
expect(platform.getOidcClientState()).toMatchInlineSnapshot(`":element-desktop-ssoid:session-id"`);
|
||||
});
|
||||
|
||||
it("dispatches view settings action on preferences event", () => {
|
||||
new ElectronPlatform();
|
||||
const [event, handler] = getElectronEventHandlerCall("preferences")!;
|
||||
// correct event bound
|
||||
expect(event).toBeTruthy();
|
||||
|
||||
handler();
|
||||
|
||||
expect(dispatchFireSpy).toHaveBeenCalledWith(Action.ViewUserSettings);
|
||||
});
|
||||
|
||||
it("creates a modal on openDesktopCapturerSourcePicker", async () => {
|
||||
const plat = new ElectronPlatform();
|
||||
Modal.createDialog = jest.fn();
|
||||
|
||||
// @ts-ignore mock
|
||||
mocked(Modal.createDialog).mockReturnValue({
|
||||
finished: new Promise((r) => r(["source"])),
|
||||
});
|
||||
|
||||
let res: () => void;
|
||||
const waitForIPCSend = new Promise<void>((r) => {
|
||||
res = r;
|
||||
});
|
||||
// @ts-ignore mock
|
||||
jest.spyOn(plat.ipc, "call").mockImplementation(() => {
|
||||
res();
|
||||
});
|
||||
|
||||
const [event, handler] = getElectronEventHandlerCall("openDesktopCapturerSourcePicker")!;
|
||||
handler();
|
||||
|
||||
await waitForIPCSend;
|
||||
|
||||
expect(event).toBeTruthy();
|
||||
expect(Modal.createDialog).toHaveBeenCalledWith(DesktopCapturerSourcePicker);
|
||||
// @ts-ignore mock
|
||||
expect(plat.ipc.call).toHaveBeenCalledWith("callDisplayMediaCallback", "source");
|
||||
});
|
||||
|
||||
it("should show a toast when showToast is fired", async () => {
|
||||
new ElectronPlatform();
|
||||
dispatcher.dispatch(
|
||||
{
|
||||
action: Action.ClientStarted,
|
||||
},
|
||||
true,
|
||||
);
|
||||
const spy = jest.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast");
|
||||
|
||||
const [event, handler] = getElectronEventHandlerCall("showToast")!;
|
||||
handler({} as any, { title: "title", description: "description" });
|
||||
|
||||
expect(event).toBeTruthy();
|
||||
await waitFor(() =>
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "title",
|
||||
props: expect.objectContaining({ description: "description" }),
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe("updates", () => {
|
||||
it("dispatches on check updates action", () => {
|
||||
new ElectronPlatform();
|
||||
const [event, handler] = getElectronEventHandlerCall("check_updates")!;
|
||||
// correct event bound
|
||||
expect(event).toBeTruthy();
|
||||
|
||||
handler({}, true);
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.CheckUpdates,
|
||||
status: UpdateCheckStatus.Downloading,
|
||||
});
|
||||
});
|
||||
|
||||
it("dispatches on check updates action when update not available", () => {
|
||||
new ElectronPlatform();
|
||||
const [, handler] = getElectronEventHandlerCall("check_updates")!;
|
||||
|
||||
handler({}, false);
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.CheckUpdates,
|
||||
status: UpdateCheckStatus.NotAvailable,
|
||||
});
|
||||
});
|
||||
|
||||
it("starts update check", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
platform.startUpdateCheck();
|
||||
expect(mockElectron.send).toHaveBeenCalledWith("check_updates");
|
||||
});
|
||||
|
||||
it("installs update", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
platform.installUpdate();
|
||||
expect(mockElectron.send).toHaveBeenCalledWith("install_update");
|
||||
});
|
||||
});
|
||||
|
||||
it("returns human readable name", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.getHumanReadableName()).toEqual("Electron Platform");
|
||||
});
|
||||
|
||||
describe("getDefaultDeviceDisplayName", () => {
|
||||
it.each([
|
||||
[
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
|
||||
"(KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
|
||||
"Element Desktop: macOS",
|
||||
],
|
||||
[
|
||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) " +
|
||||
"electron/1.0.0 Chrome/53.0.2785.113 Electron/1.4.3 Safari/537.36",
|
||||
"Element Desktop: Windows",
|
||||
],
|
||||
["Mozilla/5.0 (X11; Linux i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Element Desktop: Linux"],
|
||||
["Mozilla/5.0 (X11; FreeBSD i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Element Desktop: FreeBSD"],
|
||||
["Mozilla/5.0 (X11; OpenBSD i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Element Desktop: OpenBSD"],
|
||||
["Mozilla/5.0 (X11; SunOS i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Element Desktop: SunOS"],
|
||||
["custom user agent", "Element Desktop: Unknown"],
|
||||
])("%s = %s", (userAgent, result) => {
|
||||
Object.defineProperty(window, "navigator", { value: { userAgent }, writable: true });
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.getDefaultDeviceDisplayName()).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns true for needsUrlTooltips", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.needsUrlTooltips()).toBe(true);
|
||||
});
|
||||
|
||||
it("should override browser shortcuts", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.overrideBrowserShortcuts()).toBe(true);
|
||||
});
|
||||
|
||||
it("allows overriding native context menus", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.allowOverridingNativeContextMenus()).toBe(true);
|
||||
});
|
||||
|
||||
it("indicates support for desktop capturer", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.supportsDesktopCapturer()).toBe(true);
|
||||
});
|
||||
|
||||
it("indicates no support for jitsi screensharing", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.supportsJitsiScreensharing()).toBe(false);
|
||||
});
|
||||
|
||||
describe("notifications", () => {
|
||||
it("indicates support for notifications", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.supportsNotifications()).toBe(true);
|
||||
});
|
||||
|
||||
it("may send notifications", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.maySendNotifications()).toBe(true);
|
||||
});
|
||||
|
||||
it("pretends to request notification permission", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
const result = await platform.requestNotificationPermission();
|
||||
expect(result).toEqual("granted");
|
||||
});
|
||||
|
||||
it("creates a loud notification", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
platform.loudNotification(new MatrixEvent(), new Room("!room:server", {} as any, userId));
|
||||
expect(mockElectron.send).toHaveBeenCalledWith("loudNotification");
|
||||
});
|
||||
|
||||
it("sets notification count when count is changing", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
platform.setNotificationCount(0);
|
||||
// not called because matches internal notificaiton count
|
||||
expect(mockElectron.send).not.toHaveBeenCalledWith("setBadgeCount", 0);
|
||||
platform.setNotificationCount(1);
|
||||
expect(mockElectron.send).toHaveBeenCalledWith("setBadgeCount", 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("spellcheck", () => {
|
||||
it("indicates support for spellcheck settings", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.supportsSpellCheckSettings()).toBe(true);
|
||||
});
|
||||
|
||||
it("gets available spellcheck languages", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
mockElectron.send.mockClear();
|
||||
platform.getAvailableSpellCheckLanguages();
|
||||
|
||||
const [channel, { name }] = mockElectron.send.mock.calls[0];
|
||||
expect(channel).toEqual("ipcCall");
|
||||
expect(name).toEqual("getAvailableSpellCheckLanguages");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pickle key", () => {
|
||||
it("makes correct ipc call to get pickle key", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
mockElectron.send.mockClear();
|
||||
platform.getPickleKey(userId, deviceId);
|
||||
|
||||
const [, { name, args }] = mockElectron.send.mock.calls[0];
|
||||
expect(name).toEqual("getPickleKey");
|
||||
expect(args).toEqual([userId, deviceId]);
|
||||
});
|
||||
|
||||
it("makes correct ipc call to create pickle key", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
mockElectron.send.mockClear();
|
||||
platform.createPickleKey(userId, deviceId);
|
||||
|
||||
const [, { name, args }] = mockElectron.send.mock.calls[0];
|
||||
expect(name).toEqual("createPickleKey");
|
||||
expect(args).toEqual([userId, deviceId]);
|
||||
});
|
||||
|
||||
it("makes correct ipc call to destroy pickle key", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
mockElectron.send.mockClear();
|
||||
platform.destroyPickleKey(userId, deviceId);
|
||||
|
||||
const [, { name, args }] = mockElectron.send.mock.calls[0];
|
||||
expect(name).toEqual("destroyPickleKey");
|
||||
expect(args).toEqual([userId, deviceId]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("versions", () => {
|
||||
it("calls install update", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
platform.installUpdate();
|
||||
|
||||
expect(mockElectron.send).toHaveBeenCalledWith("install_update");
|
||||
});
|
||||
});
|
||||
|
||||
describe("breadcrumbs", () => {
|
||||
it("should send breadcrumb updates over the IPC", () => {
|
||||
const spy = jest.spyOn(BreadcrumbsStore.instance, "on");
|
||||
new ElectronPlatform();
|
||||
const cb = spy.mock.calls[0][1];
|
||||
cb();
|
||||
|
||||
expect(mockElectron.send).toHaveBeenCalledWith(
|
||||
"ipcCall",
|
||||
expect.objectContaining({
|
||||
name: "breadcrumbs",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authenticated media", () => {
|
||||
it("should respond to relevant ipc requests", async () => {
|
||||
const cli = stubClient();
|
||||
mocked(cli.getAccessToken).mockReturnValue("access_token");
|
||||
mocked(cli.getHomeserverUrl).mockReturnValue("homeserver_url");
|
||||
mocked(cli.getVersions).mockResolvedValue({
|
||||
versions: ["v1.1"],
|
||||
unstable_features: {},
|
||||
});
|
||||
|
||||
new ElectronPlatform();
|
||||
|
||||
const userAccessTokenCall = mockElectron.on.mock.calls.find((call) => call[0] === "userAccessToken");
|
||||
userAccessTokenCall;
|
||||
const userAccessTokenResponse = mockElectron.send.mock.calls.find((call) => call[0] === "userAccessToken");
|
||||
expect(userAccessTokenResponse![1]).toBe("access_token");
|
||||
|
||||
const homeserverUrlCall = mockElectron.on.mock.calls.find((call) => call[0] === "homeserverUrl");
|
||||
homeserverUrlCall;
|
||||
const homeserverUrlResponse = mockElectron.send.mock.calls.find((call) => call[0] === "homeserverUrl");
|
||||
expect(homeserverUrlResponse![1]).toBe("homeserver_url");
|
||||
|
||||
const serverSupportedVersionsCall = mockElectron.on.mock.calls.find(
|
||||
(call) => call[0] === "serverSupportedVersions",
|
||||
);
|
||||
await (serverSupportedVersionsCall as unknown as Promise<unknown>);
|
||||
const serverSupportedVersionsResponse = mockElectron.send.mock.calls.find(
|
||||
(call) => call[0] === "serverSupportedVersions",
|
||||
);
|
||||
expect(serverSupportedVersionsResponse![1]).toEqual({ versions: ["v1.1"], unstable_features: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe("settings", () => {
|
||||
let platform: ElectronPlatform;
|
||||
beforeAll(async () => {
|
||||
window.electron = mockElectron;
|
||||
platform = new ElectronPlatform();
|
||||
await platform.getConfig(); // await init
|
||||
});
|
||||
|
||||
it("supportsSetting should return true for the platform", () => {
|
||||
expect(platform.supportsSetting()).toBe(true);
|
||||
});
|
||||
|
||||
it("supportsSetting should return true for available settings", () => {
|
||||
expect(platform.supportsSetting("setting2")).toBe(true);
|
||||
});
|
||||
|
||||
it("supportsSetting should return false for unavailable settings", () => {
|
||||
expect(platform.supportsSetting("setting1")).toBe(false);
|
||||
});
|
||||
|
||||
it("should read setting value over ipc", async () => {
|
||||
mockElectron.getSettingValue.mockResolvedValue("value");
|
||||
await expect(platform.getSettingValue("setting2")).resolves.toEqual("value");
|
||||
expect(mockElectron.getSettingValue).toHaveBeenCalledWith("setting2");
|
||||
});
|
||||
|
||||
it("should write setting value over ipc", async () => {
|
||||
await platform.setSettingValue("setting2", "newValue");
|
||||
expect(mockElectron.setSettingValue).toHaveBeenCalledWith("setting2", "newValue");
|
||||
});
|
||||
});
|
||||
|
||||
it("should forward call_state dispatcher events via ipc", async () => {
|
||||
new ElectronPlatform();
|
||||
|
||||
dispatcher.dispatch(
|
||||
{
|
||||
action: "call_state",
|
||||
state: "connected",
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
const ipcMessage = mockElectron.send.mock.calls.find((call) => call[0] === "app_onAction");
|
||||
expect(ipcMessage![1]).toEqual({
|
||||
action: "call_state",
|
||||
state: "connected",
|
||||
});
|
||||
});
|
||||
|
||||
describe("Notification overlay badges", () => {
|
||||
beforeEach(() => {
|
||||
initialiseValues.mockReturnValue({
|
||||
protocol: "io.element.desktop",
|
||||
sessionId: "session-id",
|
||||
config: { _config: true },
|
||||
supportsBadgeOverlay: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should send a badge with a notification count", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await platform.initialised;
|
||||
platform.setNotificationCount(1);
|
||||
// Badges are sent asynchronously
|
||||
await waitFor(() => {
|
||||
const ipcMessage = mockElectron.send.mock.lastCall;
|
||||
expect(ipcMessage?.[1]).toEqual(1);
|
||||
expect(ipcMessage?.[2].constructor.name).toEqual("ArrayBuffer");
|
||||
});
|
||||
});
|
||||
|
||||
it("should update badge and skip duplicates", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await platform.initialised;
|
||||
platform.setNotificationCount(1);
|
||||
platform.setNotificationCount(1); // Test that duplicates do not fire.
|
||||
platform.setNotificationCount(2);
|
||||
// Badges are sent asynchronously
|
||||
await waitFor(() => {
|
||||
const [ipcMessageA, ipcMessageB] = mockElectron.send.mock.calls.filter(
|
||||
(call) => call[0] === "setBadgeCount",
|
||||
);
|
||||
|
||||
expect(ipcMessageA?.[1]).toEqual(1);
|
||||
expect(ipcMessageA?.[2].constructor.name).toEqual("ArrayBuffer");
|
||||
|
||||
expect(ipcMessageB?.[1]).toEqual(2);
|
||||
expect(ipcMessageB?.[2].constructor.name).toEqual("ArrayBuffer");
|
||||
});
|
||||
});
|
||||
it("should remove badge when notification count zeros", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await platform.initialised;
|
||||
platform.setNotificationCount(1);
|
||||
platform.setNotificationCount(0); // Test that duplicates do not fire.
|
||||
// Badges are sent asynchronously
|
||||
await waitFor(() => {
|
||||
const [ipcMessageB, ipcMessageA] = mockElectron.send.mock.calls.filter(
|
||||
(call) => call[0] === "setBadgeCount",
|
||||
);
|
||||
|
||||
expect(ipcMessageA?.[1]).toEqual(1);
|
||||
expect(ipcMessageA?.[2].constructor.name).toEqual("ArrayBuffer");
|
||||
|
||||
expect(ipcMessageB?.[1]).toEqual(0);
|
||||
expect(ipcMessageB?.[2]).toBeNull();
|
||||
});
|
||||
});
|
||||
it("should show an error badge when the application errors", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await platform.initialised;
|
||||
platform.setErrorStatus(true);
|
||||
// Badges are sent asynchronously
|
||||
await waitFor(() => {
|
||||
const ipcMessage = mockElectron.send.mock.calls.find((call) => call[0] === "setBadgeCount");
|
||||
|
||||
expect(ipcMessage?.[1]).toEqual(0);
|
||||
expect(ipcMessage?.[2].constructor.name).toEqual("ArrayBuffer");
|
||||
expect(ipcMessage?.[3]).toEqual(true);
|
||||
});
|
||||
});
|
||||
it("should restore after error is resolved", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await platform.initialised;
|
||||
platform.setErrorStatus(true);
|
||||
platform.setErrorStatus(false);
|
||||
// Badges are sent asynchronously
|
||||
await waitFor(() => {
|
||||
const [ipcMessageB, ipcMessageA] = mockElectron.send.mock.calls.filter(
|
||||
(call) => call[0] === "setBadgeCount",
|
||||
);
|
||||
|
||||
expect(ipcMessageA?.[1]).toEqual(0);
|
||||
expect(ipcMessageA?.[2].constructor.name).toEqual("ArrayBuffer");
|
||||
expect(ipcMessageA?.[3]).toEqual(true);
|
||||
|
||||
expect(ipcMessageB?.[1]).toEqual(0);
|
||||
expect(ipcMessageB?.[2]).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import PWAPlatform from "../../../../src/vector/platform/PWAPlatform";
|
||||
import WebPlatform from "../../../../src/vector/platform/WebPlatform";
|
||||
|
||||
jest.mock("../../../../src/vector/platform/WebPlatform");
|
||||
|
||||
describe("PWAPlatform", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("setNotificationCount", () => {
|
||||
it("should call Navigator::setAppBadge", () => {
|
||||
navigator.setAppBadge = jest.fn().mockResolvedValue(undefined);
|
||||
const platform = new PWAPlatform();
|
||||
expect(navigator.setAppBadge).not.toHaveBeenCalled();
|
||||
platform.setNotificationCount(123);
|
||||
expect(navigator.setAppBadge).toHaveBeenCalledWith(123);
|
||||
});
|
||||
|
||||
it("should no-op if the badge count isn't changing", () => {
|
||||
navigator.setAppBadge = jest.fn().mockResolvedValue(undefined);
|
||||
const platform = new PWAPlatform();
|
||||
platform.setNotificationCount(123);
|
||||
expect(navigator.setAppBadge).toHaveBeenCalledTimes(1);
|
||||
platform.setNotificationCount(123);
|
||||
expect(navigator.setAppBadge).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should fall back to WebPlatform::setNotificationCount if no Navigator::setAppBadge", () => {
|
||||
// @ts-ignore
|
||||
navigator.setAppBadge = undefined;
|
||||
const platform = new PWAPlatform();
|
||||
const superMethod = mocked(WebPlatform.prototype.setNotificationCount);
|
||||
expect(superMethod).not.toHaveBeenCalled();
|
||||
platform.setNotificationCount(123);
|
||||
expect(superMethod).toHaveBeenCalledWith(123);
|
||||
});
|
||||
|
||||
it("should handle Navigator::setAppBadge rejecting gracefully", () => {
|
||||
navigator.setAppBadge = jest.fn().mockRejectedValue(new Error());
|
||||
const platform = new PWAPlatform();
|
||||
expect(() => platform.setNotificationCount(123)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,285 +0,0 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import fetchMock from "@fetch-mock/jest";
|
||||
|
||||
import { UpdateCheckStatus } from "../../../../src/BasePlatform";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import WebPlatform from "../../../../src/vector/platform/WebPlatform";
|
||||
import ToastStore from "../../../../src/stores/ToastStore.ts";
|
||||
import defaultDispatcher from "../../../../src/dispatcher/dispatcher.ts";
|
||||
import { emitPromise } from "../../../test-utils";
|
||||
import { Action } from "../../../../src/dispatcher/actions.ts";
|
||||
|
||||
describe("WebPlatform", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(global, "navigator", "get").mockReturnValue({
|
||||
...navigator,
|
||||
// @ts-expect-error - mocking readonly object
|
||||
serviceWorker: {
|
||||
register: jest.fn().mockResolvedValue({
|
||||
update: jest.fn(),
|
||||
}),
|
||||
addEventListener: jest.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns human readable name", () => {
|
||||
const platform = new WebPlatform();
|
||||
expect(platform.getHumanReadableName()).toEqual("Web Platform");
|
||||
});
|
||||
|
||||
describe("service worker", () => {
|
||||
it("registers successfully", () => {
|
||||
new WebPlatform();
|
||||
expect(navigator.serviceWorker.register).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles errors", async () => {
|
||||
jest.spyOn(global, "navigator", "get").mockReturnValue({
|
||||
serviceWorker: {
|
||||
// @ts-expect-error - mocking readonly object
|
||||
register: undefined,
|
||||
},
|
||||
});
|
||||
new WebPlatform();
|
||||
|
||||
defaultDispatcher.dispatch({ action: Action.ClientStarted });
|
||||
await emitPromise(ToastStore.sharedInstance(), "update");
|
||||
const toasts = ToastStore.sharedInstance().getToasts();
|
||||
expect(toasts).toHaveLength(1);
|
||||
expect(toasts[0].title).toEqual("Failed to load service worker");
|
||||
});
|
||||
});
|
||||
|
||||
it("should call reload on window location object", () => {
|
||||
Object.defineProperty(window, "location", { value: { reload: jest.fn() }, writable: true });
|
||||
|
||||
const platform = new WebPlatform();
|
||||
expect(window.location.reload).not.toHaveBeenCalled();
|
||||
platform.reload();
|
||||
expect(window.location.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call reload to install update", () => {
|
||||
Object.defineProperty(window, "location", { value: { reload: jest.fn() }, writable: true });
|
||||
|
||||
const platform = new WebPlatform();
|
||||
expect(window.location.reload).not.toHaveBeenCalled();
|
||||
platform.installUpdate();
|
||||
expect(window.location.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("getDefaultDeviceDisplayName", () => {
|
||||
it.each([
|
||||
[
|
||||
"https://develop.element.io/#/room/!foo:bar",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) " +
|
||||
"Chrome/105.0.0.0 Safari/537.36",
|
||||
"develop.element.io: Chrome on macOS",
|
||||
],
|
||||
])("%s & %s = %s", (url, userAgent, result) => {
|
||||
jest.spyOn(global, "navigator", "get").mockReturnValue({ userAgent } as Navigator);
|
||||
Object.defineProperty(window, "location", { value: { href: url }, writable: true });
|
||||
const platform = new WebPlatform();
|
||||
expect(platform.getDefaultDeviceDisplayName()).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe("notification support", () => {
|
||||
const mockNotification = {
|
||||
requestPermission: jest.fn(),
|
||||
permission: "notGranted",
|
||||
};
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
window.Notification = mockNotification;
|
||||
mockNotification.permission = "notGranted";
|
||||
});
|
||||
|
||||
it("supportsNotifications returns false when platform does not support notifications", () => {
|
||||
// @ts-ignore
|
||||
window.Notification = undefined;
|
||||
expect(new WebPlatform().supportsNotifications()).toBe(false);
|
||||
});
|
||||
|
||||
it("supportsNotifications returns true when platform supports notifications", () => {
|
||||
expect(new WebPlatform().supportsNotifications()).toBe(true);
|
||||
});
|
||||
|
||||
it("maySendNotifications returns true when notification permissions are not granted", () => {
|
||||
expect(new WebPlatform().maySendNotifications()).toBe(false);
|
||||
});
|
||||
|
||||
it("maySendNotifications returns true when notification permissions are granted", () => {
|
||||
mockNotification.permission = "granted";
|
||||
expect(new WebPlatform().maySendNotifications()).toBe(true);
|
||||
});
|
||||
|
||||
it("requests notification permissions and returns result", async () => {
|
||||
mockNotification.requestPermission.mockImplementation((callback) => callback("test"));
|
||||
|
||||
const platform = new WebPlatform();
|
||||
const result = await platform.requestNotificationPermission();
|
||||
expect(result).toEqual("test");
|
||||
});
|
||||
});
|
||||
|
||||
describe("app version", () => {
|
||||
const envVersion = process.env.VERSION;
|
||||
const prodVersion = "1.10.13";
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(MatrixClientPeg, "userRegisteredWithinLastHours").mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = envVersion;
|
||||
});
|
||||
|
||||
it("should return true from canSelfUpdate()", async () => {
|
||||
const platform = new WebPlatform();
|
||||
const result = await platform.canSelfUpdate();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("getAppVersion returns normalized app version", async () => {
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = prodVersion;
|
||||
const platform = new WebPlatform();
|
||||
|
||||
const version = await platform.getAppVersion();
|
||||
expect(version).toEqual(prodVersion);
|
||||
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = `v${prodVersion}`;
|
||||
const version2 = await platform.getAppVersion();
|
||||
// v prefix removed
|
||||
expect(version2).toEqual(prodVersion);
|
||||
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = `version not like semver`;
|
||||
const notSemverVersion = await platform.getAppVersion();
|
||||
expect(notSemverVersion).toEqual(`version not like semver`);
|
||||
});
|
||||
|
||||
describe("pollForUpdate()", () => {
|
||||
it("should return not available and call showNoUpdate when current version matches most recent version", async () => {
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = prodVersion;
|
||||
fetchMock.getOnce("end:/version", prodVersion);
|
||||
const platform = new WebPlatform();
|
||||
|
||||
const showUpdate = jest.fn();
|
||||
const showNoUpdate = jest.fn();
|
||||
const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
|
||||
|
||||
expect(result).toEqual({ status: UpdateCheckStatus.NotAvailable });
|
||||
expect(showUpdate).not.toHaveBeenCalled();
|
||||
expect(showNoUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should strip v prefix from versions before comparing", async () => {
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = prodVersion;
|
||||
fetchMock.getOnce("end:/version", `v${prodVersion}`);
|
||||
const platform = new WebPlatform();
|
||||
|
||||
const showUpdate = jest.fn();
|
||||
const showNoUpdate = jest.fn();
|
||||
const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
|
||||
|
||||
// versions only differ by v prefix, no update
|
||||
expect(result).toEqual({ status: UpdateCheckStatus.NotAvailable });
|
||||
expect(showUpdate).not.toHaveBeenCalled();
|
||||
expect(showNoUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(
|
||||
"should return ready and call showUpdate when current version " + "differs from most recent version",
|
||||
async () => {
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = "0.0.0"; // old version
|
||||
fetchMock.getOnce("end:/version", prodVersion);
|
||||
const platform = new WebPlatform();
|
||||
|
||||
const showUpdate = jest.fn();
|
||||
const showNoUpdate = jest.fn();
|
||||
const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
|
||||
|
||||
expect(result).toEqual({ status: UpdateCheckStatus.Ready });
|
||||
expect(showUpdate).toHaveBeenCalledWith("0.0.0", prodVersion);
|
||||
expect(showNoUpdate).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("should return ready without showing update when user registered in last 24", async () => {
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = "0.0.0"; // old version
|
||||
jest.spyOn(MatrixClientPeg, "userRegisteredWithinLastHours").mockReturnValue(true);
|
||||
fetchMock.getOnce("end:/version", prodVersion);
|
||||
const platform = new WebPlatform();
|
||||
|
||||
const showUpdate = jest.fn();
|
||||
const showNoUpdate = jest.fn();
|
||||
const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
|
||||
|
||||
expect(result).toEqual({ status: UpdateCheckStatus.Ready });
|
||||
expect(showUpdate).not.toHaveBeenCalled();
|
||||
expect(showNoUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return error when version check fails", async () => {
|
||||
fetchMock.getOnce("end:/version", { throws: "oups" });
|
||||
const platform = new WebPlatform();
|
||||
|
||||
const showUpdate = jest.fn();
|
||||
const showNoUpdate = jest.fn();
|
||||
const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
|
||||
|
||||
expect(result).toEqual({ status: UpdateCheckStatus.Error, detail: "Unknown Error" });
|
||||
expect(showUpdate).not.toHaveBeenCalled();
|
||||
expect(showNoUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should return config from config.json", async () => {
|
||||
window.location.hostname = "domain.com";
|
||||
fetchMock.get(/config\.json.*/, { brand: "test" });
|
||||
const platform = new WebPlatform();
|
||||
await expect(platform.getConfig()).resolves.toEqual(expect.objectContaining({ brand: "test" }));
|
||||
});
|
||||
|
||||
it("should re-render favicon when setting error status", () => {
|
||||
const platform = new WebPlatform();
|
||||
const spy = jest.spyOn(platform.favicon, "badge");
|
||||
platform.setErrorStatus(true);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), { bgColor: "#f00" });
|
||||
});
|
||||
|
||||
describe("getOidcCallbackUrl()", () => {
|
||||
it("should not include the 'updated' query param in the redirect URI", () => {
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
href: "https://element.example.com/?updated=1.12.12",
|
||||
origin: "https://element.example.com",
|
||||
pathname: "/",
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
const platform = new WebPlatform();
|
||||
const url = platform.getOidcCallbackUrl();
|
||||
|
||||
expect(url.searchParams.has("updated")).toBe(false);
|
||||
expect(url.searchParams.get("no_universal_links")).toEqual("true");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import fetchMock from "@fetch-mock/jest";
|
||||
|
||||
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from "../../../src/widgets/ManagedHybrid";
|
||||
import { stubClient } from "../../test-utils";
|
||||
import SdkConfig from "../../../src/SdkConfig";
|
||||
import WidgetUtils from "../../../src/utils/WidgetUtils";
|
||||
import { WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
|
||||
|
||||
jest.mock("../../../src/utils/room/getJoinedNonFunctionalMembers", () => ({
|
||||
getJoinedNonFunctionalMembers: jest.fn().mockReturnValue([1, 2]),
|
||||
}));
|
||||
|
||||
describe("isManagedHybridWidgetEnabled", () => {
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
const client = stubClient();
|
||||
room = new Room("!room:server", client, client.getSafeUserId());
|
||||
});
|
||||
|
||||
it("should return false if widget_build_url is unset", () => {
|
||||
expect(isManagedHybridWidgetEnabled(room)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return true for 1-1 rooms when widget_build_url_ignore_dm is unset", () => {
|
||||
SdkConfig.put({
|
||||
widget_build_url: "https://url",
|
||||
});
|
||||
expect(isManagedHybridWidgetEnabled(room)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false for 1-1 rooms when widget_build_url_ignore_dm is true", () => {
|
||||
SdkConfig.put({
|
||||
widget_build_url: "https://url",
|
||||
widget_build_url_ignore_dm: true,
|
||||
});
|
||||
expect(isManagedHybridWidgetEnabled(room)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("addManagedHybridWidget", () => {
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
const client = stubClient();
|
||||
room = new Room("!room:server", client, client.getSafeUserId());
|
||||
});
|
||||
|
||||
it("should noop if user lacks permission", async () => {
|
||||
const logSpy = jest.spyOn(logger, "error").mockImplementation();
|
||||
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(false);
|
||||
|
||||
fetchMock.mockClear();
|
||||
await addManagedHybridWidget(room);
|
||||
expect(logSpy).toHaveBeenCalledWith("User not allowed to modify widgets in !room:server");
|
||||
expect(fetchMock).toHaveFetchedTimes(0);
|
||||
});
|
||||
|
||||
it("should noop if no widget_build_url", async () => {
|
||||
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
|
||||
|
||||
fetchMock.mockClear();
|
||||
await addManagedHybridWidget(room);
|
||||
expect(fetchMock).toHaveFetchedTimes(0);
|
||||
});
|
||||
|
||||
it("should add the widget successfully", async () => {
|
||||
fetchMock.get("https://widget-build-url/?roomId=!room:server", {
|
||||
widget_id: "WIDGET_ID",
|
||||
widget: { key: "value" },
|
||||
});
|
||||
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
|
||||
jest.spyOn(WidgetLayoutStore.instance, "canCopyLayoutToRoom").mockReturnValue(true);
|
||||
const setRoomWidgetContentSpy = jest.spyOn(WidgetUtils, "setRoomWidgetContent").mockResolvedValue();
|
||||
SdkConfig.put({
|
||||
widget_build_url: "https://widget-build-url",
|
||||
});
|
||||
|
||||
await addManagedHybridWidget(room);
|
||||
expect(fetchMock).toHaveFetched("https://widget-build-url?roomId=!room:server");
|
||||
expect(setRoomWidgetContentSpy).toHaveBeenCalledWith(room.client, room.roomId, "WIDGET_ID", {
|
||||
"key": "value",
|
||||
"io.element.managed_hybrid": true,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user