mirror of https://github.com/vector-im/riot-web
Unit test list ordering algorithms (#10682)
* unit test ImportanceAlgorithm * strict fixes * unit test NaturalAlgorithmt3chguy/dedup-icons-17oct
parent
8783021e53
commit
6c36a2400d
|
@ -0,0 +1,298 @@
|
|||
/*
|
||||
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 { MatrixEvent, Room, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore";
|
||||
import { ImportanceAlgorithm } from "../../../../../src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm";
|
||||
import { SortAlgorithm } from "../../../../../src/stores/room-list/algorithms/models";
|
||||
import * as RoomNotifs from "../../../../../src/RoomNotifs";
|
||||
import { DefaultTagID, RoomUpdateCause } from "../../../../../src/stores/room-list/models";
|
||||
import { NotificationColor } from "../../../../../src/stores/notifications/NotificationColor";
|
||||
import { AlphabeticAlgorithm } from "../../../../../src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils";
|
||||
|
||||
describe("ImportanceAlgorithm", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const tagId = DefaultTagID.Favourite;
|
||||
|
||||
const makeRoom = (id: string, name: string, order?: number): Room => {
|
||||
const room = new Room(id, client, userId);
|
||||
room.name = name;
|
||||
const tagEvent = new MatrixEvent({
|
||||
type: "m.tag",
|
||||
content: {
|
||||
tags: {
|
||||
[tagId]: {
|
||||
order,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
room.addTags(tagEvent);
|
||||
return room;
|
||||
};
|
||||
|
||||
const client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
});
|
||||
const roomA = makeRoom("!aaa:server.org", "Alpha", 2);
|
||||
const roomB = makeRoom("!bbb:server.org", "Bravo", 5);
|
||||
const roomC = makeRoom("!ccc:server.org", "Charlie", 1);
|
||||
const roomD = makeRoom("!ddd:server.org", "Delta", 4);
|
||||
const roomE = makeRoom("!eee:server.org", "Echo", 3);
|
||||
const roomX = makeRoom("!xxx:server.org", "Xylophone", 99);
|
||||
|
||||
const unreadStates: Record<string, ReturnType<(typeof RoomNotifs)["determineUnreadState"]>> = {
|
||||
red: { symbol: null, count: 1, color: NotificationColor.Red },
|
||||
grey: { symbol: null, count: 1, color: NotificationColor.Grey },
|
||||
none: { symbol: null, count: 0, color: NotificationColor.None },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(RoomNotifs, "determineUnreadState").mockReturnValue({
|
||||
symbol: null,
|
||||
count: 0,
|
||||
color: NotificationColor.None,
|
||||
});
|
||||
});
|
||||
|
||||
const setupAlgorithm = (sortAlgorithm: SortAlgorithm, rooms?: Room[]) => {
|
||||
const algorithm = new ImportanceAlgorithm(tagId, sortAlgorithm);
|
||||
algorithm.setRooms(rooms || [roomA, roomB, roomC]);
|
||||
return algorithm;
|
||||
};
|
||||
|
||||
describe("When sortAlgorithm is manual", () => {
|
||||
const sortAlgorithm = SortAlgorithm.Manual;
|
||||
it("orders rooms by tag order without categorizing", () => {
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState");
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
// didn't check notif state
|
||||
expect(RoomNotificationStateStore.instance.getRoomState).not.toHaveBeenCalled();
|
||||
// sorted according to room tag order
|
||||
expect(algorithm.orderedRooms).toEqual([roomC, roomA, roomB]);
|
||||
});
|
||||
|
||||
describe("handleRoomUpdate", () => {
|
||||
// XXX: This doesn't work because manual ordered rooms dont get categoryindices
|
||||
// possibly related https://github.com/vector-im/element-web/issues/25099
|
||||
it.skip("removes a room", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(true);
|
||||
expect(algorithm.orderedRooms).toEqual([roomC, roomB]);
|
||||
});
|
||||
|
||||
// XXX: This doesn't work because manual ordered rooms dont get categoryindices
|
||||
it.skip("adds a new room", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.NewRoom);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(true);
|
||||
expect(algorithm.orderedRooms).toEqual([roomC, roomB, roomD, roomE]);
|
||||
});
|
||||
|
||||
it("does nothing and returns false for a timeline update", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
const beforeRooms = algorithm.orderedRooms;
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.Timeline);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(false);
|
||||
// strict equal
|
||||
expect(algorithm.orderedRooms).toBe(beforeRooms);
|
||||
});
|
||||
|
||||
it("does nothing and returns false for a read receipt update", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
const beforeRooms = algorithm.orderedRooms;
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.ReadReceipt);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(false);
|
||||
// strict equal
|
||||
expect(algorithm.orderedRooms).toBe(beforeRooms);
|
||||
});
|
||||
|
||||
it("throws for an unhandle update cause", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
expect(() =>
|
||||
algorithm.handleRoomUpdate(roomA, "something unexpected" as unknown as RoomUpdateCause),
|
||||
).toThrow("Unsupported update cause: something unexpected");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("When sortAlgorithm is alphabetical", () => {
|
||||
const sortAlgorithm = SortAlgorithm.Alphabetic;
|
||||
|
||||
beforeEach(async () => {
|
||||
// destroy roomMap so we can start fresh
|
||||
// @ts-ignore private property
|
||||
RoomNotificationStateStore.instance.roomMap = new Map<Room, RoomNotificationState>();
|
||||
|
||||
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
|
||||
jest.spyOn(RoomNotifs, "determineUnreadState")
|
||||
.mockClear()
|
||||
.mockImplementation((room) => {
|
||||
switch (room) {
|
||||
// b and e have red notifs
|
||||
case roomB:
|
||||
case roomE:
|
||||
return unreadStates.red;
|
||||
// c is grey
|
||||
case roomC:
|
||||
return unreadStates.grey;
|
||||
default:
|
||||
return unreadStates.none;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("orders rooms by alpha when they have the same notif state", () => {
|
||||
jest.spyOn(RoomNotifs, "determineUnreadState").mockReturnValue({
|
||||
symbol: null,
|
||||
count: 0,
|
||||
color: NotificationColor.None,
|
||||
});
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
// sorted according to alpha
|
||||
expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]);
|
||||
});
|
||||
|
||||
it("orders rooms by notification state then alpha", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]);
|
||||
|
||||
expect(algorithm.orderedRooms).toEqual([
|
||||
// alpha within red
|
||||
roomB,
|
||||
roomE,
|
||||
// grey
|
||||
roomC,
|
||||
// alpha within none
|
||||
roomA,
|
||||
roomD,
|
||||
]);
|
||||
});
|
||||
|
||||
describe("handleRoomUpdate", () => {
|
||||
it("removes a room", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
|
||||
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(true);
|
||||
expect(algorithm.orderedRooms).toEqual([roomB, roomC]);
|
||||
// no re-sorting on a remove
|
||||
expect(AlphabeticAlgorithm.prototype.sortRooms).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("warns and returns without change when removing a room that is not indexed", () => {
|
||||
jest.spyOn(logger, "warn").mockReturnValue(undefined);
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.RoomRemoved);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(`Tried to remove unknown room from ${tagId}: ${roomD.roomId}`);
|
||||
});
|
||||
|
||||
it("adds a new room", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
|
||||
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.NewRoom);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(true);
|
||||
// inserted according to notif state
|
||||
expect(algorithm.orderedRooms).toEqual([roomB, roomE, roomC, roomA]);
|
||||
// only sorted within category
|
||||
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomB, roomE], tagId);
|
||||
});
|
||||
|
||||
it("throws for an unhandled update cause", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
expect(() =>
|
||||
algorithm.handleRoomUpdate(roomA, "something unexpected" as unknown as RoomUpdateCause),
|
||||
).toThrow("Unsupported update cause: something unexpected");
|
||||
});
|
||||
|
||||
describe("time and read receipt updates", () => {
|
||||
it("throws for when a room is not indexed", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]);
|
||||
|
||||
expect(() => algorithm.handleRoomUpdate(roomX, RoomUpdateCause.Timeline)).toThrow(
|
||||
`Room ${roomX.roomId} has no index in ${tagId}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("re-sorts category when updated room has not changed category", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]);
|
||||
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
|
||||
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.Timeline);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(true);
|
||||
expect(algorithm.orderedRooms).toEqual([roomB, roomE, roomC, roomA, roomD]);
|
||||
// only sorted within category
|
||||
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1);
|
||||
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomB, roomE], tagId);
|
||||
});
|
||||
|
||||
it("re-sorts category when updated room has changed category", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]);
|
||||
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
|
||||
|
||||
// change roomE to unreadState.none
|
||||
jest.spyOn(RoomNotifs, "determineUnreadState").mockImplementation((room) => {
|
||||
switch (room) {
|
||||
// b and e have red notifs
|
||||
case roomB:
|
||||
return unreadStates.red;
|
||||
// c is grey
|
||||
case roomC:
|
||||
return unreadStates.grey;
|
||||
case roomE:
|
||||
default:
|
||||
return unreadStates.none;
|
||||
}
|
||||
});
|
||||
// @ts-ignore don't bother mocking rest of emit properties
|
||||
roomE.emit(RoomEvent.Timeline, new MatrixEvent({ type: "whatever", room_id: roomE.roomId }));
|
||||
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.Timeline);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(true);
|
||||
expect(algorithm.orderedRooms).toEqual([roomB, roomC, roomA, roomD, roomE]);
|
||||
|
||||
// only sorted within roomE's new category
|
||||
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1);
|
||||
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomA, roomD, roomE], tagId);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
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 { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { NaturalAlgorithm } from "../../../../../src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm";
|
||||
import { SortAlgorithm } from "../../../../../src/stores/room-list/algorithms/models";
|
||||
import { DefaultTagID, RoomUpdateCause } from "../../../../../src/stores/room-list/models";
|
||||
import { AlphabeticAlgorithm } from "../../../../../src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils";
|
||||
|
||||
describe("NaturalAlgorithm", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const tagId = DefaultTagID.Favourite;
|
||||
|
||||
const makeRoom = (id: string, name: string): Room => {
|
||||
const room = new Room(id, client, userId);
|
||||
room.name = name;
|
||||
return room;
|
||||
};
|
||||
|
||||
const client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
});
|
||||
const roomA = makeRoom("!aaa:server.org", "Alpha");
|
||||
const roomB = makeRoom("!bbb:server.org", "Bravo");
|
||||
const roomC = makeRoom("!ccc:server.org", "Charlie");
|
||||
const roomD = makeRoom("!ddd:server.org", "Delta");
|
||||
const roomE = makeRoom("!eee:server.org", "Echo");
|
||||
const roomX = makeRoom("!xxx:server.org", "Xylophone");
|
||||
|
||||
const setupAlgorithm = (sortAlgorithm: SortAlgorithm, rooms?: Room[]) => {
|
||||
const algorithm = new NaturalAlgorithm(tagId, sortAlgorithm);
|
||||
algorithm.setRooms(rooms || [roomA, roomB, roomC]);
|
||||
return algorithm;
|
||||
};
|
||||
|
||||
describe("When sortAlgorithm is alphabetical", () => {
|
||||
const sortAlgorithm = SortAlgorithm.Alphabetic;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
|
||||
});
|
||||
|
||||
it("orders rooms by alpha", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
// sorted according to alpha
|
||||
expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]);
|
||||
});
|
||||
|
||||
describe("handleRoomUpdate", () => {
|
||||
it("removes a room", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
|
||||
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(true);
|
||||
expect(algorithm.orderedRooms).toEqual([roomB, roomC]);
|
||||
});
|
||||
|
||||
it("warns when removing a room that is not indexed", () => {
|
||||
jest.spyOn(logger, "warn").mockReturnValue(undefined);
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.RoomRemoved);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(true);
|
||||
expect(logger.warn).toHaveBeenCalledWith(`Tried to remove unknown room from ${tagId}: ${roomD.roomId}`);
|
||||
});
|
||||
|
||||
it("adds a new room", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
|
||||
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.NewRoom);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(true);
|
||||
expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC, roomE]);
|
||||
// only sorted within category
|
||||
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith(
|
||||
[roomA, roomB, roomC, roomE],
|
||||
tagId,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws for an unhandled update cause", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
expect(() =>
|
||||
algorithm.handleRoomUpdate(roomA, "something unexpected" as unknown as RoomUpdateCause),
|
||||
).toThrow("Unsupported update cause: something unexpected");
|
||||
});
|
||||
|
||||
describe("time and read receipt updates", () => {
|
||||
it("handles when a room is not indexed", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomX, RoomUpdateCause.Timeline);
|
||||
|
||||
// for better or worse natural alg sets this to true
|
||||
expect(shouldTriggerUpdate).toBe(true);
|
||||
expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]);
|
||||
});
|
||||
|
||||
it("re-sorts rooms when timeline updates", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
|
||||
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.Timeline);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(true);
|
||||
expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]);
|
||||
// only sorted within category
|
||||
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1);
|
||||
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomA, roomB, roomC], tagId);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue