From 0191b76e66ef5a180521e2ee1d925d56fcc9f650 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Mon, 6 Mar 2023 13:15:53 +0000 Subject: [PATCH 1/3] Tests for BreadcrumbsStore.meetsRoomRequirements --- src/stores/BreadcrumbsStore.ts | 7 +++ test/stores/BreadcrumbsStore-test.ts | 88 ++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 test/stores/BreadcrumbsStore-test.ts diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 40f0b5872d3..8a2eda2ae6c 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -65,6 +65,13 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { return !!this.state.enabled && this.meetsRoomRequirement; } + /** + * Do we have enough rooms to justify showing the breadcrumbs? + * (Or is the labs feature enabled?) + * + * @returns true if there are at least 20 visible rooms or + * feature_breadcrumbs_v2 is enabled. + */ public get meetsRoomRequirement(): boolean { if (SettingsStore.getValue("feature_breadcrumbs_v2")) return true; return this.matrixClient?.getVisibleRooms().length >= 20; diff --git a/test/stores/BreadcrumbsStore-test.ts b/test/stores/BreadcrumbsStore-test.ts new file mode 100644 index 00000000000..a73efcc4935 --- /dev/null +++ b/test/stores/BreadcrumbsStore-test.ts @@ -0,0 +1,88 @@ +/* +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 { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; + +import { createTestClient, setupAsyncStoreWithClient } from "../test-utils"; +import SettingsStore from "../../src/settings/SettingsStore"; +import { BreadcrumbsStore } from "../../src/stores/BreadcrumbsStore"; + +describe("BreadcrumbsStore", () => { + let store: BreadcrumbsStore; + const client: MatrixClient = createTestClient(); + + beforeEach(() => { + jest.resetAllMocks(); + store = BreadcrumbsStore.instance; + setupAsyncStoreWithClient(store, client); + }); + + describe("If the feature_breadcrumbs_v2 feature is not enabled", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + }); + + it("does not meet room requirements if there are not enough rooms", () => { + // We don't have enough rooms, so we don't meet requirements + mocked(client.getVisibleRooms).mockReturnValue(fakeRooms(2)); + expect(store.meetsRoomRequirement).toBe(false); + }); + + it("meets room requirements if there are enough rooms", () => { + // We do have enough rooms to show breadcrumbs + mocked(client.getVisibleRooms).mockReturnValue(fakeRooms(25)); + expect(store.meetsRoomRequirement).toBe(true); + }); + }); + + describe("If the feature_breadcrumbs_v2 feature is enabled", () => { + beforeEach(() => { + // Turn on feature_breadcrumbs_v2 setting + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (settingName) => settingName === "feature_breadcrumbs_v2", + ); + }); + + it("always meets room requirements", () => { + // With enough rooms, we meet requirements + mocked(client.getVisibleRooms).mockReturnValue(fakeRooms(25)); + expect(store.meetsRoomRequirement).toBe(true); + + // And even with not enough we do, because the feature is enabled. + mocked(client.getVisibleRooms).mockReturnValue(fakeRooms(2)); + expect(store.meetsRoomRequirement).toBe(true); + }); + }); + + /** + * Create as many fake rooms in an array as you ask for. + */ + function fakeRooms(howMany: number): Array { + const ret = []; + for (let i = 0; i < howMany; i++) { + ret.push(fakeRoom()); + } + return ret; + } + + let roomIdx = 0; + + function fakeRoom(): Room { + roomIdx++; + return new Room(`room${roomIdx}`, client, "@user:example.com"); + } +}); From 6112b6d9706f669d7a8ff2b54f246cc25ee9e8d1 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Mon, 6 Mar 2023 16:15:02 +0000 Subject: [PATCH 2/3] Tests for appending rooms to BreadcrumbsStore --- test/stores/BreadcrumbsStore-test.ts | 60 +++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/test/stores/BreadcrumbsStore-test.ts b/test/stores/BreadcrumbsStore-test.ts index a73efcc4935..24d5d84097b 100644 --- a/test/stores/BreadcrumbsStore-test.ts +++ b/test/stores/BreadcrumbsStore-test.ts @@ -16,10 +16,13 @@ limitations under the License. import { mocked } from "jest-mock"; import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { act } from "react-dom/test-utils"; -import { createTestClient, setupAsyncStoreWithClient } from "../test-utils"; +import { createTestClient, flushPromises, setupAsyncStoreWithClient } from "../test-utils"; import SettingsStore from "../../src/settings/SettingsStore"; import { BreadcrumbsStore } from "../../src/stores/BreadcrumbsStore"; +import { Action } from "../../src/dispatcher/actions"; +import { defaultDispatcher } from "../../src/dispatcher/dispatcher"; describe("BreadcrumbsStore", () => { let store: BreadcrumbsStore; @@ -29,6 +32,7 @@ describe("BreadcrumbsStore", () => { jest.resetAllMocks(); store = BreadcrumbsStore.instance; setupAsyncStoreWithClient(store, client); + jest.spyOn(SettingsStore, "setValue").mockImplementation(() => Promise.resolve()); }); describe("If the feature_breadcrumbs_v2 feature is not enabled", () => { @@ -68,6 +72,60 @@ describe("BreadcrumbsStore", () => { }); }); + it("Appends a room when you join", async () => { + // Sanity: no rooms initially + expect(store.rooms).toEqual([]); + + // Given a room + const room = fakeRoom(); + mocked(client.getRoom).mockReturnValue(room); + mocked(client.getRoomUpgradeHistory).mockReturnValue([]); + + // When we hear that we have joined it + await dispatchJoinRoom(room.roomId); + + // It is stored in the store's room list + expect(store.rooms.map((r) => r.roomId)).toEqual([room.roomId]); + }); + + it("Replaces the old room when a newer one joins", async () => { + // Given an old room and a new room + const oldRoom = fakeRoom(); + const newRoom = fakeRoom(); + mocked(client.getRoom).mockImplementation((roomId) => { + if (roomId === oldRoom.roomId) return oldRoom; + return newRoom; + }); + // Where the new one is a predecessor of the old one + mocked(client.getRoomUpgradeHistory).mockReturnValue([oldRoom, newRoom]); + + // When we hear that we joined the old room, then the new one + await dispatchJoinRoom(oldRoom.roomId); + await dispatchJoinRoom(newRoom.roomId); + + // The store only has the new one + expect(store.rooms.map((r) => r.roomId)).toEqual([newRoom.roomId]); + }); + + /** + * Send a JoinRoom event via the dispatcher, and wait for it to process. + */ + async function dispatchJoinRoom(roomId: string) { + defaultDispatcher.dispatch( + { + action: Action.JoinRoom, + roomId, + metricsTrigger: null, + }, + true, // synchronous dispatch + ); + + // Wait for event dispatch to happen + await act(async () => { + await flushPromises(); + }); + } + /** * Create as many fake rooms in an array as you ask for. */ From ad9c1828db8105aedc905a47b22a8de8e9df18c7 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Mon, 6 Mar 2023 16:34:08 +0000 Subject: [PATCH 3/3] Support dynamic room predecessors in BreadcrumbsStore --- src/stores/BreadcrumbsStore.ts | 6 +- test/stores/BreadcrumbsStore-test.ts | 114 +++++++++++++++++++++------ 2 files changed, 93 insertions(+), 27 deletions(-) diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 8a2eda2ae6c..cff80f52514 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -74,7 +74,8 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { */ public get meetsRoomRequirement(): boolean { if (SettingsStore.getValue("feature_breadcrumbs_v2")) return true; - return this.matrixClient?.getVisibleRooms().length >= 20; + const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors"); + return this.matrixClient?.getVisibleRooms(msc3946ProcessDynamicPredecessor).length >= 20; } protected async onAction(payload: SettingUpdatedPayload | ViewRoomPayload | JoinRoomPayload): Promise { @@ -145,10 +146,11 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { private async appendRoom(room: Room): Promise { let updated = false; const rooms = (this.state.rooms || []).slice(); // cheap clone + const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors"); // If the room is upgraded, use that room instead. We'll also splice out // any children of the room. - const history = this.matrixClient.getRoomUpgradeHistory(room.roomId); + const history = this.matrixClient.getRoomUpgradeHistory(room.roomId, false, msc3946ProcessDynamicPredecessor); if (history.length > 1) { room = history[history.length - 1]; // Last room is most recent in history diff --git a/test/stores/BreadcrumbsStore-test.ts b/test/stores/BreadcrumbsStore-test.ts index 24d5d84097b..9b86e789dfb 100644 --- a/test/stores/BreadcrumbsStore-test.ts +++ b/test/stores/BreadcrumbsStore-test.ts @@ -51,6 +51,29 @@ describe("BreadcrumbsStore", () => { mocked(client.getVisibleRooms).mockReturnValue(fakeRooms(25)); expect(store.meetsRoomRequirement).toBe(true); }); + + describe("And the feature_dynamic_room_predecessors is enabled", () => { + beforeEach(() => { + // Turn on feature_dynamic_room_predecessors setting + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (settingName) => settingName === "feature_dynamic_room_predecessors", + ); + }); + + it("passes through the dynamic room precessors flag", () => { + mocked(client.getVisibleRooms).mockReturnValue(fakeRooms(25)); + store.meetsRoomRequirement; + expect(client.getVisibleRooms).toHaveBeenCalledWith(true); + }); + }); + + describe("And the feature_dynamic_room_predecessors is not enabled", () => { + it("passes through the dynamic room precessors flag", () => { + mocked(client.getVisibleRooms).mockReturnValue(fakeRooms(25)); + store.meetsRoomRequirement; + expect(client.getVisibleRooms).toHaveBeenCalledWith(false); + }); + }); }); describe("If the feature_breadcrumbs_v2 feature is enabled", () => { @@ -72,39 +95,80 @@ describe("BreadcrumbsStore", () => { }); }); - it("Appends a room when you join", async () => { - // Sanity: no rooms initially - expect(store.rooms).toEqual([]); + describe("If the feature_dynamic_room_predecessors is not enabled", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + }); + + it("Appends a room when you join", async () => { + // Sanity: no rooms initially + expect(store.rooms).toEqual([]); + + // Given a room + const room = fakeRoom(); + mocked(client.getRoom).mockReturnValue(room); + mocked(client.getRoomUpgradeHistory).mockReturnValue([]); + + // When we hear that we have joined it + await dispatchJoinRoom(room.roomId); - // Given a room - const room = fakeRoom(); - mocked(client.getRoom).mockReturnValue(room); - mocked(client.getRoomUpgradeHistory).mockReturnValue([]); + // It is stored in the store's room list + expect(store.rooms.map((r) => r.roomId)).toEqual([room.roomId]); + }); + + it("Replaces the old room when a newer one joins", async () => { + // Given an old room and a new room + const oldRoom = fakeRoom(); + const newRoom = fakeRoom(); + mocked(client.getRoom).mockImplementation((roomId) => { + if (roomId === oldRoom.roomId) return oldRoom; + return newRoom; + }); + // Where the new one is a predecessor of the old one + mocked(client.getRoomUpgradeHistory).mockReturnValue([oldRoom, newRoom]); + + // When we hear that we joined the old room, then the new one + await dispatchJoinRoom(oldRoom.roomId); + await dispatchJoinRoom(newRoom.roomId); + + // The store only has the new one + expect(store.rooms.map((r) => r.roomId)).toEqual([newRoom.roomId]); + }); - // When we hear that we have joined it - await dispatchJoinRoom(room.roomId); + it("Passes through the dynamic predecessor setting", async () => { + // Given a room + const room = fakeRoom(); + mocked(client.getRoom).mockReturnValue(room); + mocked(client.getRoomUpgradeHistory).mockReturnValue([]); - // It is stored in the store's room list - expect(store.rooms.map((r) => r.roomId)).toEqual([room.roomId]); + // When we signal that we have joined + await dispatchJoinRoom(room.roomId); + + // We pass the value of the dynamic predecessor setting through + expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, false); + }); }); - it("Replaces the old room when a newer one joins", async () => { - // Given an old room and a new room - const oldRoom = fakeRoom(); - const newRoom = fakeRoom(); - mocked(client.getRoom).mockImplementation((roomId) => { - if (roomId === oldRoom.roomId) return oldRoom; - return newRoom; + describe("If the feature_dynamic_room_predecessors is enabled", () => { + beforeEach(() => { + // Turn on feature_dynamic_room_predecessors setting + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (settingName) => settingName === "feature_dynamic_room_predecessors", + ); }); - // Where the new one is a predecessor of the old one - mocked(client.getRoomUpgradeHistory).mockReturnValue([oldRoom, newRoom]); - // When we hear that we joined the old room, then the new one - await dispatchJoinRoom(oldRoom.roomId); - await dispatchJoinRoom(newRoom.roomId); + it("Passes through the dynamic predecessor setting", async () => { + // Given a room + const room = fakeRoom(); + mocked(client.getRoom).mockReturnValue(room); + mocked(client.getRoomUpgradeHistory).mockReturnValue([]); - // The store only has the new one - expect(store.rooms.map((r) => r.roomId)).toEqual([newRoom.roomId]); + // When we signal that we have joined + await dispatchJoinRoom(room.roomId); + + // We pass the value of the dynamic predecessor setting through + expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, true); + }); }); /**