Simplified Sliding Sync (#28515)
* Experimental SSS Working branch to get SSS functional on element-web. Requires https://github.com/matrix-org/matrix-js-sdk/pull/4400 * Adjust tests to use new behaviour * Remove well-known proxy URL lookup; always use native This is actually required for SSS because otherwise it would use the proxy over native support. * Linting * Debug logging * Control the race condition when swapping between rooms * Dont' filter by space as synapse doesn't support it * Remove SS code related to registering lists and managing ranges - Update the spidering code to spider all the relevant lists. - Add canonical alias to the required_state to allow room name calcs to work. Room sort order is busted because we don't yet look at `bump_stamp`. * User bumpStamp if it is present * Drop initial room load from 20 per list to 10 * Half the batch size to trickle more quickly * Prettier * prettier on tests too * Remove proxy URL & unused import * Hopefully fix tests to assert what the behaviour is supposed to be * Move the singleton to the manager tyo fix import loop * Very well, code, I will remove you Why were you there in the first place? * Strip out more unused stuff * Fix playwright test Seems like this lack of order updating unless a room is selected was just always a bug with both regular and non-sliding sync. I have no idea how the test passed on develop because it won't run. * Fix test to do maybe what it was supposed to do... possibly? * Remove test for old pre-simplified sliding sync behaviour * Unused import * Remove sliding sync proxy & test I was wrong about what this test was asserting, it was suposed to assert that notification dots aren't shown (because SS didn't support them somehow I guess) but they are fine in SSS so the test is just no longer relevant. * Remove now pointless credentials * Remove subscription removal as SSS doesn't do that * Update tests * add test * Switch to new labs flag & break if old labs flag is enabled * Remove unused import & fix test * Fix other test * Remove name & description from old labs flag as they're not displayed anywhere so not useful * Remove old sliding sync option by making it not a feature * Add back unread nindicator test but inverted and minus the bit about disabling notification which surely would have defeated the original point anyway? * Reinstate test for room_subscriptions ...and also make tests actually use sliding sync * Use UserFriendlyError * Remove empty constructor * Remove unrelated changes * Unused import * Fix import * Avoid moving import --------- Co-authored-by: Kegan Dougal <7190048+kegsay@users.noreply.github.com>
This commit is contained in:
@@ -661,6 +661,7 @@ export function mkStubRoom(
|
||||
getUnreadNotificationCount: jest.fn(() => 0),
|
||||
getRoomUnreadNotificationCount: jest.fn().mockReturnValue(0),
|
||||
getVersion: jest.fn().mockReturnValue("1"),
|
||||
getBumpStamp: jest.fn().mockReturnValue(0),
|
||||
hasMembershipState: () => false,
|
||||
isElementVideoRoom: jest.fn().mockReturnValue(false),
|
||||
isSpaceRoom: jest.fn().mockReturnValue(false),
|
||||
|
||||
@@ -6,18 +6,31 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { SlidingSync } from "matrix-js-sdk/src/sliding-sync";
|
||||
import { type SlidingSync, SlidingSyncEvent, SlidingSyncState } from "matrix-js-sdk/src/sliding-sync";
|
||||
import { mocked } from "jest-mock";
|
||||
import { type MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { ClientEvent, type MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import fetchMockJest from "fetch-mock-jest";
|
||||
import EventEmitter from "events";
|
||||
import { waitFor } from "jest-matrix-react";
|
||||
|
||||
import { SlidingSyncManager } from "../../src/SlidingSyncManager";
|
||||
import { stubClient } from "../test-utils";
|
||||
import SlidingSyncController from "../../src/settings/controllers/SlidingSyncController";
|
||||
import SettingsStore from "../../src/settings/SettingsStore";
|
||||
import { mkStubRoom, stubClient } from "../test-utils";
|
||||
|
||||
jest.mock("matrix-js-sdk/src/sliding-sync");
|
||||
const MockSlidingSync = <jest.Mock<SlidingSync>>(<unknown>SlidingSync);
|
||||
class MockSlidingSync extends EventEmitter {
|
||||
lists = {};
|
||||
listModifiedCount = 0;
|
||||
terminated = false;
|
||||
needsResend = false;
|
||||
modifyRoomSubscriptions = jest.fn();
|
||||
getRoomSubscriptions = jest.fn();
|
||||
useCustomSubscription = jest.fn();
|
||||
getListParams = jest.fn();
|
||||
setList = jest.fn();
|
||||
setListRanges = jest.fn();
|
||||
getListData = jest.fn();
|
||||
extensions = jest.fn();
|
||||
desiredRoomSubscriptions = jest.fn();
|
||||
}
|
||||
|
||||
describe("SlidingSyncManager", () => {
|
||||
let manager: SlidingSyncManager;
|
||||
@@ -25,12 +38,12 @@ describe("SlidingSyncManager", () => {
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
slidingSync = new MockSlidingSync();
|
||||
slidingSync = new MockSlidingSync() as unknown as SlidingSync;
|
||||
manager = new SlidingSyncManager();
|
||||
client = stubClient();
|
||||
// by default the client has no rooms: stubClient magically makes rooms annoyingly.
|
||||
mocked(client.getRoom).mockReturnValue(null);
|
||||
manager.configure(client, "invalid");
|
||||
(manager as any).configure(client, "invalid");
|
||||
manager.slidingSync = slidingSync;
|
||||
fetchMockJest.reset();
|
||||
fetchMockJest.get("https://proxy/client/server.json", {});
|
||||
@@ -39,12 +52,13 @@ describe("SlidingSyncManager", () => {
|
||||
describe("setRoomVisible", () => {
|
||||
it("adds a subscription for the room", async () => {
|
||||
const roomId = "!room:id";
|
||||
mocked(client.getRoom).mockReturnValue(mkStubRoom(roomId, "foo", client));
|
||||
const subs = new Set<string>();
|
||||
mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs);
|
||||
mocked(slidingSync.modifyRoomSubscriptions).mockResolvedValue("yep");
|
||||
await manager.setRoomVisible(roomId, true);
|
||||
await manager.setRoomVisible(roomId);
|
||||
expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set<string>([roomId]));
|
||||
});
|
||||
|
||||
it("adds a custom subscription for a lazy-loadable room", async () => {
|
||||
const roomId = "!lazy:id";
|
||||
const room = new Room(roomId, client, client.getUserId()!);
|
||||
@@ -67,19 +81,37 @@ describe("SlidingSyncManager", () => {
|
||||
});
|
||||
const subs = new Set<string>();
|
||||
mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs);
|
||||
mocked(slidingSync.modifyRoomSubscriptions).mockResolvedValue("yep");
|
||||
await manager.setRoomVisible(roomId, true);
|
||||
await manager.setRoomVisible(roomId);
|
||||
expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set<string>([roomId]));
|
||||
// we aren't prescriptive about what the sub name is.
|
||||
expect(slidingSync.useCustomSubscription).toHaveBeenCalledWith(roomId, expect.anything());
|
||||
});
|
||||
|
||||
it("waits if the room is not yet known", async () => {
|
||||
const roomId = "!room:id";
|
||||
mocked(client.getRoom).mockReturnValue(null);
|
||||
const subs = new Set<string>();
|
||||
mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs);
|
||||
|
||||
const setVisibleDone = jest.fn();
|
||||
manager.setRoomVisible(roomId).then(setVisibleDone);
|
||||
|
||||
await waitFor(() => expect(client.getRoom).toHaveBeenCalledWith(roomId));
|
||||
|
||||
expect(setVisibleDone).not.toHaveBeenCalled();
|
||||
|
||||
const stubRoom = mkStubRoom(roomId, "foo", client);
|
||||
mocked(client.getRoom).mockReturnValue(stubRoom);
|
||||
client.emit(ClientEvent.Room, stubRoom);
|
||||
|
||||
await waitFor(() => expect(setVisibleDone).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
|
||||
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"],
|
||||
});
|
||||
@@ -96,7 +128,6 @@ describe("SlidingSyncManager", () => {
|
||||
mocked(slidingSync.getListParams).mockReturnValue({
|
||||
ranges: [[0, 42]],
|
||||
});
|
||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
||||
await manager.ensureListRegistered(listKey, {
|
||||
sort: ["by_recency"],
|
||||
});
|
||||
@@ -114,7 +145,6 @@ describe("SlidingSyncManager", () => {
|
||||
mocked(slidingSync.getListParams).mockReturnValue({
|
||||
ranges: [[0, 42]],
|
||||
});
|
||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
||||
await manager.ensureListRegistered(listKey, {
|
||||
ranges: [[0, 52]],
|
||||
});
|
||||
@@ -128,7 +158,6 @@ describe("SlidingSyncManager", () => {
|
||||
ranges: [[0, 42]],
|
||||
sort: ["by_recency"],
|
||||
});
|
||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
||||
await manager.ensureListRegistered(listKey, {
|
||||
ranges: [[0, 42]],
|
||||
sort: ["by_recency"],
|
||||
@@ -139,183 +168,77 @@ describe("SlidingSyncManager", () => {
|
||||
});
|
||||
|
||||
describe("startSpidering", () => {
|
||||
it("requests in batchSizes", async () => {
|
||||
it("requests in expanding batchSizes", async () => {
|
||||
const gapMs = 1;
|
||||
const batchSize = 10;
|
||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
||||
mocked(slidingSync.setListRanges).mockResolvedValue("yep");
|
||||
mocked(slidingSync.getListData).mockImplementation((key) => {
|
||||
return {
|
||||
joinedCount: 64,
|
||||
roomIndexToRoomId: {},
|
||||
};
|
||||
});
|
||||
await manager.startSpidering(batchSize, gapMs);
|
||||
await (manager as any).startSpidering(slidingSync, batchSize, gapMs);
|
||||
|
||||
// we expect calls for 10,19 -> 20,29 -> 30,39 -> 40,49 -> 50,59 -> 60,69
|
||||
const wantWindows = [
|
||||
[10, 19],
|
||||
[20, 29],
|
||||
[30, 39],
|
||||
[40, 49],
|
||||
[50, 59],
|
||||
[60, 69],
|
||||
[0, 10],
|
||||
[0, 20],
|
||||
[0, 30],
|
||||
[0, 40],
|
||||
[0, 50],
|
||||
[0, 60],
|
||||
[0, 70],
|
||||
];
|
||||
expect(slidingSync.getListData).toHaveBeenCalledTimes(wantWindows.length);
|
||||
expect(slidingSync.setList).toHaveBeenCalledTimes(1);
|
||||
expect(slidingSync.setListRanges).toHaveBeenCalledTimes(wantWindows.length - 1);
|
||||
wantWindows.forEach((range, i) => {
|
||||
if (i === 0) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(slidingSync.setList).toHaveBeenCalledWith(
|
||||
SlidingSyncManager.ListSearch,
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect.objectContaining({
|
||||
ranges: [[0, batchSize - 1], range],
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
expect(slidingSync.setListRanges).toHaveBeenCalledWith(SlidingSyncManager.ListSearch, [
|
||||
[0, batchSize - 1],
|
||||
range,
|
||||
]);
|
||||
});
|
||||
|
||||
for (let i = 1; i < wantWindows.length; ++i) {
|
||||
// each time we emit, it should expand the range of all 5 lists by 10 until
|
||||
// they all include all the rooms (64), which is 6 emits.
|
||||
slidingSync.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, null, undefined);
|
||||
await waitFor(() => expect(slidingSync.getListData).toHaveBeenCalledTimes(i * 5));
|
||||
expect(slidingSync.setListRanges).toHaveBeenCalledTimes(i * 5);
|
||||
expect(slidingSync.setListRanges).toHaveBeenCalledWith("spaces", [wantWindows[i]]);
|
||||
}
|
||||
});
|
||||
it("handles accounts with zero rooms", async () => {
|
||||
const gapMs = 1;
|
||||
const batchSize = 10;
|
||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
||||
mocked(slidingSync.getListData).mockImplementation((key) => {
|
||||
return {
|
||||
joinedCount: 0,
|
||||
roomIndexToRoomId: {},
|
||||
};
|
||||
});
|
||||
await manager.startSpidering(batchSize, gapMs);
|
||||
expect(slidingSync.getListData).toHaveBeenCalledTimes(1);
|
||||
expect(slidingSync.setList).toHaveBeenCalledTimes(1);
|
||||
expect(slidingSync.setList).toHaveBeenCalledWith(
|
||||
SlidingSyncManager.ListSearch,
|
||||
expect.objectContaining({
|
||||
ranges: [
|
||||
[0, batchSize - 1],
|
||||
[batchSize, batchSize + batchSize - 1],
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
it("continues even when setList rejects", async () => {
|
||||
const gapMs = 1;
|
||||
const batchSize = 10;
|
||||
mocked(slidingSync.setList).mockRejectedValue("narp");
|
||||
mocked(slidingSync.getListData).mockImplementation((key) => {
|
||||
return {
|
||||
joinedCount: 0,
|
||||
roomIndexToRoomId: {},
|
||||
};
|
||||
});
|
||||
await manager.startSpidering(batchSize, gapMs);
|
||||
expect(slidingSync.getListData).toHaveBeenCalledTimes(1);
|
||||
expect(slidingSync.setList).toHaveBeenCalledTimes(1);
|
||||
expect(slidingSync.setList).toHaveBeenCalledWith(
|
||||
SlidingSyncManager.ListSearch,
|
||||
expect.objectContaining({
|
||||
ranges: [
|
||||
[0, batchSize - 1],
|
||||
[batchSize, batchSize + batchSize - 1],
|
||||
],
|
||||
}),
|
||||
);
|
||||
await (manager as any).startSpidering(slidingSync, batchSize, gapMs);
|
||||
slidingSync.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, null, undefined);
|
||||
await waitFor(() => expect(slidingSync.getListData).toHaveBeenCalledTimes(5));
|
||||
// should not have needed to expand the range
|
||||
expect(slidingSync.setListRanges).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe("checkSupport", () => {
|
||||
beforeEach(() => {
|
||||
SlidingSyncController.serverSupportsSlidingSync = false;
|
||||
jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/");
|
||||
SlidingSyncManager.serverSupportsSlidingSync = false;
|
||||
});
|
||||
it("shorts out if the server has 'native' sliding sync support", async () => {
|
||||
jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(true);
|
||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy();
|
||||
expect(SlidingSyncManager.serverSupportsSlidingSync).toBeFalsy();
|
||||
await manager.checkSupport(client);
|
||||
expect(manager.getProxyFromWellKnown).not.toHaveBeenCalled(); // We return earlier
|
||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy();
|
||||
});
|
||||
it("tries to find a sliding sync proxy url from the client well-known if there's no 'native' support", async () => {
|
||||
jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(false);
|
||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy();
|
||||
await manager.checkSupport(client);
|
||||
expect(manager.getProxyFromWellKnown).toHaveBeenCalled();
|
||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy();
|
||||
});
|
||||
it("should query well-known on server_name not baseUrl", async () => {
|
||||
fetchMockJest.get("https://matrix.org/.well-known/matrix/client", {
|
||||
"m.homeserver": {
|
||||
base_url: "https://matrix-client.matrix.org",
|
||||
server: "matrix.org",
|
||||
},
|
||||
"org.matrix.msc3575.proxy": {
|
||||
url: "https://proxy/",
|
||||
},
|
||||
});
|
||||
fetchMockJest.get("https://matrix-client.matrix.org/_matrix/client/versions", { versions: ["v1.4"] });
|
||||
|
||||
mocked(manager.getProxyFromWellKnown).mockRestore();
|
||||
jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(false);
|
||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy();
|
||||
await manager.checkSupport(client);
|
||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy();
|
||||
expect(fetchMockJest).not.toHaveFetched("https://matrix-client.matrix.org/.well-known/matrix/client");
|
||||
});
|
||||
});
|
||||
describe("nativeSlidingSyncSupport", () => {
|
||||
beforeEach(() => {
|
||||
SlidingSyncController.serverSupportsSlidingSync = false;
|
||||
});
|
||||
it("should make an OPTIONS request to avoid unintended side effects", async () => {
|
||||
// See https://github.com/element-hq/element-web/issues/27426
|
||||
|
||||
const unstableSpy = jest
|
||||
.spyOn(client, "doesServerSupportUnstableFeature")
|
||||
.mockImplementation(async (feature: string) => {
|
||||
expect(feature).toBe("org.matrix.msc3575");
|
||||
return true;
|
||||
});
|
||||
const proxySpy = jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/");
|
||||
|
||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy();
|
||||
await manager.checkSupport(client); // first thing it does is call nativeSlidingSyncSupport
|
||||
expect(proxySpy).not.toHaveBeenCalled();
|
||||
expect(unstableSpy).toHaveBeenCalled();
|
||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy();
|
||||
expect(SlidingSyncManager.serverSupportsSlidingSync).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe("setup", () => {
|
||||
let untypedManager: any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(manager, "configure");
|
||||
jest.spyOn(manager, "startSpidering");
|
||||
untypedManager = manager;
|
||||
jest.spyOn(untypedManager, "configure");
|
||||
jest.spyOn(untypedManager, "startSpidering");
|
||||
});
|
||||
it("uses the baseUrl as a proxy if no proxy is set in the client well-known and the server has no native support", async () => {
|
||||
it("uses the baseUrl", async () => {
|
||||
await manager.setup(client);
|
||||
expect(manager.configure).toHaveBeenCalled();
|
||||
expect(manager.configure).toHaveBeenCalledWith(client, client.baseUrl);
|
||||
expect(manager.startSpidering).toHaveBeenCalled();
|
||||
});
|
||||
it("uses the proxy declared in the client well-known", async () => {
|
||||
jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/");
|
||||
await manager.setup(client);
|
||||
expect(manager.configure).toHaveBeenCalled();
|
||||
expect(manager.configure).toHaveBeenCalledWith(client, "https://proxy/");
|
||||
expect(manager.startSpidering).toHaveBeenCalled();
|
||||
});
|
||||
it("uses the legacy `feature_sliding_sync_proxy_url` if it was set", async () => {
|
||||
jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/");
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => {
|
||||
if (name === "feature_sliding_sync_proxy_url") return "legacy-proxy";
|
||||
});
|
||||
await manager.setup(client);
|
||||
expect(manager.configure).toHaveBeenCalled();
|
||||
expect(manager.configure).toHaveBeenCalledWith(client, "legacy-proxy");
|
||||
expect(manager.startSpidering).toHaveBeenCalled();
|
||||
expect(untypedManager.configure).toHaveBeenCalled();
|
||||
expect(untypedManager.configure).toHaveBeenCalledWith(client, client.baseUrl);
|
||||
expect(untypedManager.startSpidering).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { mocked } from "jest-mock";
|
||||
|
||||
import RoomListStoreV3 from "../../../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import { mkStubRoom } from "../../../../test-utils";
|
||||
import { LISTS_UPDATE_EVENT } from "../../../../../src/stores/room-list/SlidingRoomListStore";
|
||||
import { LISTS_UPDATE_EVENT } from "../../../../../src/stores/room-list/RoomListStore";
|
||||
import { useRoomListViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||
import { FilterKey } from "../../../../../src/stores/room-list-v3/skip-list/filters";
|
||||
import { SecondaryFilters } from "../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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 { waitFor, renderHook, act } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { type SlidingSync } from "matrix-js-sdk/src/sliding-sync";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { useSlidingSyncRoomSearch } from "../../../src/hooks/useSlidingSyncRoomSearch";
|
||||
import { MockEventEmitter, stubClient } from "../../test-utils";
|
||||
import { SlidingSyncManager } from "../../../src/SlidingSyncManager";
|
||||
|
||||
describe("useSlidingSyncRoomSearch", () => {
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
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
|
||||
const { result } = renderHook(() => useSlidingSyncRoomSearch());
|
||||
const query = {
|
||||
limit: 10,
|
||||
query: "foo",
|
||||
};
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.rooms).toEqual([]);
|
||||
|
||||
// run the query
|
||||
act(() => {
|
||||
result.current.search(query);
|
||||
});
|
||||
|
||||
// wait for loading to finish
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
// now we expect there to be rooms
|
||||
expect(result.current.rooms).toEqual([roomA, roomB]);
|
||||
|
||||
// run the query again
|
||||
act(() => {
|
||||
result.current.search(query);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -161,7 +161,7 @@ describe("MemberListStore", () => {
|
||||
describe("sliding sync", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, value) => {
|
||||
return settingName === "feature_sliding_sync"; // this is enabled, everything else is disabled.
|
||||
return settingName === "feature_simplified_sliding_sync"; // this is enabled, everything else is disabled.
|
||||
});
|
||||
client.members = jest.fn();
|
||||
});
|
||||
|
||||
@@ -383,43 +383,35 @@ describe("RoomViewStore", function () {
|
||||
describe("Sliding Sync", function () {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, value) => {
|
||||
return settingName === "feature_sliding_sync"; // this is enabled, everything else is disabled.
|
||||
return settingName === "feature_simplified_sliding_sync"; // this is enabled, everything else is disabled.
|
||||
});
|
||||
});
|
||||
|
||||
it("subscribes to the room", async () => {
|
||||
const setRoomVisible = jest
|
||||
.spyOn(slidingSyncManager, "setRoomVisible")
|
||||
.mockReturnValue(Promise.resolve(""));
|
||||
const setRoomVisible = jest.spyOn(slidingSyncManager, "setRoomVisible").mockReturnValue(Promise.resolve());
|
||||
const subscribedRoomId = "!sub1:localhost";
|
||||
dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId });
|
||||
await untilDispatch(Action.ActiveRoomChanged, dis);
|
||||
expect(roomViewStore.getRoomId()).toBe(subscribedRoomId);
|
||||
expect(setRoomVisible).toHaveBeenCalledWith(subscribedRoomId, true);
|
||||
expect(setRoomVisible).toHaveBeenCalledWith(subscribedRoomId);
|
||||
});
|
||||
|
||||
// Regression test for an in-the-wild bug where rooms would rapidly switch forever in sliding sync mode
|
||||
// Previously a regression test for an in-the-wild bug where rooms would rapidly switch forever in sliding sync mode
|
||||
// although that was before the complexity was removed with similified mode. I've removed the complexity but kept the
|
||||
// test anyway.
|
||||
it("doesn't get stuck in a loop if you view rooms quickly", async () => {
|
||||
const setRoomVisible = jest
|
||||
.spyOn(slidingSyncManager, "setRoomVisible")
|
||||
.mockReturnValue(Promise.resolve(""));
|
||||
const setRoomVisible = jest.spyOn(slidingSyncManager, "setRoomVisible").mockReturnValue(Promise.resolve());
|
||||
const subscribedRoomId = "!sub1:localhost";
|
||||
const subscribedRoomId2 = "!sub2:localhost";
|
||||
dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId }, true);
|
||||
dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId2 }, true);
|
||||
await untilDispatch(Action.ActiveRoomChanged, dis);
|
||||
// sub(1) then unsub(1) sub(2), unsub(1)
|
||||
const wantCalls = [
|
||||
[subscribedRoomId, true],
|
||||
[subscribedRoomId, false],
|
||||
[subscribedRoomId2, true],
|
||||
[subscribedRoomId, false],
|
||||
];
|
||||
// should view 1, then 2
|
||||
const wantCalls = [[subscribedRoomId], [subscribedRoomId2]];
|
||||
expect(setRoomVisible).toHaveBeenCalledTimes(wantCalls.length);
|
||||
wantCalls.forEach((v, i) => {
|
||||
try {
|
||||
expect(setRoomVisible.mock.calls[i][0]).toEqual(v[0]);
|
||||
expect(setRoomVisible.mock.calls[i][1]).toEqual(v[1]);
|
||||
} catch {
|
||||
throw new Error(`i=${i} got ${setRoomVisible.mock.calls[i]} want ${v}`);
|
||||
}
|
||||
|
||||
@@ -1,341 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 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 { mocked } from "jest-mock";
|
||||
import { type SlidingSync, SlidingSyncEvent } from "matrix-js-sdk/src/sliding-sync";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
LISTS_UPDATE_EVENT,
|
||||
SlidingRoomListStoreClass,
|
||||
SlidingSyncSortToFilter,
|
||||
} from "../../../../src/stores/room-list/SlidingRoomListStore";
|
||||
import { type SpaceStoreClass } from "../../../../src/stores/spaces/SpaceStore";
|
||||
import { MockEventEmitter, stubClient, untilEmission } from "../../../test-utils";
|
||||
import { TestSdkContext } from "../../TestSdkContext";
|
||||
import { SlidingSyncManager } from "../../../../src/SlidingSyncManager";
|
||||
import { type RoomViewStore } from "../../../../src/stores/RoomViewStore";
|
||||
import { MatrixDispatcher } from "../../../../src/dispatcher/dispatcher";
|
||||
import { SortAlgorithm } from "../../../../src/stores/room-list/algorithms/models";
|
||||
import { DefaultTagID, type TagID } from "../../../../src/stores/room-list/models";
|
||||
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";
|
||||
|
||||
jest.mock("../../../../src/SlidingSyncManager");
|
||||
const MockSlidingSyncManager = <jest.Mock<SlidingSyncManager>>(<unknown>SlidingSyncManager);
|
||||
|
||||
describe("SlidingRoomListStore", () => {
|
||||
let store: SlidingRoomListStoreClass;
|
||||
let context: TestSdkContext;
|
||||
let dis: MatrixDispatcher;
|
||||
let activeSpace: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
context = new TestSdkContext();
|
||||
context.client = stubClient();
|
||||
context._SpaceStore = new MockEventEmitter<SpaceStoreClass>({
|
||||
traverseSpace: jest.fn(),
|
||||
get activeSpace() {
|
||||
return activeSpace;
|
||||
},
|
||||
}) as SpaceStoreClass;
|
||||
context._SlidingSyncManager = new MockSlidingSyncManager();
|
||||
context._SlidingSyncManager.slidingSync = mocked(
|
||||
new MockEventEmitter({
|
||||
getListData: jest.fn(),
|
||||
}) as unknown as SlidingSync,
|
||||
);
|
||||
context._RoomViewStore = mocked(
|
||||
new MockEventEmitter({
|
||||
getRoomId: jest.fn(),
|
||||
}) as unknown as RoomViewStore,
|
||||
);
|
||||
mocked(context._SlidingSyncManager.ensureListRegistered).mockResolvedValue({
|
||||
ranges: [[0, 10]],
|
||||
});
|
||||
|
||||
dis = new MatrixDispatcher();
|
||||
store = new SlidingRoomListStoreClass(dis, context);
|
||||
});
|
||||
|
||||
describe("spaces", () => {
|
||||
it("alters 'filters.spaces' on the DefaultTagID.Untagged list when the selected space changes", async () => {
|
||||
await store.start(); // call onReady
|
||||
const spaceRoomId = "!foo:bar";
|
||||
|
||||
const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => {
|
||||
return listName === DefaultTagID.Untagged && !isLoading;
|
||||
});
|
||||
|
||||
// change the active space
|
||||
activeSpace = spaceRoomId;
|
||||
context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
|
||||
await p;
|
||||
|
||||
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 () => {
|
||||
// change the active space before we are ready
|
||||
const spaceRoomId = "!foo2:bar";
|
||||
activeSpace = spaceRoomId;
|
||||
const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => {
|
||||
return listName === DefaultTagID.Untagged && !isLoading;
|
||||
});
|
||||
await store.start(); // call onReady
|
||||
await p;
|
||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(
|
||||
DefaultTagID.Untagged,
|
||||
expect.objectContaining({
|
||||
filters: expect.objectContaining({
|
||||
spaces: [spaceRoomId],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("includes subspaces in 'filters.spaces' when the selected space has subspaces", async () => {
|
||||
await store.start(); // call onReady
|
||||
const spaceRoomId = "!foo:bar";
|
||||
const subSpace1 = "!ss1:bar";
|
||||
const subSpace2 = "!ss2:bar";
|
||||
|
||||
const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => {
|
||||
return listName === DefaultTagID.Untagged && !isLoading;
|
||||
});
|
||||
|
||||
mocked(context._SpaceStore!.traverseSpace).mockImplementation(
|
||||
(spaceId: string, fn: (roomId: string) => void) => {
|
||||
if (spaceId === spaceRoomId) {
|
||||
fn(subSpace1);
|
||||
fn(subSpace2);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// change the active space
|
||||
activeSpace = spaceRoomId;
|
||||
context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
|
||||
await p;
|
||||
|
||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, {
|
||||
filters: expect.objectContaining({
|
||||
spaces: [spaceRoomId, subSpace1, subSpace2],
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("setTagSorting alters the 'sort' option in the list", async () => {
|
||||
const tagId: TagID = "foo";
|
||||
await store.setTagSorting(tagId, SortAlgorithm.Alphabetic);
|
||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(tagId, {
|
||||
sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic],
|
||||
});
|
||||
expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Alphabetic);
|
||||
|
||||
await store.setTagSorting(tagId, SortAlgorithm.Recent);
|
||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(tagId, {
|
||||
sort: SlidingSyncSortToFilter[SortAlgorithm.Recent],
|
||||
});
|
||||
expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Recent);
|
||||
});
|
||||
|
||||
it("getTagsForRoom gets the tags for the room", async () => {
|
||||
await store.start();
|
||||
const roomA = "!a:localhost";
|
||||
const roomB = "!b:localhost";
|
||||
const keyToListData: Record<string, { joinedCount: number; roomIndexToRoomId: Record<number, string> }> = {
|
||||
[DefaultTagID.Untagged]: {
|
||||
joinedCount: 10,
|
||||
roomIndexToRoomId: {
|
||||
0: roomA,
|
||||
1: roomB,
|
||||
},
|
||||
},
|
||||
[DefaultTagID.Favourite]: {
|
||||
joinedCount: 2,
|
||||
roomIndexToRoomId: {
|
||||
0: roomB,
|
||||
},
|
||||
},
|
||||
};
|
||||
mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => {
|
||||
return keyToListData[key] || null;
|
||||
});
|
||||
|
||||
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([
|
||||
DefaultTagID.Favourite,
|
||||
DefaultTagID.Untagged,
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits LISTS_UPDATE_EVENT when slidingSync lists update", async () => {
|
||||
await store.start();
|
||||
const roomA = "!a:localhost";
|
||||
const roomB = "!b:localhost";
|
||||
const roomC = "!c:localhost";
|
||||
const tagId = DefaultTagID.Favourite;
|
||||
const joinCount = 10;
|
||||
const roomIndexToRoomId = {
|
||||
// mixed to ensure we sort
|
||||
1: roomB,
|
||||
2: roomC,
|
||||
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()!),
|
||||
];
|
||||
mocked(context.client!.getRoom).mockImplementation((roomId: string) => {
|
||||
switch (roomId) {
|
||||
case roomA:
|
||||
return rooms[0];
|
||||
case roomB:
|
||||
return rooms[1];
|
||||
case roomC:
|
||||
return rooms[2];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const p = untilEmission(store, LISTS_UPDATE_EVENT);
|
||||
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
|
||||
await p;
|
||||
expect(store.getCount(tagId)).toEqual(joinCount);
|
||||
expect(store.orderedLists[tagId]).toEqual(rooms);
|
||||
});
|
||||
|
||||
it("sets the sticky room on the basis of the viewed room in RoomViewStore", async () => {
|
||||
await store.start();
|
||||
// seed the store with 3 rooms
|
||||
const roomIdA = "!a:localhost";
|
||||
const roomIdB = "!b:localhost";
|
||||
const roomIdC = "!c:localhost";
|
||||
const tagId = DefaultTagID.Favourite;
|
||||
const joinCount = 10;
|
||||
const roomIndexToRoomId = {
|
||||
// mixed to ensure we sort
|
||||
1: roomIdB,
|
||||
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) => {
|
||||
switch (roomId) {
|
||||
case roomIdA:
|
||||
return roomA;
|
||||
case roomIdB:
|
||||
return roomB;
|
||||
case roomIdC:
|
||||
return roomC;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => {
|
||||
if (key !== tagId) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
roomIndexToRoomId: roomIndexToRoomId,
|
||||
joinedCount: joinCount,
|
||||
};
|
||||
});
|
||||
let p = untilEmission(store, LISTS_UPDATE_EVENT);
|
||||
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
|
||||
await p;
|
||||
expect(store.orderedLists[tagId]).toEqual([roomA, roomB, roomC]);
|
||||
|
||||
// make roomB sticky and inform the store
|
||||
mocked(context.roomViewStore.getRoomId).mockReturnValue(roomIdB);
|
||||
context.roomViewStore.emit(UPDATE_EVENT);
|
||||
|
||||
// bump room C to the top, room B should not move from i=1 despite the list update saying to
|
||||
roomIndexToRoomId[0] = roomIdC;
|
||||
roomIndexToRoomId[1] = roomIdA;
|
||||
roomIndexToRoomId[2] = roomIdB;
|
||||
p = untilEmission(store, LISTS_UPDATE_EVENT);
|
||||
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
|
||||
await p;
|
||||
|
||||
// check that B didn't move and that A was put below B
|
||||
expect(store.orderedLists[tagId]).toEqual([roomC, roomB, roomA]);
|
||||
|
||||
// make room C sticky: rooms should move as a result, without needing an additional list update
|
||||
mocked(context.roomViewStore.getRoomId).mockReturnValue(roomIdC);
|
||||
p = untilEmission(store, LISTS_UPDATE_EVENT);
|
||||
context.roomViewStore.emit(UPDATE_EVENT);
|
||||
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]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user