feat: show call participants in room list (Discord-style)
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
This commit is contained in:
@@ -0,0 +1,531 @@
|
||||
/*
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user