Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
"matrix_rtc_transport_missing": "The server is not configured to work with {{brand}}. Please contact your server admin (Domain: {{domain}}, Error Code: {{ errorCode }}).",
"membership_manager": "Membership Manager Error",
"membership_manager_description": "The Membership Manager had to shut down. This is caused by many consequtive failed network requests.",
"no_matrix_2_authorization_service": "Your authorization service for you media server (SFU) is not on the newest version",
"no_matrix_2_authorization_service": "The authorization service for your media server (SFU) is out of date.",
"open_elsewhere": "Opened in another tab",
"open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page.",
"room_creation_restricted": "Failed to create call",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@
"livekit-client": "^2.13.0",
"lodash-es": "^4.17.21",
"loglevel": "^1.9.1",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#4a75d2c92f1ac7476a6d398057b91c65054f1b80",
"matrix-widget-api": "^1.14.0",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.16.1",
"node-stdlib-browser": "^1.3.1",
"normalize.css": "^8.0.1",
"observable-hooks": "^4.2.3",
Expand Down
4 changes: 2 additions & 2 deletions sdk/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import {
tryMakeSticky,
widget,
} from "./helper";
import { ElementWidgetActions } from "../src/widget";
import { ElementWidgetActions, initializeWidget } from "../src/widget";
import { type Connection } from "../src/state/CallViewModel/remoteMembers/Connection";

