322 lines
13 KiB
TypeScript
322 lines
13 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
|
||
|
Please see LICENSE files in the repository root for full details.
|
||
|
*/
|
||
|
|
||
|
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||
|
import { mocked } from "jest-mock";
|
||
|
|
||
|
import WidgetStore, { IApp } from "../../../src/stores/WidgetStore";
|
||
|
import { Container, WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
|
||
|
import { stubClient } from "../../test-utils";
|
||
|
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||
|
import SettingsStore from "../../../src/settings/SettingsStore";
|
||
|
|
||
|
// 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, Container.Right).length).toStrictEqual(mockApps.length);
|
||
|
});
|
||
|
|
||
|
it("add widget to top container", async () => {
|
||
|
store.recalculateRoom(mockRoom);
|
||
|
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([mockApps[0]]);
|
||
|
expect(store.getContainerHeight(mockRoom, Container.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, Container.Top)).toStrictEqual([mockApps[0], mockApps[1]]);
|
||
|
});
|
||
|
|
||
|
it("add three widgets to top container", async () => {
|
||
|
store.recalculateRoom(mockRoom);
|
||
|
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||
|
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||
|
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
||
|
expect(new Set(store.getContainerWidgets(mockRoom, Container.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], Container.Top);
|
||
|
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||
|
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
||
|
expect(store.canAddToContainer(mockRoom, Container.Top)).toEqual(false);
|
||
|
});
|
||
|
|
||
|
it("remove pins when maximising (other widget)", async () => {
|
||
|
store.recalculateRoom(mockRoom);
|
||
|
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||
|
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||
|
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
||
|
store.moveToContainer(mockRoom, mockApps[3], Container.Center);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]);
|
||
|
expect(new Set(store.getContainerWidgets(mockRoom, Container.Right))).toEqual(
|
||
|
new Set([mockApps[0], mockApps[1], mockApps[2]]),
|
||
|
);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([mockApps[3]]);
|
||
|
});
|
||
|
|
||
|
it("remove pins when maximising (one of the pinned widgets)", async () => {
|
||
|
store.recalculateRoom(mockRoom);
|
||
|
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||
|
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||
|
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
||
|
store.moveToContainer(mockRoom, mockApps[0], Container.Center);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([mockApps[0]]);
|
||
|
expect(new Set(store.getContainerWidgets(mockRoom, Container.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], Container.Center);
|
||
|
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([mockApps[1]]);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
||
|
expect(new Set(store.getContainerWidgets(mockRoom, Container.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], Container.Center);
|
||
|
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([mockApps[0]]);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
||
|
expect(new Set(store.getContainerWidgets(mockRoom, Container.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, Container.Top)).toEqual([]);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.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, Container.Top)).toEqual([]);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Right)).toEqual([]);
|
||
|
});
|
||
|
|
||
|
it("should clear the layout if the client is not viable", () => {
|
||
|
store.recalculateRoom(mockRoom);
|
||
|
defaultDispatcher.dispatch(
|
||
|
{
|
||
|
action: "on_client_not_viable",
|
||
|
},
|
||
|
true,
|
||
|
);
|
||
|
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Right)).toEqual([]);
|
||
|
});
|
||
|
|
||
|
it("should return the expected resizer distributions", () => {
|
||
|
// this only works for top widgets
|
||
|
store.recalculateRoom(mockRoom);
|
||
|
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||
|
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||
|
expect(store.getResizerDistributions(mockRoom, Container.Top)).toEqual(["50.0%"]);
|
||
|
});
|
||
|
|
||
|
it("should set and return container height", () => {
|
||
|
store.recalculateRoom(mockRoom);
|
||
|
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||
|
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||
|
store.setContainerHeight(mockRoom, Container.Top, 23);
|
||
|
expect(store.getContainerHeight(mockRoom, Container.Top)).toBe(23);
|
||
|
});
|
||
|
|
||
|
it("should move a widget within a container", () => {
|
||
|
store.recalculateRoom(mockRoom);
|
||
|
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||
|
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||
|
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([
|
||
|
mockApps[0],
|
||
|
mockApps[1],
|
||
|
mockApps[2],
|
||
|
]);
|
||
|
store.moveWithinContainer(mockRoom, Container.Top, mockApps[0], 1);
|
||
|
expect(store.getContainerWidgets(mockRoom, Container.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], Container.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);
|
||
|
});
|
||
|
});
|
||
|
});
|