diff --git a/cypress.config.ts b/cypress.config.ts index dfa17ab32c..f9bc521bdd 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -30,8 +30,8 @@ export default defineConfig({ specPattern: "cypress/e2e/**/*.{js,jsx,ts,tsx}", }, env: { - // Docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image. - SLIDING_SYNC_PROXY_TAG: "v0.6.0", + // Docker tag to use for `ghcr.io/matrix-org/sliding-sync` image. + SLIDING_SYNC_PROXY_TAG: "v0.99.0-rc1", HOMESERVER: "synapse", }, retries: { diff --git a/cypress/e2e/sliding-sync/sliding-sync.ts b/cypress/e2e/sliding-sync/sliding-sync.ts index 1b2642eeb4..c6d2c298fe 100644 --- a/cypress/e2e/sliding-sync/sliding-sync.ts +++ b/cypress/e2e/sliding-sync/sliding-sync.ts @@ -21,8 +21,6 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { Interception } from "cypress/types/net-stubbing"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; -import { SettingLevel } from "../../../src/settings/SettingLevel"; -import { Layout } from "../../../src/settings/enums/Layout"; import { ProxyInstance } from "../../plugins/sliding-sync"; describe("Sliding Sync", () => { @@ -102,21 +100,6 @@ describe("Sliding Sync", () => { }); }; - // sanity check everything works - it("should correctly render expected messages", () => { - cy.get("@roomId").then((roomId) => cy.visit("/#/room/" + roomId)); - cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - - // Wait until configuration is finished - cy.contains( - ".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary", - "created and configured the room.", - ); - - // Click "expand" link button - cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click(); - }); - it("should render the Rooms list in reverse chronological order by default and allowing sorting A-Z", () => { // create rooms and check room names are correct cy.createRoom({ name: "Apple" }).then(() => cy.contains(".mx_RoomSublist", "Apple")); diff --git a/cypress/plugins/sliding-sync/index.ts b/cypress/plugins/sliding-sync/index.ts index 8204fb578d..ab39c7a42b 100644 --- a/cypress/plugins/sliding-sync/index.ts +++ b/cypress/plugins/sliding-sync/index.ts @@ -23,7 +23,7 @@ import { getFreePort } from "../utils/port"; import { HomeserverInstance } from "../utils/homeserver"; // A cypress plugin to add command to start & stop https://github.com/matrix-org/sliding-sync -// SLIDING_SYNC_PROXY_TAG env used as the docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image. +// SLIDING_SYNC_PROXY_TAG env used as the docker tag to use for `ghcr.io/matrix-org/sliding-sync` image. export interface ProxyInstance { containerId: string; @@ -72,7 +72,7 @@ async function proxyStart(dockerTag: string, homeserver: HomeserverInstance): Pr const port = await getFreePort(); console.log(new Date(), "starting proxy container...", dockerTag); const containerId = await dockerRun({ - image: "ghcr.io/matrix-org/sliding-sync-proxy:" + dockerTag, + image: "ghcr.io/matrix-org/sliding-sync:" + dockerTag, containerName: "react-sdk-cypress-sliding-sync-proxy", params: [ "--rm", diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index 75a796df19..4a6c113253 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -119,12 +119,10 @@ export class SlidingSyncManager { public slidingSync: SlidingSync; private client: MatrixClient; - private listIdToIndex: Record; private configureDefer: IDeferred; public constructor() { - this.listIdToIndex = {}; this.configureDefer = defer(); } @@ -134,13 +132,18 @@ export class SlidingSyncManager { public configure(client: MatrixClient, proxyUrl: string): SlidingSync { this.client = client; - this.listIdToIndex = {}; // by default use the encrypted subscription as that gets everything, which is a safer // default than potentially missing member events. - this.slidingSync = new SlidingSync(proxyUrl, [], ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS); + this.slidingSync = new SlidingSync( + proxyUrl, + new Map(), + ENCRYPTED_SUBSCRIPTION, + client, + SLIDING_SYNC_TIMEOUT_MS, + ); this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION); // set the space list - this.slidingSync.setList(this.getOrAllocateListIndex(SlidingSyncManager.ListSpaces), { + this.slidingSync.setList(SlidingSyncManager.ListSpaces, { ranges: [[0, 20]], sort: ["by_name"], slow_get_all_rooms: true, @@ -173,47 +176,16 @@ export class SlidingSyncManager { return this.slidingSync; } - public listIdForIndex(index: number): string | null { - for (const listId in this.listIdToIndex) { - if (this.listIdToIndex[listId] === index) { - return listId; - } - } - return null; - } - - /** - * Allocate or retrieve the list index for an arbitrary list ID. For example SlidingSyncManager.ListSpaces - * @param listId A string which represents the list. - * @returns The index to use when registering lists or listening for callbacks. - */ - public getOrAllocateListIndex(listId: string): number { - let index = this.listIdToIndex[listId]; - if (index === undefined) { - // assign next highest index - index = -1; - for (const id in this.listIdToIndex) { - const listIndex = this.listIdToIndex[id]; - if (listIndex > index) { - index = listIndex; - } - } - index++; - this.listIdToIndex[listId] = index; - } - return index; - } - /** * Ensure that this list is registered. - * @param listIndex The list index to register + * @param listKey The list key to register * @param updateArgs The fields to update on the list. * @returns The complete list request params */ - public async ensureListRegistered(listIndex: number, updateArgs: PartialSlidingSyncRequest): Promise { - logger.debug("ensureListRegistered:::", listIndex, updateArgs); + public async ensureListRegistered(listKey: string, updateArgs: PartialSlidingSyncRequest): Promise { + logger.debug("ensureListRegistered:::", listKey, updateArgs); await this.configureDefer.promise; - let list = this.slidingSync.getList(listIndex); + let list = this.slidingSync.getListParams(listKey); if (!list) { list = { ranges: [[0, 20]], @@ -252,14 +224,14 @@ export class SlidingSyncManager { try { // if we only have range changes then call a different function so we don't nuke the list from before if (updateArgs.ranges && Object.keys(updateArgs).length === 1) { - await this.slidingSync.setListRanges(listIndex, updateArgs.ranges); + await this.slidingSync.setListRanges(listKey, updateArgs.ranges); } else { - await this.slidingSync.setList(listIndex, list); + await this.slidingSync.setList(listKey, list); } } catch (err) { logger.debug("ensureListRegistered: update failed txn_id=", err); } - return this.slidingSync.getList(listIndex); + return this.slidingSync.getListParams(listKey)!; } public async setRoomVisible(roomId: string, visible: boolean): Promise { @@ -304,7 +276,6 @@ export class SlidingSyncManager { */ public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise { await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load - const listIndex = this.getOrAllocateListIndex(SlidingSyncManager.ListSearch); let startIndex = batchSize; let hasMore = true; let firstTime = true; @@ -316,7 +287,7 @@ export class SlidingSyncManager { [startIndex, endIndex], ]; if (firstTime) { - await this.slidingSync.setList(listIndex, { + await this.slidingSync.setList(SlidingSyncManager.ListSearch, { // e.g [0,19] [20,39] then [0,19] [40,59]. We keep [0,20] constantly to ensure // any changes to the list whilst spidering are caught. ranges: ranges, @@ -342,15 +313,17 @@ export class SlidingSyncManager { }, }); } else { - await this.slidingSync.setListRanges(listIndex, ranges); + await this.slidingSync.setListRanges(SlidingSyncManager.ListSearch, ranges); } - // gradually request more over time - await sleep(gapBetweenRequestsMs); } catch (err) { // do nothing, as we reject only when we get interrupted but that's fine as the next // request will include our data + } finally { + // gradually request more over time, even on errors. + await sleep(gapBetweenRequestsMs); } - hasMore = endIndex + 1 < this.slidingSync.getListData(listIndex)?.joinedCount; + const listData = this.slidingSync.getListData(SlidingSyncManager.ListSearch)!; + hasMore = endIndex + 1 < listData.joinedCount; startIndex += batchSize; firstTime = false; } diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 11efcda896..1dd25cbff5 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -340,9 +340,8 @@ export default class RoomSublist extends React.Component { private onShowAllClick = async (): Promise => { if (this.slidingSyncMode) { - const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId); const count = RoomListStore.instance.getCount(this.props.tagId); - await SlidingSyncManager.instance.ensureListRegistered(slidingSyncIndex, { + await SlidingSyncManager.instance.ensureListRegistered(this.props.tagId, { ranges: [[0, count]], }); } @@ -566,10 +565,9 @@ export default class RoomSublist extends React.Component { let isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic; let isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; if (this.slidingSyncMode) { - const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId); - const slidingList = SlidingSyncManager.instance.slidingSync.getList(slidingSyncIndex); - isAlphabetical = slidingList.sort[0] === "by_name"; - isUnreadFirst = slidingList.sort[0] === "by_notification_level"; + const slidingList = SlidingSyncManager.instance.slidingSync.getListParams(this.props.tagId); + isAlphabetical = (slidingList?.sort || [])[0] === "by_name"; + isUnreadFirst = (slidingList?.sort || [])[0] === "by_notification_level"; } // Invites don't get some nonsense options, so only add them if we have to. diff --git a/src/hooks/useSlidingSyncRoomSearch.ts b/src/hooks/useSlidingSyncRoomSearch.ts index 3713ab7618..7d5d1d92aa 100644 --- a/src/hooks/useSlidingSyncRoomSearch.ts +++ b/src/hooks/useSlidingSyncRoomSearch.ts @@ -34,7 +34,6 @@ export const useSlidingSyncRoomSearch = (): { const [rooms, setRooms] = useState([]); const [loading, setLoading] = useState(false); - const listIndex = SlidingSyncManager.instance.getOrAllocateListIndex(SlidingSyncManager.ListSearch); const [updateQuery, updateResult] = useLatestResult<{ term: string; limit?: number }, Room[]>(setRooms); @@ -50,14 +49,16 @@ export const useSlidingSyncRoomSearch = (): { try { setLoading(true); - await SlidingSyncManager.instance.ensureListRegistered(listIndex, { + await SlidingSyncManager.instance.ensureListRegistered(SlidingSyncManager.ListSearch, { ranges: [[0, limit]], filters: { room_name_like: term, }, }); const rooms = []; - const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(listIndex); + const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData( + SlidingSyncManager.ListSearch, + )!; let i = 0; while (roomIndexToRoomId[i]) { const roomId = roomIndexToRoomId[i]; @@ -78,7 +79,7 @@ export const useSlidingSyncRoomSearch = (): { // TODO: delete the list? } }, - [updateQuery, updateResult, listIndex], + [updateQuery, updateResult], ); return { diff --git a/src/stores/room-list/SlidingRoomListStore.ts b/src/stores/room-list/SlidingRoomListStore.ts index 3807dd6559..ff2113379d 100644 --- a/src/stores/room-list/SlidingRoomListStore.ts +++ b/src/stores/room-list/SlidingRoomListStore.ts @@ -84,20 +84,20 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl public constructor(dis: MatrixDispatcher, private readonly context: SdkContextClass) { super(dis); this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares + this.stickyRoomId = null; } public async setTagSorting(tagId: TagID, sort: SortAlgorithm): Promise { logger.info("SlidingRoomListStore.setTagSorting ", tagId, sort); this.tagIdToSortAlgo[tagId] = sort; - const slidingSyncIndex = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); switch (sort) { case SortAlgorithm.Alphabetic: - await this.context.slidingSyncManager.ensureListRegistered(slidingSyncIndex, { + await this.context.slidingSyncManager.ensureListRegistered(tagId, { sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic], }); break; case SortAlgorithm.Recent: - await this.context.slidingSyncManager.ensureListRegistered(slidingSyncIndex, { + await this.context.slidingSyncManager.ensureListRegistered(tagId, { sort: SlidingSyncSortToFilter[SortAlgorithm.Recent], }); break; @@ -164,8 +164,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl // check all lists for each tag we know about and see if the room is there const tags: TagID[] = []; for (const tagId in this.tagIdToSortAlgo) { - const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); - const listData = this.context.slidingSyncManager.slidingSync.getListData(index); + const listData = this.context.slidingSyncManager.slidingSync.getListData(tagId); if (!listData) { continue; } @@ -251,19 +250,19 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl } // now set the rooms - const rooms = orderedRoomIds.map((roomId) => { - return this.matrixClient.getRoom(roomId); + const rooms: Room[] = []; + orderedRoomIds.forEach((roomId) => { + const room = this.matrixClient.getRoom(roomId); + if (!room) { + return; + } + rooms.push(room); }); tagMap[tagId] = rooms; this.tagMap = tagMap; } - private onSlidingSyncListUpdate( - listIndex: number, - joinCount: number, - roomIndexToRoomId: Record, - ): void { - const tagId = this.context.slidingSyncManager.listIdForIndex(listIndex); + private onSlidingSyncListUpdate(tagId: string, joinCount: number, roomIndexToRoomId: Record): void { this.counts[tagId] = joinCount; this.refreshOrderedLists(tagId, roomIndexToRoomId); // let the UI update @@ -295,8 +294,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl if (room) { // resort it based on the slidingSync view of the list. This may cause this old sticky // room to cease to exist. - const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); - const listData = this.context.slidingSyncManager.slidingSync.getListData(index); + const listData = this.context.slidingSyncManager.slidingSync.getListData(tagId); if (!listData) { continue; } @@ -334,9 +332,8 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl const sort = SortAlgorithm.Recent; // default to recency sort, TODO: read from config this.tagIdToSortAlgo[tagId] = sort; this.emit(LISTS_LOADING_EVENT, tagId, true); - const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); this.context.slidingSyncManager - .ensureListRegistered(index, { + .ensureListRegistered(tagId, { filters: filter, sort: SlidingSyncSortToFilter[sort], }) @@ -361,15 +358,17 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl if (roomId === activeSpace) { return; } + if (!filters.spaces) { + filters.spaces = []; + } filters.spaces.push(roomId); // add subspace }, false, ); this.emit(LISTS_LOADING_EVENT, tagId, true); - const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId); this.context.slidingSyncManager - .ensureListRegistered(index, { + .ensureListRegistered(tagId, { filters: filters, }) .then(() => { diff --git a/test/SlidingSyncManager-test.ts b/test/SlidingSyncManager-test.ts index 58784e554d..6b5a854e84 100644 --- a/test/SlidingSyncManager-test.ts +++ b/test/SlidingSyncManager-test.ts @@ -78,13 +78,76 @@ describe("SlidingSyncManager", () => { }); }); + describe("ensureListRegistered", () => { + it("creates a new list based on the key", async () => { + const listKey = "key"; + mocked(slidingSync.getListParams).mockReturnValue(null); + mocked(slidingSync.setList).mockResolvedValue("yep"); + await manager.ensureListRegistered(listKey, { + sort: ["by_recency"], + }); + expect(slidingSync.setList).toBeCalledWith( + listKey, + expect.objectContaining({ + sort: ["by_recency"], + }), + ); + }); + + it("updates an existing list based on the key", async () => { + const listKey = "key"; + mocked(slidingSync.getListParams).mockReturnValue({ + ranges: [[0, 42]], + }); + mocked(slidingSync.setList).mockResolvedValue("yep"); + await manager.ensureListRegistered(listKey, { + sort: ["by_recency"], + }); + expect(slidingSync.setList).toBeCalledWith( + listKey, + expect.objectContaining({ + sort: ["by_recency"], + ranges: [[0, 42]], + }), + ); + }); + + it("updates ranges on an existing list based on the key if there's no other changes", async () => { + const listKey = "key"; + mocked(slidingSync.getListParams).mockReturnValue({ + ranges: [[0, 42]], + }); + mocked(slidingSync.setList).mockResolvedValue("yep"); + await manager.ensureListRegistered(listKey, { + ranges: [[0, 52]], + }); + expect(slidingSync.setList).not.toBeCalled(); + expect(slidingSync.setListRanges).toBeCalledWith(listKey, [[0, 52]]); + }); + + it("no-ops for idential changes", async () => { + const listKey = "key"; + mocked(slidingSync.getListParams).mockReturnValue({ + ranges: [[0, 42]], + sort: ["by_recency"], + }); + mocked(slidingSync.setList).mockResolvedValue("yep"); + await manager.ensureListRegistered(listKey, { + ranges: [[0, 42]], + sort: ["by_recency"], + }); + expect(slidingSync.setList).not.toBeCalled(); + expect(slidingSync.setListRanges).not.toBeCalled(); + }); + }); + describe("startSpidering", () => { it("requests in batchSizes", async () => { const gapMs = 1; const batchSize = 10; mocked(slidingSync.setList).mockResolvedValue("yep"); mocked(slidingSync.setListRanges).mockResolvedValue("yep"); - mocked(slidingSync.getListData).mockImplementation((i) => { + mocked(slidingSync.getListData).mockImplementation((key) => { return { joinedCount: 64, roomIndexToRoomId: {}, @@ -106,24 +169,24 @@ describe("SlidingSyncManager", () => { wantWindows.forEach((range, i) => { if (i === 0) { expect(slidingSync.setList).toBeCalledWith( - manager.getOrAllocateListIndex(SlidingSyncManager.ListSearch), + SlidingSyncManager.ListSearch, expect.objectContaining({ ranges: [[0, batchSize - 1], range], }), ); return; } - expect(slidingSync.setListRanges).toBeCalledWith( - manager.getOrAllocateListIndex(SlidingSyncManager.ListSearch), - [[0, batchSize - 1], range], - ); + expect(slidingSync.setListRanges).toBeCalledWith(SlidingSyncManager.ListSearch, [ + [0, batchSize - 1], + range, + ]); }); }); it("handles accounts with zero rooms", async () => { const gapMs = 1; const batchSize = 10; mocked(slidingSync.setList).mockResolvedValue("yep"); - mocked(slidingSync.getListData).mockImplementation((i) => { + mocked(slidingSync.getListData).mockImplementation((key) => { return { joinedCount: 0, roomIndexToRoomId: {}, @@ -133,7 +196,7 @@ describe("SlidingSyncManager", () => { expect(slidingSync.getListData).toBeCalledTimes(1); expect(slidingSync.setList).toBeCalledTimes(1); expect(slidingSync.setList).toBeCalledWith( - manager.getOrAllocateListIndex(SlidingSyncManager.ListSearch), + SlidingSyncManager.ListSearch, expect.objectContaining({ ranges: [ [0, batchSize - 1], @@ -146,7 +209,7 @@ describe("SlidingSyncManager", () => { const gapMs = 1; const batchSize = 10; mocked(slidingSync.setList).mockRejectedValue("narp"); - mocked(slidingSync.getListData).mockImplementation((i) => { + mocked(slidingSync.getListData).mockImplementation((key) => { return { joinedCount: 0, roomIndexToRoomId: {}, @@ -156,7 +219,7 @@ describe("SlidingSyncManager", () => { expect(slidingSync.getListData).toBeCalledTimes(1); expect(slidingSync.setList).toBeCalledTimes(1); expect(slidingSync.setList).toBeCalledWith( - manager.getOrAllocateListIndex(SlidingSyncManager.ListSearch), + SlidingSyncManager.ListSearch, expect.objectContaining({ ranges: [ [0, batchSize - 1], diff --git a/test/hooks/useSlidingSyncRoomSearch-test.tsx b/test/hooks/useSlidingSyncRoomSearch-test.tsx new file mode 100644 index 0000000000..fbac2fe06e --- /dev/null +++ b/test/hooks/useSlidingSyncRoomSearch-test.tsx @@ -0,0 +1,111 @@ +/* +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. +*/ + +// eslint-disable-next-line deprecate/import +import { mount } from "enzyme"; +import { sleep } from "matrix-js-sdk/src/utils"; +import React from "react"; +import { act } from "react-dom/test-utils"; +import { mocked } from "jest-mock"; +import { SlidingSync } from "matrix-js-sdk/src/sliding-sync"; +import { Room } from "matrix-js-sdk/src/matrix"; + +import { SlidingSyncRoomSearchOpts, useSlidingSyncRoomSearch } from "../../src/hooks/useSlidingSyncRoomSearch"; +import { MockEventEmitter, stubClient } from "../test-utils"; +import { SlidingSyncManager } from "../../src/SlidingSyncManager"; + +type RoomSearchHook = { + loading: boolean; + rooms: Room[]; + search(opts: SlidingSyncRoomSearchOpts): Promise; +}; + +// hooks must be inside a React component else you get: +// "Invalid hook call. Hooks can only be called inside of the body of a function component." +function RoomSearchComponent(props: { onClick: (h: RoomSearchHook) => void }) { + const roomSearch = useSlidingSyncRoomSearch(); + return
props.onClick(roomSearch)} />; +} + +describe("useSlidingSyncRoomSearch", () => { + it("should display rooms when searching", async () => { + const client = stubClient(); + const roomA = new Room("!a:localhost", client, client.getUserId()!); + const roomB = new Room("!b:localhost", client, client.getUserId()!); + const slidingSync = mocked( + new MockEventEmitter({ + getListData: jest.fn(), + }) as unknown as SlidingSync, + ); + jest.spyOn(SlidingSyncManager.instance, "ensureListRegistered").mockResolvedValue({ + ranges: [[0, 9]], + }); + SlidingSyncManager.instance.slidingSync = slidingSync; + mocked(slidingSync.getListData).mockReturnValue({ + joinedCount: 2, + roomIndexToRoomId: { + 0: roomA.roomId, + 1: roomB.roomId, + }, + }); + mocked(client.getRoom).mockImplementation((roomId) => { + switch (roomId) { + case roomA.roomId: + return roomA; + case roomB.roomId: + return roomB; + default: + return null; + } + }); + + // first check that everything is empty and then do the search + let executeHook = (roomSearch: RoomSearchHook) => { + expect(roomSearch.loading).toBe(false); + expect(roomSearch.rooms).toEqual([]); + roomSearch.search({ + limit: 10, + query: "foo", + }); + }; + const wrapper = mount( + { + executeHook(roomSearch); + }} + />, + ); + + // run the query + await act(async () => { + await sleep(1); + wrapper.simulate("click"); + return act(() => sleep(1)); + }); + // now we expect there to be rooms + executeHook = (roomSearch) => { + expect(roomSearch.loading).toBe(false); + expect(roomSearch.rooms).toEqual([roomA, roomB]); + }; + + // run the query + await act(async () => { + await sleep(1); + wrapper.simulate("click"); + return act(() => sleep(1)); + }); + }); +}); diff --git a/test/stores/room-list/SlidingRoomListStore-test.ts b/test/stores/room-list/SlidingRoomListStore-test.ts index 0c882ce35b..3014423349 100644 --- a/test/stores/room-list/SlidingRoomListStore-test.ts +++ b/test/stores/room-list/SlidingRoomListStore-test.ts @@ -30,7 +30,7 @@ import { RoomViewStore } from "../../../src/stores/RoomViewStore"; import { MatrixDispatcher } from "../../../src/dispatcher/dispatcher"; import { SortAlgorithm } from "../../../src/stores/room-list/algorithms/models"; import { DefaultTagID, TagID } from "../../../src/stores/room-list/models"; -import { UPDATE_SELECTED_SPACE } from "../../../src/stores/spaces"; +import { MetaSpace, UPDATE_SELECTED_SPACE } from "../../../src/stores/spaces"; import { LISTS_LOADING_EVENT } from "../../../src/stores/room-list/RoomListStore"; import { UPDATE_EVENT } from "../../../src/stores/AsyncStore"; @@ -42,7 +42,6 @@ describe("SlidingRoomListStore", () => { let context: TestSdkContext; let dis: MatrixDispatcher; let activeSpace: string; - let tagIdToIndex = {}; beforeEach(async () => { context = new TestSdkContext(); @@ -64,27 +63,6 @@ describe("SlidingRoomListStore", () => { getRoomId: jest.fn(), }) as unknown as RoomViewStore, ); - - // mock implementations to allow the store to map tag IDs to sliding sync list indexes and vice versa - let index = 0; - tagIdToIndex = {}; - mocked(context._SlidingSyncManager.getOrAllocateListIndex).mockImplementation((listId: string): number => { - if (tagIdToIndex[listId] != null) { - return tagIdToIndex[listId]; - } - tagIdToIndex[listId] = index; - index++; - return index; - }); - mocked(context.slidingSyncManager.listIdForIndex).mockImplementation((i) => { - for (const tagId in tagIdToIndex) { - const j = tagIdToIndex[tagId]; - if (i === j) { - return tagId; - } - } - return null; - }); mocked(context._SlidingSyncManager.ensureListRegistered).mockResolvedValue({ ranges: [[0, 10]], }); @@ -104,17 +82,31 @@ describe("SlidingRoomListStore", () => { // change the active space activeSpace = spaceRoomId; - context._SpaceStore.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false); + context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false); await p; - expect(context._SlidingSyncManager.ensureListRegistered).toHaveBeenCalledWith( - tagIdToIndex[DefaultTagID.Untagged], - { - filters: expect.objectContaining({ - spaces: [spaceRoomId], - }), + expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, { + filters: expect.objectContaining({ + spaces: [spaceRoomId], + }), + }); + }); + + it("gracefully handles subspaces in the home metaspace", async () => { + const subspace = "!sub:space"; + mocked(context._SpaceStore!.traverseSpace).mockImplementation( + (spaceId: string, fn: (roomId: string) => void) => { + fn(subspace); }, ); + activeSpace = MetaSpace.Home; + await store.start(); // call onReady + + expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, { + filters: expect.objectContaining({ + spaces: [subspace], + }), + }); }); it("alters 'filters.spaces' on the DefaultTagID.Untagged list if it loads with an active space", async () => { @@ -126,8 +118,8 @@ describe("SlidingRoomListStore", () => { }); await store.start(); // call onReady await p; - expect(context._SlidingSyncManager.ensureListRegistered).toHaveBeenCalledWith( - tagIdToIndex[DefaultTagID.Untagged], + expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith( + DefaultTagID.Untagged, expect.objectContaining({ filters: expect.objectContaining({ spaces: [spaceRoomId], @@ -146,7 +138,7 @@ describe("SlidingRoomListStore", () => { return listName === DefaultTagID.Untagged && !isLoading; }); - mocked(context._SpaceStore.traverseSpace).mockImplementation( + mocked(context._SpaceStore!.traverseSpace).mockImplementation( (spaceId: string, fn: (roomId: string) => void) => { if (spaceId === spaceRoomId) { fn(subSpace1); @@ -157,31 +149,27 @@ describe("SlidingRoomListStore", () => { // change the active space activeSpace = spaceRoomId; - context._SpaceStore.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false); + context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false); await p; - expect(context._SlidingSyncManager.ensureListRegistered).toHaveBeenCalledWith( - tagIdToIndex[DefaultTagID.Untagged], - { - filters: expect.objectContaining({ - spaces: [spaceRoomId, subSpace1, subSpace2], - }), - }, - ); + expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, { + filters: expect.objectContaining({ + spaces: [spaceRoomId, subSpace1, subSpace2], + }), + }); }); }); it("setTagSorting alters the 'sort' option in the list", async () => { - mocked(context._SlidingSyncManager.getOrAllocateListIndex).mockReturnValue(0); const tagId: TagID = "foo"; await store.setTagSorting(tagId, SortAlgorithm.Alphabetic); - expect(context._SlidingSyncManager.ensureListRegistered).toBeCalledWith(0, { + expect(context._SlidingSyncManager!.ensureListRegistered).toBeCalledWith(tagId, { sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic], }); expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Alphabetic); await store.setTagSorting(tagId, SortAlgorithm.Recent); - expect(context._SlidingSyncManager.ensureListRegistered).toBeCalledWith(0, { + expect(context._SlidingSyncManager!.ensureListRegistered).toBeCalledWith(tagId, { sort: SlidingSyncSortToFilter[SortAlgorithm.Recent], }); expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Recent); @@ -189,33 +177,31 @@ describe("SlidingRoomListStore", () => { it("getTagsForRoom gets the tags for the room", async () => { await store.start(); - const untaggedIndex = context._SlidingSyncManager.getOrAllocateListIndex(DefaultTagID.Untagged); - const favIndex = context._SlidingSyncManager.getOrAllocateListIndex(DefaultTagID.Favourite); const roomA = "!a:localhost"; const roomB = "!b:localhost"; - const indexToListData = { - [untaggedIndex]: { + const keyToListData: Record }> = { + [DefaultTagID.Untagged]: { joinedCount: 10, roomIndexToRoomId: { 0: roomA, 1: roomB, }, }, - [favIndex]: { + [DefaultTagID.Favourite]: { joinedCount: 2, roomIndexToRoomId: { 0: roomB, }, }, }; - mocked(context._SlidingSyncManager.slidingSync.getListData).mockImplementation((i: number) => { - return indexToListData[i] || null; + mocked(context._SlidingSyncManager!.slidingSync.getListData).mockImplementation((key: string) => { + return keyToListData[key] || null; }); - expect(store.getTagsForRoom(new Room(roomA, context.client, context.client.getUserId()))).toEqual([ + expect(store.getTagsForRoom(new Room(roomA, context.client!, context.client!.getUserId()))).toEqual([ DefaultTagID.Untagged, ]); - expect(store.getTagsForRoom(new Room(roomB, context.client, context.client.getUserId()))).toEqual([ + expect(store.getTagsForRoom(new Room(roomB, context.client!, context.client!.getUserId()))).toEqual([ DefaultTagID.Favourite, DefaultTagID.Untagged, ]); @@ -227,7 +213,6 @@ describe("SlidingRoomListStore", () => { const roomB = "!b:localhost"; const roomC = "!c:localhost"; const tagId = DefaultTagID.Favourite; - const listIndex = context.slidingSyncManager.getOrAllocateListIndex(tagId); const joinCount = 10; const roomIndexToRoomId = { // mixed to ensure we sort @@ -236,11 +221,11 @@ describe("SlidingRoomListStore", () => { 0: roomA, }; const rooms = [ - new Room(roomA, context.client, context.client.getUserId()), - new Room(roomB, context.client, context.client.getUserId()), - new Room(roomC, context.client, context.client.getUserId()), + new Room(roomA, context.client!, context.client!.getUserId()), + new Room(roomB, context.client!, context.client!.getUserId()), + new Room(roomC, context.client!, context.client!.getUserId()), ]; - mocked(context.client.getRoom).mockImplementation((roomId: string) => { + mocked(context.client!.getRoom).mockImplementation((roomId: string) => { switch (roomId) { case roomA: return rooms[0]; @@ -252,7 +237,7 @@ describe("SlidingRoomListStore", () => { return null; }); const p = untilEmission(store, LISTS_UPDATE_EVENT); - context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, listIndex, joinCount, roomIndexToRoomId); + context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId); await p; expect(store.getCount(tagId)).toEqual(joinCount); expect(store.orderedLists[tagId]).toEqual(rooms); @@ -265,7 +250,6 @@ describe("SlidingRoomListStore", () => { const roomIdB = "!b:localhost"; const roomIdC = "!c:localhost"; const tagId = DefaultTagID.Favourite; - const listIndex = context.slidingSyncManager.getOrAllocateListIndex(tagId); const joinCount = 10; const roomIndexToRoomId = { // mixed to ensure we sort @@ -273,10 +257,10 @@ describe("SlidingRoomListStore", () => { 2: roomIdC, 0: roomIdA, }; - const roomA = new Room(roomIdA, context.client, context.client.getUserId()); - const roomB = new Room(roomIdB, context.client, context.client.getUserId()); - const roomC = new Room(roomIdC, context.client, context.client.getUserId()); - mocked(context.client.getRoom).mockImplementation((roomId: string) => { + const roomA = new Room(roomIdA, context.client!, context.client!.getUserId()); + const roomB = new Room(roomIdB, context.client!, context.client!.getUserId()); + const roomC = new Room(roomIdC, context.client!, context.client!.getUserId()); + mocked(context.client!.getRoom).mockImplementation((roomId: string) => { switch (roomId) { case roomIdA: return roomA; @@ -287,8 +271,8 @@ describe("SlidingRoomListStore", () => { } return null; }); - mocked(context._SlidingSyncManager.slidingSync.getListData).mockImplementation((i: number) => { - if (i !== listIndex) { + mocked(context._SlidingSyncManager!.slidingSync.getListData).mockImplementation((key: string) => { + if (key !== tagId) { return null; } return { @@ -297,7 +281,7 @@ describe("SlidingRoomListStore", () => { }; }); let p = untilEmission(store, LISTS_UPDATE_EVENT); - context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, listIndex, joinCount, roomIndexToRoomId); + context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId); await p; expect(store.orderedLists[tagId]).toEqual([roomA, roomB, roomC]); @@ -310,7 +294,7 @@ describe("SlidingRoomListStore", () => { roomIndexToRoomId[1] = roomIdA; roomIndexToRoomId[2] = roomIdB; p = untilEmission(store, LISTS_UPDATE_EVENT); - context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, listIndex, joinCount, roomIndexToRoomId); + context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId); await p; // check that B didn't move and that A was put below B @@ -323,4 +307,43 @@ describe("SlidingRoomListStore", () => { await p; expect(store.orderedLists[tagId].map((r) => r.roomId)).toEqual([roomC, roomA, roomB].map((r) => r.roomId)); }); + + it("gracefully handles unknown room IDs", async () => { + await store.start(); + const roomIdA = "!a:localhost"; + const roomIdB = "!b:localhost"; // does not exist + const roomIdC = "!c:localhost"; + const roomIndexToRoomId = { + 0: roomIdA, + 1: roomIdB, // does not exist + 2: roomIdC, + }; + const tagId = DefaultTagID.Favourite; + const joinCount = 10; + // seed the store with 2 rooms + const roomA = new Room(roomIdA, context.client!, context.client!.getUserId()); + const roomC = new Room(roomIdC, context.client!, context.client!.getUserId()); + mocked(context.client!.getRoom).mockImplementation((roomId: string) => { + switch (roomId) { + case roomIdA: + return roomA; + case roomIdC: + return roomC; + } + return null; + }); + mocked(context._SlidingSyncManager!.slidingSync.getListData).mockImplementation((key: string) => { + if (key !== tagId) { + return null; + } + return { + roomIndexToRoomId: roomIndexToRoomId, + joinedCount: joinCount, + }; + }); + const p = untilEmission(store, LISTS_UPDATE_EVENT); + context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId); + await p; + expect(store.orderedLists[tagId]).toEqual([roomA, roomC]); + }); });