element-web/apps/web/test/unit-tests/stores/WidgetLayoutStore-test.ts
David Baker 09bbf796dc
Add support for Widget & Room Header Buttons module APIs (#32734)
* Add support for Widget & Room Header Buttons module APIs

To support https://github.com/element-hq/element-modules/pull/217

* Update for new api

* Test addRoomHeaderButtonCallback

* Extra mock api

* Test for widgetapi

* Convert enum

* Convert other enum usage

* Add tests for widget context menu move buttons

Which have just changed because of the enum

* Add tests for moving the widgets

* Fix copyright

Co-authored-by: Florian Duros <florianduros@element.io>

* Update module API

* A little import/export

---------

Co-authored-by: Florian Duros <florianduros@element.io>
2026-03-13 13:44:18 +00:00

310 lines
12 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import WidgetStore, { type IApp } from "../../../src/stores/WidgetStore";
import { WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
import { stubClient } from "../../test-utils";
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../src/settings/SettingsStore";
import { Action } from "../../../src/dispatcher/actions.ts";
// setup test env values
const roomId = "!room:server";
describe("WidgetLayoutStore", () => {
let client: MatrixClient;
let store: WidgetLayoutStore;
let roomUpdateListener: (event: string) => void;
let mockApps: IApp[];
let mockRoom: Room;
let layoutEventContent: Record<string, any> | null;
beforeEach(() => {
layoutEventContent = null;
mockRoom = <Room>{
roomId: roomId,
currentState: {
getStateEvents: (_l, _x) => {
return {
getId: () => "$layoutEventId",
getContent: () => layoutEventContent,
};
},
},
};
mockApps = [
<IApp>{ roomId: roomId, id: "1" },
<IApp>{ roomId: roomId, id: "2" },
<IApp>{ roomId: roomId, id: "3" },
<IApp>{ roomId: roomId, id: "4" },
];
// fake the WidgetStore.instance to just return an object with `getApps`
jest.spyOn(WidgetStore, "instance", "get").mockReturnValue({
on: jest.fn(),
off: jest.fn(),
getApps: () => mockApps,
} as unknown as WidgetStore);
SettingsStore.reset();
});
beforeAll(() => {
// we need to init a client so it does not error, when asking for DeviceStorage handlers (SettingsStore.setValue("Widgets.layout"))
client = stubClient();
roomUpdateListener = jest.fn();
// @ts-ignore bypass private ctor for tests
store = new WidgetLayoutStore();
store.addListener(`update_${roomId}`, roomUpdateListener);
});
afterAll(() => {
store.removeListener(`update_${roomId}`, roomUpdateListener);
});
it("all widgets should be in the right container by default", () => {
store.recalculateRoom(mockRoom);
expect(store.getContainerWidgets(mockRoom, "right").length).toStrictEqual(mockApps.length);
});
it("add widget to top container", async () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], "top");
expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[0]]);
expect(store.getContainerHeight(mockRoom, "top")).toBeNull();
});
it("ordering of top container widgets should be consistent even if no index specified", async () => {
layoutEventContent = {
widgets: {
"1": {
container: "top",
},
"2": {
container: "top",
},
},
};
store.recalculateRoom(mockRoom);
expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[0], mockApps[1]]);
});
it("add three widgets to top container", async () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], "top");
store.moveToContainer(mockRoom, mockApps[1], "top");
store.moveToContainer(mockRoom, mockApps[2], "top");
expect(new Set(store.getContainerWidgets(mockRoom, "top"))).toEqual(
new Set([mockApps[0], mockApps[1], mockApps[2]]),
);
});
it("cannot add more than three widgets to top container", async () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], "top");
store.moveToContainer(mockRoom, mockApps[1], "top");
store.moveToContainer(mockRoom, mockApps[2], "top");
expect(store.canAddToContainer(mockRoom, "top")).toEqual(false);
});
it("remove pins when maximising (other widget)", async () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], "top");
store.moveToContainer(mockRoom, mockApps[1], "top");
store.moveToContainer(mockRoom, mockApps[2], "top");
store.moveToContainer(mockRoom, mockApps[3], "center");
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
new Set([mockApps[0], mockApps[1], mockApps[2]]),
);
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([mockApps[3]]);
});
it("remove pins when maximising (one of the pinned widgets)", async () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], "top");
store.moveToContainer(mockRoom, mockApps[1], "top");
store.moveToContainer(mockRoom, mockApps[2], "top");
store.moveToContainer(mockRoom, mockApps[0], "center");
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([mockApps[0]]);
expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
new Set([mockApps[1], mockApps[2], mockApps[3]]),
);
});
it("remove maximised when pinning (other widget)", async () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], "center");
store.moveToContainer(mockRoom, mockApps[1], "top");
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([mockApps[1]]);
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
new Set([mockApps[2], mockApps[3], mockApps[0]]),
);
});
it("remove maximised when pinning (same widget)", async () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], "center");
store.moveToContainer(mockRoom, mockApps[0], "top");
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([mockApps[0]]);
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
new Set([mockApps[2], mockApps[3], mockApps[1]]),
);
});
it("should recalculate all rooms when the client is ready", async () => {
mocked(client.getVisibleRooms).mockReturnValue([mockRoom]);
await store.start();
expect(roomUpdateListener).toHaveBeenCalled();
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
expect(store.getContainerWidgets(mockRoom, "right")).toEqual([
mockApps[0],
mockApps[1],
mockApps[2],
mockApps[3],
]);
});
it("should clear the layout and emit an update if there are no longer apps in the room", () => {
store.recalculateRoom(mockRoom);
mocked(roomUpdateListener).mockClear();
jest.spyOn(WidgetStore, "instance", "get").mockReturnValue(<WidgetStore>(
({ getApps: (): IApp[] => [] } as unknown as WidgetStore)
));
store.recalculateRoom(mockRoom);
expect(roomUpdateListener).toHaveBeenCalled();
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
expect(store.getContainerWidgets(mockRoom, "right")).toEqual([]);
});
it("should clear the layout if the client is not viable", () => {
store.recalculateRoom(mockRoom);
defaultDispatcher.dispatch({ action: Action.ClientNotViable }, true);
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
expect(store.getContainerWidgets(mockRoom, "right")).toEqual([]);
});
it("should return the expected resizer distributions", () => {
// this only works for top widgets
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], "top");
store.moveToContainer(mockRoom, mockApps[1], "top");
expect(store.getResizerDistributions(mockRoom, "top")).toEqual(["50.0%"]);
});
it("should set and return container height", () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], "top");
store.moveToContainer(mockRoom, mockApps[1], "top");
store.setContainerHeight(mockRoom, "top", 23);
expect(store.getContainerHeight(mockRoom, "top")).toBe(23);
});
it("should move a widget within a container", () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], "top");
store.moveToContainer(mockRoom, mockApps[1], "top");
store.moveToContainer(mockRoom, mockApps[2], "top");
expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[0], mockApps[1], mockApps[2]]);
store.moveWithinContainer(mockRoom, "top", mockApps[0], 1);
expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[1], mockApps[0], mockApps[2]]);
});
it("should copy the layout to the room", async () => {
await store.start();
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], "top");
store.copyLayoutToRoom(mockRoom);
expect(mocked(client.sendStateEvent).mock.calls).toMatchInlineSnapshot(`
[
[
"!room:server",
"io.element.widgets.layout",
{
"widgets": {
"1": {
"container": "top",
"height": undefined,
"index": 0,
"width": 100,
},
"2": {
"container": "right",
},
"3": {
"container": "right",
},
"4": {
"container": "right",
},
},
},
"",
],
]
`);
});
it("Can call onNotReady before onReady has been called", () => {
// Just to quieten SonarCloud :-(
// @ts-ignore bypass private ctor for tests
const store = new WidgetLayoutStore();
// @ts-ignore calling private method
store.onNotReady();
});
describe("when feature_dynamic_room_predecessors is not enabled", () => {
beforeAll(() => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
});
it("passes the flag in to getVisibleRooms", async () => {
mocked(client.getVisibleRooms).mockRestore();
mocked(client.getVisibleRooms).mockReturnValue([]);
// @ts-ignore bypass private ctor for tests
const store = new WidgetLayoutStore();
await store.start();
expect(client.getVisibleRooms).toHaveBeenCalledWith(false);
});
});
describe("when feature_dynamic_room_predecessors is enabled", () => {
beforeAll(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_dynamic_room_predecessors",
);
});
it("passes the flag in to getVisibleRooms", async () => {
mocked(client.getVisibleRooms).mockRestore();
mocked(client.getVisibleRooms).mockReturnValue([]);
// @ts-ignore bypass private ctor for tests
const store = new WidgetLayoutStore();
await store.start();
expect(client.getVisibleRooms).toHaveBeenCalledWith(true);
});
});
});