Unit test list ordering algorithms (#10682)

* unit test ImportanceAlgorithm

* strict fixes

* unit test NaturalAlgorithm
t3chguy/dedup-icons-17oct
Kerry 2023-04-26 08:59:50 +12:00 committed by GitHub
parent 8783021e53
commit 6c36a2400d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 434 additions and 0 deletions

View File

@ -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);
});
});
});
});
});

View File

@ -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);
});
});
});
});
});