interface MatrixRTCSdk {
Expand Down Expand Up @@ -88,7 +88,7 @@ export async function createMatrixRTCSdk(
application: string = "m.call",
id: string = "",
): Promise<MatrixRTCSdk> {
logger.info("Hello");
initializeWidget();
const client = await widget.client;
logger.info("client created");
const scope = new ObservableScope();
Expand Down
3 changes: 2 additions & 1 deletion src/button/ReactionToggleButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
import { alice, local, localRtcMember } from "../utils/test-fixtures";
import { type MockRTCSession } from "../utils/test";
import { ReactionsSenderProvider } from "../reactions/useReactionsSender";

import { initializeWidget } from "../widget";
initializeWidget();
vi.mock("livekit-client/e2ee-worker?worker");

const localIdent = `${localRtcMember.userId}:${localRtcMember.deviceId}`;
Expand Down
3 changes: 3 additions & 0 deletions src/initializer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { getUrlParams } from "./UrlParams";
import { Config } from "./config/Config";
import { platform } from "./Platform";
import { isFailure } from "./utils/fetch";
import { initializeWidget } from "./widget";

// This generates a map of locale names to their URL (based on import.meta.url), which looks like this:
// {
Expand Down Expand Up @@ -115,6 +116,8 @@ export class Initializer {
}

public static async initBeforeReact(): Promise<void> {
initializeWidget();

const polyfills: Promise<unknown>[] = [];
if (shouldPolyfillSegmenter()) {
polyfills.push(import("@formatjs/intl-segmenter/polyfill-force"));
Expand Down
3 changes: 2 additions & 1 deletion src/livekit/MatrixAudioRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import {
mockRemoteParticipant,
mockTrack,
} from "../utils/test";

import { initializeWidget } from "../widget";
initializeWidget();
export const TestAudioContextConstructor = vi.fn(() => testAudioContext);

const MediaDevicesProvider = MediaDevicesContext.MediaDevicesContext.Provider;
Expand Down
3 changes: 2 additions & 1 deletion src/room/CallEventAudioRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import {
localRtcMember,
} from "../utils/test-fixtures";
import { MAX_PARTICIPANT_COUNT_FOR_SOUND } from "../state/CallViewModel/CallViewModel";

import { initializeWidget } from "../widget";
initializeWidget();
vitest.mock("livekit-client/e2ee-worker?worker");
vitest.mock("../useAudioContext");
vitest.mock("../soundUtils");
Expand Down
3 changes: 2 additions & 1 deletion src/room/InCallView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer";
import { MediaDevicesContext } from "../MediaDevicesContext";
import { HeaderStyle } from "../UrlParams";

import { initializeWidget } from "../widget";
initializeWidget();
vi.hoisted(
() =>
(global.ImageData = class MockImageData {
Expand Down
2 changes: 2 additions & 0 deletions src/room/ReactionAudioRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
local,
localRtcMember,
} from "../utils/test-fixtures";
import { initializeWidget } from "../widget";
initializeWidget();

function TestComponent({ vm }: { vm: CallViewModel }): ReactNode {
return (
Expand Down
2 changes: 2 additions & 0 deletions src/room/ReactionsOverlay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
bobRtcMember,
} from "../utils/test-fixtures";
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
import { initializeWidget } from "../widget";
initializeWidget();

vi.mock("livekit-client/e2ee-worker?worker");

Expand Down
3 changes: 3 additions & 0 deletions src/state/CallViewModel/CallViewModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ import { getValue } from "../../utils/observable.ts";
import { type Behavior, constant } from "../Behavior.ts";
import { withCallViewModel as withCallViewModelInMode } from "./CallViewModelTestUtils.ts";
import { MatrixRTCMode } from "../../settings/settings.ts";
import { initializeWidget } from "../../widget.ts";

initializeWidget();

vi.mock("rxjs", async (importOriginal) => ({
...(await importOriginal()),
Expand Down
3 changes: 3 additions & 0 deletions src/state/CallViewModel/localMember/LocalMember.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ import { ConnectionManagerData } from "../remoteMembers/ConnectionManager";
import { ConnectionState, type Connection } from "../remoteMembers/Connection";
import { type Publisher } from "./Publisher";
import { type LocalTransportWithSFUConfig } from "./LocalTransport";
import { initializeWidget } from "../../../widget";

initializeWidget();

const MATRIX_RTC_MODE = MatrixRTCMode.Legacy;
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
Expand Down
2 changes: 2 additions & 0 deletions src/state/MuteStates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
import { constant } from "./Behavior";
import { ObservableScope } from "./ObservableScope";
import { flushPromises, mockMediaDevices } from "../utils/test";
import { initializeWidget } from "../widget";
initializeWidget();

const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
vi.mock("../UrlParams", () => ({ getUrlParams }));
Expand Down
127 changes: 127 additions & 0 deletions src/widget.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
Copyright 2026 New Vector Ltd.

SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/

import { beforeAll, describe, expect, vi, it } from "vitest";
import { createRoomWidgetClient, EventType } from "matrix-js-sdk";

import { getUrlParams } from "./UrlParams";
import { initializeWidget, widget } from "./widget";
import { Config } from "./config/Config";
import { ElementCallReactionEventType } from "./reactions";

vi.mock("matrix-js-sdk", { spy: true });
const createRoomWidgetClientSpy = vi.mocked(createRoomWidgetClient);

vi.mock("./config/Config", () => ({
Config: {
init: vi.fn().mockImplementation(async () => Promise.resolve()),
},
}));
const configInitSpy = vi.mocked(Config.init);

vi.mock("./UrlParams", () => ({
getUrlParams: vi.fn(() => ({
widgetId: "id",
parentUrl: "http://parentUrl",
roomId: "room",
userId: "myYser",
deviceId: "AAAAA",
baseUrl: "http://baseUrl",
e2eEnabled: true,
})),
}));

initializeWidget();
describe("widget", () => {
beforeAll(() => {});

it("should create an embedded client with the correct params", () => {
expect(getUrlParams()).toStrictEqual({
widgetId: "id",
parentUrl: "http://parentUrl",
roomId: "room",
userId: "myYser",
deviceId: "AAAAA",
baseUrl: "http://baseUrl",
e2eEnabled: true,
});
expect(widget).toBeDefined();
expect(configInitSpy).toHaveBeenCalled();
const sendEvent = [
EventType.CallNotify, // Sent as a deprecated fallback
EventType.RTCNotification,
];
const sendRecvEvent = [
"org.matrix.rageshake_request",
EventType.CallEncryptionKeysPrefix,
EventType.Reaction,
EventType.RoomRedaction,
ElementCallReactionEventType,
EventType.RTCDecline,
EventType.RTCMembership,
];

const sendState = [
Comment on lines +54 to +68
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BillCarsonFr For the record this is what I was referring to with the test being a close copy of the code: The obvious way to write it has all the same lists from widget.ts duplicated here, with an expect at the end rather than a function call.

My take is that writing tests like this is busywork, since they don't tell the reviewer anything that they wouldn't have already known from reading the source file. In general my attitude toward tests - maybe an unpopular opinion :) - is that properties of code that are trivial or self-evident do not need testing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also can get behind that. But there is some hardcoded values in there that we also check.
I was considering just writing down the full list as raw strings. But for the sake of time made it like that.

But what makes this a bit of a maintenance. burden also has the advantage, that if I mess around with the widget file, the test will force me to rethink if that is what I actually want to do.

"myYser", // Legacy call membership events
`_myYser_AAAAA_m.call`, // Session membership events
`myYser_AAAAA_m.call`, // The above with no leading underscore, for room versions whose auth rules allow it
].map((stateKey) => ({
eventType: EventType.GroupCallMemberPrefix,
stateKey,
}));
const receiveState = [
{ eventType: EventType.RoomCreate },
{ eventType: EventType.RoomName },
{ eventType: EventType.RoomMember },
{ eventType: EventType.RoomEncryption },
{ eventType: EventType.GroupCallMemberPrefix },
];

const sendRecvToDevice = [
EventType.CallInvite,
EventType.CallCandidates,
EventType.CallAnswer,
EventType.CallHangup,
EventType.CallReject,
EventType.CallSelectAnswer,
EventType.CallNegotiate,
EventType.CallSDPStreamMetadataChanged,
EventType.CallSDPStreamMetadataChangedPrefix,
EventType.CallReplaces,
EventType.CallEncryptionKeysPrefix,
];

expect(createRoomWidgetClientSpy.mock.calls[0][1]).toStrictEqual({
sendEvent: [...sendEvent, ...sendRecvEvent],
receiveEvent: sendRecvEvent,
sendState,
receiveState,
sendToDevice: sendRecvToDevice,
receiveToDevice: sendRecvToDevice,
turnServers: false,
sendDelayedEvents: true,
updateDelayedEvents: true,
sendSticky: true,
receiveSticky: true,
});

expect(createRoomWidgetClientSpy.mock.calls[0][2]).toStrictEqual("room");
expect(createRoomWidgetClientSpy.mock.calls[0][3]).toStrictEqual({
baseUrl: "http://baseUrl",
userId: "myYser",
deviceId: "AAAAA",
timelineSupport: true,
useE2eForGroupCall: true,
fallbackICEServerAllowed: undefined,
store: expect.any(Object),
cryptoStore: expect.any(Object),
idBaseUrl: undefined,
scheduler: expect.any(Object),
});
expect(createRoomWidgetClientSpy.mock.calls[0][4]).toStrictEqual(false);
});
});
42 changes: 31 additions & 11 deletions src/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ Please see LICENSE in the repository root for full details.
*/

import { logger } from "matrix-js-sdk/lib/logger";
import { EventType, createRoomWidgetClient } from "matrix-js-sdk";
import {
EventType,
createRoomWidgetClient,
type MatrixClient,
} from "matrix-js-sdk";
import {
WidgetApi,
MatrixCapabilities,
WidgetApiToWidgetAction,
} from "matrix-widget-api";

import type { MatrixClient } from "matrix-js-sdk";
import type { IWidgetApiRequest } from "matrix-widget-api";
import { LazyEventEmitter } from "./LazyEventEmitter";
import { getUrlParams } from "./UrlParams";
Expand Down Expand Up @@ -55,15 +58,29 @@ export interface WidgetHelpers {

/**
* A point of access to the widget API, if the app is running as a widget. This
* is declared and initialized on the top level because the widget messaging
* is initialized with `initializeWidget`. This should happen at the top level because the widget messaging
* needs to be set up ASAP on load to ensure it doesn't miss any requests.
*/
export const widget = ((): WidgetHelpers | null => {
export let widget: WidgetHelpers | null;

/**
* Should be called as soon as possible on app start. (In the initilizer before react)
*/
// this needs to be a seperate call and cannot be done on import to allow us to spy on methods in here before
// execution.
export const initializeWidget = (): void => {
try {
const { widgetId, parentUrl } = getUrlParams();
const {
widgetId,
parentUrl,
roomId,
userId,
deviceId,
baseUrl,
e2eEnabled,
allowIceFallback,
} = getUrlParams();

const { roomId, userId, deviceId, baseUrl, e2eEnabled, allowIceFallback } =
getUrlParams();
if (!roomId) throw new Error("Room ID must be supplied");
if (!userId) throw new Error("User ID must be supplied");
if (!deviceId) throw new Error("Device ID must be supplied");
Expand Down Expand Up @@ -106,6 +123,7 @@ export const widget = ((): WidgetHelpers | null => {
EventType.RoomRedaction,
ElementCallReactionEventType,
EventType.RTCDecline,
EventType.RTCMembership,
];

const sendState = [
Expand Down Expand Up @@ -150,6 +168,8 @@ export const widget = ((): WidgetHelpers | null => {
turnServers: false,
sendDelayedEvents: true,
updateDelayedEvents: true,
sendSticky: true,
receiveSticky: true,
},
roomId,
{
Expand All @@ -172,14 +192,14 @@ export const widget = ((): WidgetHelpers | null => {
return client;
};

return { api, lazyActions, client: clientPromise() };
widget = { api, lazyActions, client: clientPromise() };
} else {
if (import.meta.env.MODE !== "test")
logger.info("No widget API available");
return null;
widget = null;
}
} catch (e) {
logger.warn("Continuing without the widget API", e);
return null;
widget = null;
}
})();
};
Loading