mirror of
https://github.com/vector-im/element-web.git
synced 2026-05-06 04:36:21 +02:00
Merge pull request #31132 from element-hq/midhun/module-impl/builtin
Module API Implementation: Builtins
This commit is contained in:
commit
8c772f3a77
54
src/modules/AccountDataApi.ts
Normal file
54
src/modules/AccountDataApi.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2025 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 { Watchable, type AccountDataApi as IAccountDataApi } from "@element-hq/element-web-module-api";
|
||||
import { ClientEvent, type MatrixEvent, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
|
||||
export class AccountDataApi implements IAccountDataApi {
|
||||
public get(eventType: string): Watchable<unknown> {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
return new AccountDataWatchable(cli, eventType);
|
||||
}
|
||||
|
||||
public async set(eventType: string, content: any): Promise<void> {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
//@ts-expect-error: JS-SDK accepts known event-types, intentionally allow arbitrary types.
|
||||
await cli.setAccountData(eventType, content);
|
||||
}
|
||||
|
||||
public async delete(eventType: string): Promise<void> {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
//@ts-expect-error: JS-SDK accepts known event-types, intentionally allow arbitrary types.
|
||||
await cli.deleteAccountData(eventType);
|
||||
}
|
||||
}
|
||||
|
||||
class AccountDataWatchable extends Watchable<unknown> {
|
||||
public constructor(
|
||||
private cli: MatrixClient,
|
||||
private eventType: string,
|
||||
) {
|
||||
//@ts-expect-error: JS-SDK accepts known event-types, intentionally allow arbitrary types.
|
||||
super(cli.getAccountData(eventType)?.getContent());
|
||||
}
|
||||
|
||||
private onAccountData = (event: MatrixEvent): void => {
|
||||
if (event.getType() === this.eventType) {
|
||||
this.value = event.getContent();
|
||||
}
|
||||
};
|
||||
|
||||
protected onFirstWatch(): void {
|
||||
this.cli.on(ClientEvent.AccountData, this.onAccountData);
|
||||
}
|
||||
|
||||
protected onLastWatch(): void {
|
||||
this.cli.off(ClientEvent.AccountData, this.onAccountData);
|
||||
}
|
||||
}
|
||||
@ -27,7 +27,9 @@ import { NavigationApi } from "./Navigation.ts";
|
||||
import { openDialog } from "./Dialog.tsx";
|
||||
import { overwriteAccountAuth } from "./Auth.ts";
|
||||
import { ElementWebExtrasApi } from "./ExtrasApi.ts";
|
||||
import { ElementWebBuiltinsApi } from "./BuiltinsApi.ts";
|
||||
import { ElementWebBuiltinsApi } from "./BuiltinsApi.tsx";
|
||||
import { ClientApi } from "./ClientApi.ts";
|
||||
import { StoresApi } from "./StoresApi.ts";
|
||||
|
||||
const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) => {
|
||||
let used = false;
|
||||
@ -84,6 +86,8 @@ export class ModuleApi implements Api {
|
||||
public readonly extras = new ElementWebExtrasApi();
|
||||
public readonly builtins = new ElementWebBuiltinsApi();
|
||||
public readonly rootNode = document.getElementById("matrixchat")!;
|
||||
public readonly client = new ClientApi();
|
||||
public readonly stores = new StoresApi();
|
||||
|
||||
public createRoot(element: Element): Root {
|
||||
return createRoot(element);
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 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 { type RoomViewProps, type BuiltinsApi } from "@element-hq/element-web-module-api";
|
||||
|
||||
export class ElementWebBuiltinsApi implements BuiltinsApi {
|
||||
private _roomView?: React.ComponentType<RoomViewProps>;
|
||||
|
||||
/**
|
||||
* Sets the components used to render a RoomView
|
||||
*
|
||||
* This only really exists here because referencing RoomView directly causes a nightmare of
|
||||
* circular dependencies that break the whole app, so instead we avoid referencing it here
|
||||
* and pass it in from somewhere it's already referenced (see related comment in app.tsx).
|
||||
*
|
||||
* @param component The RoomView component
|
||||
*/
|
||||
public setRoomViewComponent(component: React.ComponentType<RoomViewProps>): void {
|
||||
this._roomView = component;
|
||||
}
|
||||
|
||||
public getRoomViewComponent(): React.ComponentType<RoomViewProps> {
|
||||
if (!this._roomView) {
|
||||
throw new Error("No RoomView component has been set");
|
||||
}
|
||||
|
||||
return this._roomView;
|
||||
}
|
||||
}
|
||||
75
src/modules/BuiltinsApi.tsx
Normal file
75
src/modules/BuiltinsApi.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2025 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 React from "react";
|
||||
import { type RoomViewProps, type BuiltinsApi } from "@element-hq/element-web-module-api";
|
||||
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
interface RoomViewPropsWithRoomId extends RoomViewProps {
|
||||
roomId?: string;
|
||||
}
|
||||
|
||||
interface RoomAvatarProps {
|
||||
room: Room;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
interface Components {
|
||||
roomView: React.ComponentType<RoomViewPropsWithRoomId>;
|
||||
roomAvatar: React.ComponentType<RoomAvatarProps>;
|
||||
}
|
||||
|
||||
export class ElementWebBuiltinsApi implements BuiltinsApi {
|
||||
private _roomView?: React.ComponentType<RoomViewPropsWithRoomId>;
|
||||
private _roomAvatar?: React.ComponentType<RoomAvatarProps>;
|
||||
|
||||
/**
|
||||
* Sets the components used by the API.
|
||||
*
|
||||
* This only really exists here because referencing these components directly causes a nightmare of
|
||||
* circular dependencies that break the whole app, so instead we avoid referencing it here
|
||||
* and pass it in from somewhere it's already referenced (see related comment in app.tsx).
|
||||
*
|
||||
* @param component The components used by the api, see {@link Components}
|
||||
*/
|
||||
public setComponents(components: Components): void {
|
||||
this._roomView = components.roomView;
|
||||
this._roomAvatar = components.roomAvatar;
|
||||
}
|
||||
|
||||
public getRoomViewComponent(): React.ComponentType<RoomViewPropsWithRoomId> {
|
||||
if (!this._roomView) {
|
||||
throw new Error("No RoomView component has been set");
|
||||
}
|
||||
|
||||
return this._roomView;
|
||||
}
|
||||
|
||||
public getRoomAvatarComponent(): React.ComponentType<RoomAvatarProps> {
|
||||
if (!this._roomAvatar) {
|
||||
throw new Error("No RoomAvatar component has been set");
|
||||
}
|
||||
|
||||
return this._roomAvatar;
|
||||
}
|
||||
|
||||
public renderRoomView(roomId: string): React.ReactNode {
|
||||
const Component = this.getRoomViewComponent();
|
||||
return <Component roomId={roomId} />;
|
||||
}
|
||||
|
||||
public renderRoomAvatar(roomId: string, size?: string): React.ReactNode {
|
||||
const room = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||
if (!room) {
|
||||
throw new Error(`No room such room: ${roomId}`);
|
||||
}
|
||||
const Component = this.getRoomAvatarComponent();
|
||||
return <Component room={room} size={size} />;
|
||||
}
|
||||
}
|
||||
20
src/modules/ClientApi.ts
Normal file
20
src/modules/ClientApi.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/*
|
||||
Copyright 2025 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 type { ClientApi as IClientApi, Room } from "@element-hq/element-web-module-api";
|
||||
import { Room as ModuleRoom } from "./models/Room";
|
||||
import { AccountDataApi } from "./AccountDataApi";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
|
||||
export class ClientApi implements IClientApi {
|
||||
public readonly accountData = new AccountDataApi();
|
||||
|
||||
public getRoom(roomId: string): Room | null {
|
||||
const sdkRoom = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||
if (sdkRoom) return new ModuleRoom(sdkRoom);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -5,8 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type LocationRenderFunction, type NavigationApi as INavigationApi } from "@element-hq/element-web-module-api";
|
||||
|
||||
import type {
|
||||
LocationRenderFunction,
|
||||
NavigationApi as INavigationApi,
|
||||
OpenRoomOptions,
|
||||
} from "@element-hq/element-web-module-api";
|
||||
import { navigateToPermalink } from "../utils/permalinks/navigator.ts";
|
||||
import { parsePermalink } from "../utils/permalinks/Permalinks.ts";
|
||||
import dispatcher from "../dispatcher/dispatcher.ts";
|
||||
@ -21,27 +24,25 @@ export class NavigationApi implements INavigationApi {
|
||||
|
||||
const parts = parsePermalink(link);
|
||||
if (parts?.roomIdOrAlias) {
|
||||
if (parts.roomIdOrAlias.startsWith("#")) {
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_alias: parts.roomIdOrAlias,
|
||||
via_servers: parts.viaServers ?? undefined,
|
||||
auto_join: join,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
} else {
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: parts.roomIdOrAlias,
|
||||
via_servers: parts.viaServers ?? undefined,
|
||||
auto_join: join,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
}
|
||||
this.openRoom(parts.roomIdOrAlias, {
|
||||
viaServers: parts.viaServers ?? undefined,
|
||||
autoJoin: join,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public registerLocationRenderer(path: string, renderer: LocationRenderFunction): void {
|
||||
this.locationRenderers.set(path, renderer);
|
||||
}
|
||||
|
||||
public openRoom(roomIdOrAlias: string, opts: OpenRoomOptions = {}): void {
|
||||
const key = roomIdOrAlias.startsWith("#") ? "room_alias" : "room_id";
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
[key]: roomIdOrAlias,
|
||||
via_servers: opts.viaServers,
|
||||
auto_join: opts.autoJoin,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
106
src/modules/StoresApi.ts
Normal file
106
src/modules/StoresApi.ts
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2025 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 {
|
||||
type StoresApi as IStoresApi,
|
||||
type RoomListStoreApi as IRoomListStore,
|
||||
type Room,
|
||||
Watchable,
|
||||
} from "@element-hq/element-web-module-api";
|
||||
|
||||
import type { RoomListStoreV3Class, RoomListStoreV3Event } from "../stores/room-list-v3/RoomListStoreV3";
|
||||
import { Room as ModuleRoom } from "./models/Room";
|
||||
|
||||
interface RlsEvents {
|
||||
LISTS_LOADED_EVENT: RoomListStoreV3Event.ListsLoaded;
|
||||
LISTS_UPDATE_EVENT: RoomListStoreV3Event.ListsUpdate;
|
||||
}
|
||||
|
||||
export class RoomListStoreApi implements IRoomListStore {
|
||||
private rls?: RoomListStoreV3Class;
|
||||
private LISTS_LOADED_EVENT?: RoomListStoreV3Event.ListsLoaded;
|
||||
private LISTS_UPDATE_EVENT?: RoomListStoreV3Event.ListsUpdate;
|
||||
public readonly moduleLoadPromise: Promise<void>;
|
||||
|
||||
public constructor() {
|
||||
this.moduleLoadPromise = this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the RLS through a dynamic import. This is necessary to prevent
|
||||
* circular dependency issues.
|
||||
*/
|
||||
private async init(): Promise<void> {
|
||||
const module = await import("../stores/room-list-v3/RoomListStoreV3");
|
||||
this.rls = module.default.instance;
|
||||
this.LISTS_LOADED_EVENT = module.LISTS_LOADED_EVENT;
|
||||
this.LISTS_UPDATE_EVENT = module.LISTS_UPDATE_EVENT;
|
||||
}
|
||||
|
||||
public getRooms(): RoomsWatchable {
|
||||
return new RoomsWatchable(this.roomListStore, this.events);
|
||||
}
|
||||
|
||||
private get events(): RlsEvents {
|
||||
if (!this.LISTS_LOADED_EVENT || !this.LISTS_UPDATE_EVENT) {
|
||||
throw new Error("Event type was not loaded correctly, did you forget to await waitForReady()?");
|
||||
}
|
||||
return { LISTS_LOADED_EVENT: this.LISTS_LOADED_EVENT, LISTS_UPDATE_EVENT: this.LISTS_UPDATE_EVENT };
|
||||
}
|
||||
|
||||
private get roomListStore(): RoomListStoreV3Class {
|
||||
if (!this.rls) {
|
||||
throw new Error("rls is undefined, did you forget to await waitForReady()?");
|
||||
}
|
||||
return this.rls;
|
||||
}
|
||||
|
||||
public async waitForReady(): Promise<void> {
|
||||
// Wait for the module to load first
|
||||
await this.moduleLoadPromise;
|
||||
|
||||
// Check if RLS is already loaded
|
||||
if (!this.roomListStore.isLoadingRooms) return;
|
||||
|
||||
// Await a promise that resolves when RLS has loaded
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
const { LISTS_LOADED_EVENT } = this.events;
|
||||
this.roomListStore.once(LISTS_LOADED_EVENT, resolve);
|
||||
await promise;
|
||||
}
|
||||
}
|
||||
|
||||
class RoomsWatchable extends Watchable<Room[]> {
|
||||
public constructor(
|
||||
private readonly rls: RoomListStoreV3Class,
|
||||
private readonly events: RlsEvents,
|
||||
) {
|
||||
super(rls.getSortedRooms().map((sdkRoom) => new ModuleRoom(sdkRoom)));
|
||||
}
|
||||
|
||||
private onRlsUpdate = (): void => {
|
||||
this.value = this.rls.getSortedRooms().map((sdkRoom) => new ModuleRoom(sdkRoom));
|
||||
};
|
||||
|
||||
protected onFirstWatch(): void {
|
||||
this.rls.on(this.events.LISTS_UPDATE_EVENT, this.onRlsUpdate);
|
||||
}
|
||||
|
||||
protected onLastWatch(): void {
|
||||
this.rls.off(this.events.LISTS_UPDATE_EVENT, this.onRlsUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
export class StoresApi implements IStoresApi {
|
||||
private roomListStoreApi?: IRoomListStore;
|
||||
|
||||
public get roomListStore(): IRoomListStore {
|
||||
if (!this.roomListStoreApi) {
|
||||
this.roomListStoreApi = new RoomListStoreApi();
|
||||
}
|
||||
return this.roomListStoreApi;
|
||||
}
|
||||
}
|
||||
45
src/modules/models/Room.ts
Normal file
45
src/modules/models/Room.ts
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright 2025 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 { type Room as IRoom, Watchable } from "@element-hq/element-web-module-api";
|
||||
import { RoomEvent, type Room as SdkRoom } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
export class Room implements IRoom {
|
||||
public name: Watchable<string>;
|
||||
|
||||
public constructor(private sdkRoom: SdkRoom) {
|
||||
this.name = new WatchableName(sdkRoom);
|
||||
}
|
||||
|
||||
public getLastActiveTimestamp(): number {
|
||||
return this.sdkRoom.getLastActiveTimestamp();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.sdkRoom.roomId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom watchable for room name.
|
||||
*/
|
||||
class WatchableName extends Watchable<string> {
|
||||
public constructor(private sdkRoom: SdkRoom) {
|
||||
super(sdkRoom.name);
|
||||
}
|
||||
|
||||
private onNameUpdate = (): void => {
|
||||
super.value = this.sdkRoom.name;
|
||||
};
|
||||
protected onFirstWatch(): void {
|
||||
this.sdkRoom.on(RoomEvent.Name, this.onNameUpdate);
|
||||
}
|
||||
|
||||
protected onLastWatch(): void {
|
||||
this.sdkRoom.off(RoomEvent.Name, this.onNameUpdate);
|
||||
}
|
||||
}
|
||||
@ -32,6 +32,7 @@ import { getInitialScreenAfterLogin, getScreenFromLocation, init as initRouting,
|
||||
import { UserFriendlyError } from "../languageHandler";
|
||||
import { ModuleApi } from "../modules/Api";
|
||||
import { RoomView } from "../components/structures/RoomView";
|
||||
import RoomAvatar from "../components/views/avatars/RoomAvatar";
|
||||
|
||||
logger.log(`Application is running in ${process.env.NODE_ENV} mode`);
|
||||
|
||||
@ -55,10 +56,9 @@ function onTokenLoginCompleted(): void {
|
||||
}
|
||||
|
||||
export async function loadApp(fragParams: QueryDict, matrixChatRef: React.Ref<MatrixChat>): Promise<ReactElement> {
|
||||
// XXX: This lives here because RoomVew import so many things that importing it in a sensible place (eg.
|
||||
// the builtins module or init.tsx) causes a circular dependency. Instead, we reference RoomView here where we
|
||||
// already reference it indirectly via MatrixChat.
|
||||
ModuleApi.instance.builtins.setRoomViewComponent(RoomView);
|
||||
// XXX: This lives here because certain components import so many things that importing it in a sensible place (eg.
|
||||
// the builtins module or init.tsx) causes a circular dependency.
|
||||
ModuleApi.instance.builtins.setComponents({ roomView: RoomView, roomAvatar: RoomAvatar });
|
||||
|
||||
initRouting();
|
||||
const platform = PlatformPeg.get();
|
||||
|
||||
@ -650,6 +650,7 @@ export function mkStubRoom(
|
||||
getJoinedMembers: jest.fn().mockReturnValue([]),
|
||||
getLiveTimeline: jest.fn().mockReturnValue(stubTimeline),
|
||||
getLastLiveEvent: jest.fn().mockReturnValue(undefined),
|
||||
getLastActiveTimestamp: jest.fn().mockReturnValue(1183140000),
|
||||
getMember: jest.fn().mockReturnValue({
|
||||
userId: "@member:domain.bla",
|
||||
name: "Member",
|
||||
|
||||
72
test/unit-tests/modules/AccountDataApi-test.ts
Normal file
72
test/unit-tests/modules/AccountDataApi-test.ts
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2025 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 } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { AccountDataApi } from "../../../src/modules/AccountDataApi";
|
||||
import { mkEvent, stubClient } from "../../test-utils/test-utils";
|
||||
|
||||
describe("AccountDataApi", () => {
|
||||
describe("AccountDataWatchable", () => {
|
||||
it("should return content of account data event on get()", () => {
|
||||
const cli = stubClient();
|
||||
const api = new AccountDataApi();
|
||||
// Mock cli to return a event
|
||||
const content = { foo: "bar" };
|
||||
const event = mkEvent({ content, type: "m.test", user: "@foobar:matrix.org", event: true });
|
||||
cli.getAccountData = () => event;
|
||||
expect(api.get("m.test").value).toStrictEqual(content);
|
||||
});
|
||||
|
||||
it("should update value on event", () => {
|
||||
const cli = stubClient();
|
||||
const api = new AccountDataApi();
|
||||
// Mock cli to return a event
|
||||
const content = { foo: "bar" };
|
||||
const event = mkEvent({ content, type: "m.test", user: "@foobar:matrix.org", event: true });
|
||||
cli.getAccountData = () => event;
|
||||
|
||||
const watchable = api.get("m.test");
|
||||
expect(watchable.value).toStrictEqual(content);
|
||||
|
||||
const fn = jest.fn();
|
||||
watchable.watch(fn);
|
||||
|
||||
// Let's say that the account data event changed
|
||||
const event2 = mkEvent({
|
||||
content: { foo: "abc" },
|
||||
type: "m.test",
|
||||
user: "@foobar:matrix.org",
|
||||
event: true,
|
||||
});
|
||||
cli.emit(ClientEvent.AccountData, event2);
|
||||
// Watchable value should have been updated
|
||||
expect(watchable.value).toStrictEqual({ foo: "abc" });
|
||||
// Watched callbacks should be called
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Make sure unwatch removed the event listener
|
||||
cli.off = jest.fn();
|
||||
watchable.unwatch(fn);
|
||||
expect(cli.off).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set account data via js-sdk on set()", async () => {
|
||||
const cli = stubClient();
|
||||
const api = new AccountDataApi();
|
||||
await api.set("m.test", { foo: "bar" });
|
||||
expect(cli.setAccountData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should delete account data via js-sdk on set()", async () => {
|
||||
const cli = stubClient();
|
||||
const api = new AccountDataApi();
|
||||
await api.delete("m.test");
|
||||
expect(cli.deleteAccountData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -1,17 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 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 { ElementWebBuiltinsApi } from "../../../src/modules/BuiltinsApi";
|
||||
|
||||
describe("ElementWebBuiltinsApi", () => {
|
||||
it("returns the RoomView component thats been set", () => {
|
||||
const builtinsApi = new ElementWebBuiltinsApi();
|
||||
const sentinel = {};
|
||||
builtinsApi.setRoomViewComponent(sentinel as any);
|
||||
expect(builtinsApi.getRoomViewComponent()).toBe(sentinel);
|
||||
});
|
||||
});
|
||||
53
test/unit-tests/modules/BuiltinsApi-test.tsx
Normal file
53
test/unit-tests/modules/BuiltinsApi-test.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright 2025 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 React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import { ElementWebBuiltinsApi } from "../../../src/modules/BuiltinsApi.tsx";
|
||||
import { stubClient } from "../../test-utils/test-utils";
|
||||
|
||||
const Avatar: React.FC<{ room: { roomId: string }; size: string }> = ({ room, size }) => {
|
||||
return (
|
||||
<div>
|
||||
Avatar, {room.roomId}, {size}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
describe("ElementWebBuiltinsApi", () => {
|
||||
it("returns the RoomView component thats been set", () => {
|
||||
const builtinsApi = new ElementWebBuiltinsApi();
|
||||
const sentinel = {};
|
||||
builtinsApi.setComponents({ roomView: sentinel, roomAvatar: Avatar } as any);
|
||||
expect(builtinsApi.getRoomViewComponent()).toBe(sentinel);
|
||||
});
|
||||
|
||||
it("returns rendered RoomView component", () => {
|
||||
const builtinsApi = new ElementWebBuiltinsApi();
|
||||
const RoomView = () => <div>hello world</div>;
|
||||
builtinsApi.setComponents({ roomView: RoomView, roomAvatar: Avatar } as any);
|
||||
const { container } = render(<> {builtinsApi.renderRoomView("!foo:m.org")}</>);
|
||||
expect(container).toHaveTextContent("hello world");
|
||||
});
|
||||
|
||||
it("returns rendered RoomAvatar component", () => {
|
||||
stubClient();
|
||||
const builtinsApi = new ElementWebBuiltinsApi();
|
||||
builtinsApi.setComponents({ roomView: {}, roomAvatar: Avatar } as any);
|
||||
const { container } = render(<> {builtinsApi.renderRoomAvatar("!foo:m.org", "50")}</>);
|
||||
expect(container).toHaveTextContent("Avatar");
|
||||
expect(container).toHaveTextContent("!foo:m.org");
|
||||
expect(container).toHaveTextContent("50");
|
||||
});
|
||||
|
||||
it("should throw error if called before components are set", () => {
|
||||
stubClient();
|
||||
const builtinsApi = new ElementWebBuiltinsApi();
|
||||
expect(() => builtinsApi.renderRoomAvatar("!foo:m.org")).toThrow("No RoomAvatar component has been set");
|
||||
expect(() => builtinsApi.renderRoomView("!foo:m.org")).toThrow("No RoomView component has been set");
|
||||
});
|
||||
});
|
||||
20
test/unit-tests/modules/ClientApi-test.ts
Normal file
20
test/unit-tests/modules/ClientApi-test.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/*
|
||||
Copyright 2025 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 { ClientApi } from "../../../src/modules/ClientApi";
|
||||
import { Room } from "../../../src/modules/models/Room";
|
||||
import { stubClient } from "../../test-utils/test-utils";
|
||||
|
||||
describe("ClientApi", () => {
|
||||
it("should return module room from getRoom()", () => {
|
||||
stubClient();
|
||||
const client = new ClientApi();
|
||||
const moduleRoom = client.getRoom("!foo:matrix.org");
|
||||
expect(moduleRoom).toBeInstanceOf(Room);
|
||||
expect(moduleRoom?.id).toStrictEqual("!foo:matrix.org");
|
||||
});
|
||||
});
|
||||
@ -37,5 +37,25 @@ describe("NavigationApi", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should dispatch correct action on openRoom", () => {
|
||||
const spy = jest.spyOn(defaultDispatcher, "dispatch");
|
||||
// Non alias
|
||||
api.openRoom("!foo:m.org");
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "view_room",
|
||||
room_id: "!foo:m.org",
|
||||
}),
|
||||
);
|
||||
// Alias
|
||||
api.openRoom("#bar:m.org");
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "view_room",
|
||||
room_alias: "#bar:m.org",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
84
test/unit-tests/modules/StoresApi-test.ts
Normal file
84
test/unit-tests/modules/StoresApi-test.ts
Normal file
@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright 2025 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 { waitFor } from "jest-matrix-react";
|
||||
|
||||
import { type RoomListStoreApi, StoresApi } from "../../../src/modules/StoresApi";
|
||||
import RoomListStoreV3, {
|
||||
LISTS_LOADED_EVENT,
|
||||
LISTS_UPDATE_EVENT,
|
||||
} from "../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import { mkRoom, stubClient } from "../../test-utils/test-utils";
|
||||
import { Room } from "../../../src/modules/models/Room";
|
||||
import {} from "../../../src/stores/room-list/algorithms/Algorithm";
|
||||
|
||||
describe("StoresApi", () => {
|
||||
describe("RoomListStoreApi", () => {
|
||||
it("should return promise that resolves when RLS is ready", async () => {
|
||||
jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(true);
|
||||
const store = new StoresApi();
|
||||
let hasResolved = false;
|
||||
// The following async function will set hasResolved to false
|
||||
// only when waitForReady resolves.
|
||||
(async () => {
|
||||
await store.roomListStore.waitForReady();
|
||||
hasResolved = true;
|
||||
})();
|
||||
// Shouldn't have resolved yet.
|
||||
expect(hasResolved).toStrictEqual(false);
|
||||
|
||||
// Wait for the module to load so that we can test the listener.
|
||||
await (store.roomListStore as RoomListStoreApi).moduleLoadPromise;
|
||||
// Emit the loaded event.
|
||||
RoomListStoreV3.instance.emit(LISTS_LOADED_EVENT);
|
||||
// Should resolve now.
|
||||
await waitFor(() => {
|
||||
expect(hasResolved).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRooms()", () => {
|
||||
it("should return rooms from RLS", async () => {
|
||||
const cli = stubClient();
|
||||
const room1 = mkRoom(cli, "!foo1:m.org");
|
||||
const room2 = mkRoom(cli, "!foo2:m.org");
|
||||
const room3 = mkRoom(cli, "!foo3:m.org");
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRooms").mockReturnValue([room1, room2, room3]);
|
||||
jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(false);
|
||||
|
||||
const store = new StoresApi();
|
||||
await store.roomListStore.waitForReady();
|
||||
const watchable = store.roomListStore.getRooms();
|
||||
expect(watchable.value).toHaveLength(3);
|
||||
expect(watchable.value[0]).toBeInstanceOf(Room);
|
||||
});
|
||||
|
||||
it("should update from RLS", async () => {
|
||||
const cli = stubClient();
|
||||
const room1 = mkRoom(cli, "!foo1:m.org");
|
||||
const room2 = mkRoom(cli, "!foo2:m.org");
|
||||
const rooms = [room1, room2];
|
||||
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRooms").mockReturnValue(rooms);
|
||||
jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(false);
|
||||
|
||||
const store = new StoresApi();
|
||||
await store.roomListStore.waitForReady();
|
||||
const watchable = store.roomListStore.getRooms();
|
||||
const fn = jest.fn();
|
||||
watchable.watch(fn);
|
||||
expect(watchable.value).toHaveLength(2);
|
||||
|
||||
const room3 = mkRoom(cli, "!foo3:m.org");
|
||||
rooms.push(room3);
|
||||
RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(watchable.value).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
50
test/unit-tests/modules/models/Room-test.ts
Normal file
50
test/unit-tests/modules/models/Room-test.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2025 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 { Room } from "../../../../src/modules/models/Room";
|
||||
import { mkRoom, stubClient } from "../../../test-utils";
|
||||
|
||||
describe("Room", () => {
|
||||
it("should return id from sdk room", () => {
|
||||
const cli = stubClient();
|
||||
const sdkRoom = mkRoom(cli, "!foo:m.org");
|
||||
const room = new Room(sdkRoom);
|
||||
expect(room.id).toStrictEqual("!foo:m.org");
|
||||
});
|
||||
|
||||
it("should return last timestamp from sdk room", () => {
|
||||
const cli = stubClient();
|
||||
const sdkRoom = mkRoom(cli, "!foo:m.org");
|
||||
const room = new Room(sdkRoom);
|
||||
expect(room.getLastActiveTimestamp()).toStrictEqual(sdkRoom.getLastActiveTimestamp());
|
||||
});
|
||||
|
||||
describe("watchableName", () => {
|
||||
it("should return name from sdkRoom", () => {
|
||||
const cli = stubClient();
|
||||
const sdkRoom = mkRoom(cli, "!foo:m.org");
|
||||
sdkRoom.name = "Foo Name";
|
||||
const room = new Room(sdkRoom);
|
||||
expect(room.name.value).toStrictEqual("Foo Name");
|
||||
});
|
||||
|
||||
it("should add/remove event listener on sdk room", () => {
|
||||
const cli = stubClient();
|
||||
const sdkRoom = mkRoom(cli, "!foo:m.org");
|
||||
sdkRoom.name = "Foo Name";
|
||||
|
||||
const room = new Room(sdkRoom);
|
||||
const fn = jest.fn();
|
||||
|
||||
room.name.watch(fn);
|
||||
expect(sdkRoom.on).toHaveBeenCalledTimes(1);
|
||||
|
||||
room.name.unwatch(fn);
|
||||
expect(sdkRoom.off).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user