From 6d354e3e10b5c5abfe590e1459d8305c3b38cedf Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 18 Jan 2023 15:49:34 +0100 Subject: [PATCH] Add test coverage (#9928) --- src/stores/widgets/WidgetLayoutStore.ts | 4 +- .../RoomDeviceSettingsHandler-test.ts | 52 ++++-- test/stores/AutoRageshakeStore-test.ts | 100 ++++++++++ test/stores/WidgetLayoutStore-test.ts | 174 ++++++++++++++++-- 4 files changed, 301 insertions(+), 29 deletions(-) create mode 100644 test/stores/AutoRageshakeStore-test.ts diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index 352393c435..ef5f62de73 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -414,7 +414,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { widgets.forEach((w, i) => { localLayout[w.id] = { container: container, - width: widths[i], + width: widths?.[i], index: i, height: height, }; @@ -437,7 +437,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { widgets.forEach((w, i) => { localLayout[w.id] = { container: container, - width: widths[i], + width: widths?.[i], index: i, height: height, }; diff --git a/test/settings/handlers/RoomDeviceSettingsHandler-test.ts b/test/settings/handlers/RoomDeviceSettingsHandler-test.ts index 694cfd5d88..e451edf600 100644 --- a/test/settings/handlers/RoomDeviceSettingsHandler-test.ts +++ b/test/settings/handlers/RoomDeviceSettingsHandler-test.ts @@ -15,21 +15,49 @@ limitations under the License. */ import RoomDeviceSettingsHandler from "../../../src/settings/handlers/RoomDeviceSettingsHandler"; -import { WatchManager } from "../../../src/settings/WatchManager"; +import { SettingLevel } from "../../../src/settings/SettingLevel"; +import { CallbackFn, WatchManager } from "../../../src/settings/WatchManager"; describe("RoomDeviceSettingsHandler", () => { - it("should correctly read cached values", () => { - const watchers = new WatchManager(); - const handler = new RoomDeviceSettingsHandler(watchers); + const roomId = "!room:example.com"; + const value = "test value"; + const testSettings = [ + "RightPanel.phases", + // special case in RoomDeviceSettingsHandler + "blacklistUnverifiedDevices", + ]; + let watchers: WatchManager; + let handler: RoomDeviceSettingsHandler; + let settingListener: CallbackFn; - const settingName = "RightPanel.phases"; - const roomId = "!room:server"; - const value = { - isOpen: true, - history: [{}], - }; + beforeEach(() => { + watchers = new WatchManager(); + handler = new RoomDeviceSettingsHandler(watchers); + settingListener = jest.fn(); + }); - handler.setValue(settingName, roomId, value); - expect(handler.getValue(settingName, roomId)).toEqual(value); + afterEach(() => { + watchers.unwatchSetting(settingListener); + }); + + it.each(testSettings)("should write/read/clear the value for »%s«", (setting: string): void => { + // initial value should be null + watchers.watchSetting(setting, roomId, settingListener); + + expect(handler.getValue(setting, roomId)).toBeNull(); + + // set and read value + handler.setValue(setting, roomId, value); + expect(settingListener).toHaveBeenCalledWith(roomId, SettingLevel.ROOM_DEVICE, value); + expect(handler.getValue(setting, roomId)).toEqual(value); + + // clear value + handler.setValue(setting, roomId, null); + expect(settingListener).toHaveBeenCalledWith(roomId, SettingLevel.ROOM_DEVICE, null); + expect(handler.getValue(setting, roomId)).toBeNull(); + }); + + it("canSetValue should return true", () => { + expect(handler.canSetValue("test setting", roomId)).toBe(true); }); }); diff --git a/test/stores/AutoRageshakeStore-test.ts b/test/stores/AutoRageshakeStore-test.ts new file mode 100644 index 0000000000..ff57bda59c --- /dev/null +++ b/test/stores/AutoRageshakeStore-test.ts @@ -0,0 +1,100 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { mocked } from "jest-mock"; +import { ClientEvent, EventType, MatrixClient, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix"; +import { SyncState } from "matrix-js-sdk/src/sync"; + +import SettingsStore from "../../src/settings/SettingsStore"; +import AutoRageshakeStore from "../../src/stores/AutoRageshakeStore"; +import { mkEvent, stubClient } from "../test-utils"; + +jest.mock("../../src/rageshake/submit-rageshake"); + +describe("AutoRageshakeStore", () => { + const roomId = "!room:example.com"; + let client: MatrixClient; + let utdEvent: MatrixEvent; + let autoRageshakeStore: AutoRageshakeStore; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); + + client = stubClient(); + + // @ts-ignore bypass private ctor for tests + autoRageshakeStore = new AutoRageshakeStore(); + autoRageshakeStore.start(); + + utdEvent = mkEvent({ + event: true, + content: {}, + room: roomId, + user: client.getSafeUserId(), + type: EventType.RoomMessage, + }); + jest.spyOn(utdEvent, "isDecryptionFailure").mockReturnValue(true); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("when the initial sync completed", () => { + beforeEach(() => { + client.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Stopped, { nextSyncToken: "abc123" }); + }); + + describe("and an undecryptable event occurs", () => { + beforeEach(() => { + client.emit(MatrixEventEvent.Decrypted, utdEvent); + // simulate event grace period + jest.advanceTimersByTime(5500); + }); + + it("should send a rageshake", () => { + expect(mocked(client).sendToDevice.mock.calls).toMatchInlineSnapshot(` + [ + [ + "im.vector.auto_rs_request", + { + "@userId:matrix.org": { + "undefined": { + "device_id": undefined, + "event_id": "${utdEvent.getId()}", + "recipient_rageshake": undefined, + "room_id": "!room:example.com", + "sender_key": undefined, + "session_id": undefined, + "user_id": "@userId:matrix.org", + }, + }, + }, + ], + ] + `); + }); + }); + }); +}); diff --git a/test/stores/WidgetLayoutStore-test.ts b/test/stores/WidgetLayoutStore-test.ts index 54d40c52b7..81f373d0f6 100644 --- a/test/stores/WidgetLayoutStore-test.ts +++ b/test/stores/WidgetLayoutStore-test.ts @@ -14,11 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Room } from "matrix-js-sdk/src/matrix"; +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"; // setup test env values const roomId = "!room:server"; @@ -34,31 +36,54 @@ const mockRoom = { }, }; -const mockApps = [ - { roomId: roomId, id: "1" }, - { roomId: roomId, id: "2" }, - { roomId: roomId, id: "3" }, - { roomId: roomId, id: "4" }, -]; - -// fake the WidgetStore.instance to just return an object with `getApps` -jest.spyOn(WidgetStore, "instance", "get").mockReturnValue({ getApps: (_room) => mockApps }); - describe("WidgetLayoutStore", () => { - // we need to init a client so it does not error, when asking for DeviceStorage handlers (SettingsStore.setValue("Widgets.layout")) - stubClient(); + let client: MatrixClient; + let store: WidgetLayoutStore; + let roomUpdateListener: (event: string) => void; + let mockApps: IApp[]; - const store = WidgetLayoutStore.instance; + beforeEach(() => { + mockApps = [ + { roomId: roomId, id: "1" }, + { roomId: roomId, id: "2" }, + { roomId: roomId, id: "3" }, + { roomId: roomId, id: "4" }, + ]; - it("all widgets should be in the right container by default", async () => { + // 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); + }); + + 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("add three widgets to top container", async () => { store.recalculateRoom(mockRoom); store.moveToContainer(mockRoom, mockApps[0], Container.Top); @@ -68,6 +93,7 @@ describe("WidgetLayoutStore", () => { 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); @@ -75,6 +101,7 @@ describe("WidgetLayoutStore", () => { 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); @@ -87,6 +114,7 @@ describe("WidgetLayoutStore", () => { ); 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); @@ -99,6 +127,7 @@ describe("WidgetLayoutStore", () => { 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); @@ -109,6 +138,7 @@ describe("WidgetLayoutStore", () => { 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); @@ -119,4 +149,118 @@ describe("WidgetLayoutStore", () => { 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([mockApps[0]]); + expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]); + expect(store.getContainerWidgets(mockRoom, Container.Right)).toEqual([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(( + ({ 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": 23, + "index": 2, + "width": 64, + }, + "2": { + "container": "top", + "height": 23, + "index": 0, + "width": 10, + }, + "3": { + "container": "top", + "height": 23, + "index": 1, + "width": 26, + }, + "4": { + "container": "right", + }, + }, + }, + "", + ], + ] + `); + }); });