Migrate batch of tests to vitest (#34108)
* Migrate utils/dm tests to vitest * Migrate utils/room tests to vitest * Migrate utils/location tests to vitest * Fix types * Satisfy tsc & knip
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import { EventType, KNOWN_SAFE_ROOM_VERSION, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { createTestClient } from "test-utils";
|
||||
|
||||
import { canEncryptToAllUsers } from "../../createRoom";
|
||||
import { type LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../models/LocalRoom";
|
||||
import { DirectoryMember, type Member, ThreepidMember } from "../direct-messages";
|
||||
import { createDmLocalRoom } from "./createDmLocalRoom";
|
||||
import { privateShouldBeEncrypted } from "../rooms";
|
||||
|
||||
vi.mock("../rooms", () => ({
|
||||
privateShouldBeEncrypted: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../createRoom", () => ({
|
||||
canEncryptToAllUsers: vi.fn(),
|
||||
}));
|
||||
|
||||
function assertLocalRoom(room: LocalRoom, targets: Member[], encrypted: boolean) {
|
||||
expect(room.roomId).toBe(LOCAL_ROOM_ID_PREFIX + "t1");
|
||||
expect(room.name).toBe(targets.length ? targets[0].name : "Empty Room");
|
||||
expect(room.encrypted).toBe(encrypted);
|
||||
expect(room.targets).toEqual(targets);
|
||||
expect(room.getMyMembership()).toBe(KnownMembership.Join);
|
||||
|
||||
const roomCreateEvent = room.currentState.getStateEvents(EventType.RoomCreate)[0];
|
||||
expect(roomCreateEvent).toBeDefined();
|
||||
expect(roomCreateEvent.getContent()["room_version"]).toBe(KNOWN_SAFE_ROOM_VERSION);
|
||||
|
||||
// check that the user and all targets are joined
|
||||
expect(room.getMember("@userId:matrix.org")?.membership).toBe(KnownMembership.Join);
|
||||
targets.forEach((target: Member) => {
|
||||
expect(room.getMember(target.userId)?.membership).toBe(KnownMembership.Join);
|
||||
});
|
||||
|
||||
if (encrypted) {
|
||||
const encryptionEvent = room.currentState.getStateEvents(EventType.RoomEncryption)[0];
|
||||
expect(encryptionEvent).toBeDefined();
|
||||
}
|
||||
}
|
||||
|
||||
describe("createDmLocalRoom", () => {
|
||||
let mockClient: MatrixClient;
|
||||
const userId1 = "@user1:example.com";
|
||||
const member1 = new DirectoryMember({ user_id: userId1 });
|
||||
const member2 = new ThreepidMember("user2");
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = createTestClient();
|
||||
});
|
||||
|
||||
describe("when rooms should be encrypted", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(privateShouldBeEncrypted).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should create an encrytped room for 3PID targets", async () => {
|
||||
const room = await createDmLocalRoom(mockClient, [member2]);
|
||||
expect(mockClient.store.storeRoom).toHaveBeenCalledWith(room);
|
||||
assertLocalRoom(room, [member2], true);
|
||||
});
|
||||
|
||||
describe("for MXID targets with encryption available", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(canEncryptToAllUsers).mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("should create an encrypted room", async () => {
|
||||
const room = await createDmLocalRoom(mockClient, [member1]);
|
||||
expect(mockClient.store.storeRoom).toHaveBeenCalledWith(room);
|
||||
assertLocalRoom(room, [member1], true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("for MXID targets with encryption unavailable", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(canEncryptToAllUsers).mockResolvedValue(false);
|
||||
});
|
||||
|
||||
it("should create an unencrypted room", async () => {
|
||||
const room = await createDmLocalRoom(mockClient, [member1]);
|
||||
expect(mockClient.store.storeRoom).toHaveBeenCalledWith(room);
|
||||
assertLocalRoom(room, [member1], false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("if rooms should not be encrypted", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(privateShouldBeEncrypted).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should create an unencrypted room", async () => {
|
||||
const room = await createDmLocalRoom(mockClient, [member1]);
|
||||
assertLocalRoom(room, [member1], false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
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 { describe, it, expect } from "vitest";
|
||||
|
||||
import { filterValidMDirect } from "./filterValidMDirect";
|
||||
|
||||
const roomId1 = "!room1:example.com";
|
||||
const roomId2 = "!room2:example.com";
|
||||
const userId1 = "@user1:example.com";
|
||||
const userId2 = "@user2:example.com";
|
||||
const userId3 = "@user3:example.com";
|
||||
|
||||
describe("filterValidMDirect", () => {
|
||||
it("should return an empty object as valid content", () => {
|
||||
expect(filterValidMDirect({})).toEqual({
|
||||
valid: true,
|
||||
filteredContent: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return valid content", () => {
|
||||
expect(
|
||||
filterValidMDirect({
|
||||
[userId1]: [roomId1, roomId2],
|
||||
[userId2]: [roomId1],
|
||||
}),
|
||||
).toEqual({
|
||||
valid: true,
|
||||
filteredContent: {
|
||||
[userId1]: [roomId1, roomId2],
|
||||
[userId2]: [roomId1],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return an empy object for null", () => {
|
||||
expect(filterValidMDirect(null)).toEqual({
|
||||
valid: false,
|
||||
filteredContent: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return an empy object for a non-object", () => {
|
||||
expect(filterValidMDirect(23)).toEqual({
|
||||
valid: false,
|
||||
filteredContent: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("should only return valid content", () => {
|
||||
const invalidContent = {
|
||||
[userId1]: [23],
|
||||
[userId2]: [roomId2],
|
||||
[userId3]: "room1",
|
||||
};
|
||||
|
||||
expect(filterValidMDirect(invalidContent)).toEqual({
|
||||
valid: false,
|
||||
filteredContent: {
|
||||
[userId1]: [],
|
||||
[userId2]: [roomId2],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import { type MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { createTestClient, makeMembershipEvent, mkThirdPartyInviteEvent } from "test-utils";
|
||||
|
||||
import DMRoomMap from "../DMRoomMap";
|
||||
import { LocalRoom } from "../../models/LocalRoom";
|
||||
import { findDMForUser } from "./findDMForUser";
|
||||
import { getFunctionalMembers } from "../room/getFunctionalMembers";
|
||||
|
||||
vi.mock("../room/getFunctionalMembers", () => ({
|
||||
getFunctionalMembers: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("findDMForUser", () => {
|
||||
const userId1 = "@user1:example.com";
|
||||
const userId2 = "@user2:example.com";
|
||||
const userId3 = "@user3:example.com";
|
||||
const botId = "@bot:example.com";
|
||||
const thirdPartyId = "party@example.com";
|
||||
let room1: Room;
|
||||
let room2: LocalRoom;
|
||||
let room3: Room;
|
||||
let room4: Room;
|
||||
let room5: Room;
|
||||
let room6: Room;
|
||||
let room7: Room;
|
||||
const unknownRoomId = "!unknown:example.com";
|
||||
let dmRoomMap: DMRoomMap;
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = createTestClient();
|
||||
|
||||
// always return the bot user as functional member
|
||||
vi.mocked(getFunctionalMembers).mockReturnValue([botId]);
|
||||
|
||||
room1 = new Room("!room1:example.com", mockClient, userId1);
|
||||
room1.getMyMembership = () => KnownMembership.Join;
|
||||
room1.currentState.setStateEvents([
|
||||
makeMembershipEvent(room1.roomId, userId1, KnownMembership.Join),
|
||||
makeMembershipEvent(room1.roomId, userId2, KnownMembership.Join),
|
||||
]);
|
||||
|
||||
// this should not be a DM room because it is a local room
|
||||
room2 = new LocalRoom("!room2:example.com", mockClient, userId1);
|
||||
room2.getMyMembership = () => KnownMembership.Join;
|
||||
room2.getLastActiveTimestamp = () => 100;
|
||||
|
||||
room3 = new Room("!room3:example.com", mockClient, userId1);
|
||||
room3.getMyMembership = () => KnownMembership.Join;
|
||||
room3.currentState.setStateEvents([
|
||||
makeMembershipEvent(room3.roomId, userId1, KnownMembership.Join),
|
||||
makeMembershipEvent(room3.roomId, userId2, KnownMembership.Join),
|
||||
// Adding the bot user here. Should be excluded when determining if the room is a DM.
|
||||
makeMembershipEvent(room3.roomId, botId, KnownMembership.Join),
|
||||
]);
|
||||
|
||||
// this should not be a DM room because it has only one joined user
|
||||
room4 = new Room("!room4:example.com", mockClient, userId1);
|
||||
room4.getMyMembership = () => KnownMembership.Join;
|
||||
room4.currentState.setStateEvents([
|
||||
makeMembershipEvent(room4.roomId, userId1, KnownMembership.Invite),
|
||||
makeMembershipEvent(room4.roomId, userId2, KnownMembership.Join),
|
||||
]);
|
||||
|
||||
// this should not be a DM room because it has no users
|
||||
room5 = new Room("!room5:example.com", mockClient, userId1);
|
||||
room5.getLastActiveTimestamp = () => 100;
|
||||
|
||||
// room not correctly stored in userId → room map; should be found by the "all rooms" fallback
|
||||
room6 = new Room("!room6:example.com", mockClient, userId1);
|
||||
room6.getMyMembership = () => KnownMembership.Join;
|
||||
room6.currentState.setStateEvents([
|
||||
makeMembershipEvent(room6.roomId, userId1, KnownMembership.Join),
|
||||
makeMembershipEvent(room6.roomId, userId3, KnownMembership.Join),
|
||||
]);
|
||||
|
||||
// room with pending third-party invite
|
||||
room7 = new Room("!room7:example.com", mockClient, userId1);
|
||||
room7.getMyMembership = () => KnownMembership.Join;
|
||||
room7.currentState.setStateEvents([
|
||||
makeMembershipEvent(room7.roomId, userId1, KnownMembership.Join),
|
||||
mkThirdPartyInviteEvent(thirdPartyId, "third-party", room7.roomId),
|
||||
]);
|
||||
|
||||
vi.mocked(mockClient.getRoom).mockImplementation((roomId?: string) => {
|
||||
return (
|
||||
{
|
||||
[room1.roomId]: room1,
|
||||
[room2.roomId]: room2,
|
||||
[room3.roomId]: room3,
|
||||
[room4.roomId]: room4,
|
||||
[room5.roomId]: room5,
|
||||
[room6.roomId]: room6,
|
||||
[room7.roomId]: room7,
|
||||
}[roomId!] || null
|
||||
);
|
||||
});
|
||||
|
||||
dmRoomMap = {
|
||||
getDMRoomForIdentifiers: vi.fn(),
|
||||
getDMRoomsForUserId: vi.fn(),
|
||||
getRoomIds: vi.fn().mockReturnValue(
|
||||
new Set([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
room5.roomId,
|
||||
room6.roomId,
|
||||
room7.roomId,
|
||||
unknownRoomId, // this room does not exist in client
|
||||
]),
|
||||
),
|
||||
} as unknown as DMRoomMap;
|
||||
vi.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
vi.mocked(dmRoomMap.getDMRoomsForUserId).mockImplementation((userId: string) => {
|
||||
if (userId === userId1) {
|
||||
return [room1.roomId, room2.roomId, room3.roomId, room4.roomId, room5.roomId, unknownRoomId];
|
||||
}
|
||||
|
||||
if (userId === thirdPartyId) {
|
||||
return [room7.roomId];
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
});
|
||||
|
||||
describe("for an empty DM room list", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(dmRoomMap.getDMRoomsForUserId).mockReturnValue([]);
|
||||
vi.mocked(dmRoomMap.getRoomIds).mockReturnValue(new Set());
|
||||
});
|
||||
|
||||
it("should return undefined", () => {
|
||||
expect(findDMForUser(mockClient, userId1)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should find a room ordered by last activity 1", () => {
|
||||
room1.getLastActiveTimestamp = () => 2;
|
||||
room3.getLastActiveTimestamp = () => 1;
|
||||
|
||||
expect(findDMForUser(mockClient, userId1)).toBe(room1);
|
||||
});
|
||||
|
||||
it("should find a room ordered by last activity 2", () => {
|
||||
room1.getLastActiveTimestamp = () => 1;
|
||||
room3.getLastActiveTimestamp = () => 2;
|
||||
|
||||
expect(findDMForUser(mockClient, userId1)).toBe(room3);
|
||||
});
|
||||
|
||||
it("should find a room by the 'all rooms' fallback", () => {
|
||||
room1.getLastActiveTimestamp = () => 1;
|
||||
room6.getLastActiveTimestamp = () => 2;
|
||||
|
||||
expect(findDMForUser(mockClient, userId3)).toBe(room6);
|
||||
});
|
||||
|
||||
it("should find a room with a pending third-party invite", () => {
|
||||
expect(findDMForUser(mockClient, thirdPartyId)).toBe(room7);
|
||||
});
|
||||
|
||||
it("should not find a room for an unknown Id", () => {
|
||||
expect(findDMForUser(mockClient, "@unknown:example.com")).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import { type MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { createTestClient } from "test-utils";
|
||||
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import { DirectoryMember, ThreepidMember } from "../direct-messages";
|
||||
import { findDMForUser } from "./findDMForUser";
|
||||
import { findDMRoom } from "./findDMRoom";
|
||||
import DMRoomMap from "../DMRoomMap";
|
||||
|
||||
vi.mock("../dm/findDMForUser", () => ({
|
||||
findDMForUser: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("findDMRoom", () => {
|
||||
const userId1 = "@user1:example.com";
|
||||
const member1 = new DirectoryMember({ user_id: userId1 });
|
||||
const member2 = new ThreepidMember("user2");
|
||||
let mockClient: MatrixClient;
|
||||
let room1: Room;
|
||||
let dmRoomMap: DMRoomMap;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = createTestClient();
|
||||
vi.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
room1 = new Room("!room1:example.com", mockClient, userId1);
|
||||
|
||||
dmRoomMap = {
|
||||
getDMRoomForIdentifiers: vi.fn(),
|
||||
getDMRoomsForUserId: vi.fn(),
|
||||
} as unknown as DMRoomMap;
|
||||
vi.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
});
|
||||
|
||||
it("should return the room for a single target with a room", () => {
|
||||
vi.mocked(findDMForUser).mockReturnValue(room1);
|
||||
expect(findDMRoom(mockClient, [member1])).toBe(room1);
|
||||
});
|
||||
|
||||
it("should return undefined for a single target without a room", () => {
|
||||
vi.mocked(findDMForUser).mockReturnValue(undefined);
|
||||
expect(findDMRoom(mockClient, [member1])).toBeNull();
|
||||
});
|
||||
|
||||
it("should return the room for 2 targets with a room", () => {
|
||||
vi.mocked(dmRoomMap.getDMRoomForIdentifiers).mockReturnValue(room1);
|
||||
expect(findDMRoom(mockClient, [member1, member2])).toBe(room1);
|
||||
});
|
||||
|
||||
it("should return null for 2 targets without a room", () => {
|
||||
vi.mocked(dmRoomMap.getDMRoomForIdentifiers).mockReturnValue(null);
|
||||
expect(findDMRoom(mockClient, [member1, member2])).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
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 { describe, it, expect } from "vitest";
|
||||
import {
|
||||
M_TEXT,
|
||||
type ILocationContent,
|
||||
type LocationAssetType,
|
||||
M_ASSET,
|
||||
M_LOCATION,
|
||||
M_TIMESTAMP,
|
||||
ContentHelpers,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { isSelfLocation } from "./isSelfLocation";
|
||||
|
||||
describe("isSelfLocation", () => {
|
||||
it("Returns true for a full m.asset event", () => {
|
||||
const content = ContentHelpers.makeLocationContent("", "0", Date.now());
|
||||
expect(isSelfLocation(content)).toBe(true);
|
||||
});
|
||||
|
||||
it("Returns true for a missing m.asset", () => {
|
||||
const content = {
|
||||
body: "",
|
||||
msgtype: "m.location",
|
||||
geo_uri: "",
|
||||
[M_LOCATION.name]: { uri: "" },
|
||||
[M_TEXT.name]: "",
|
||||
[M_TIMESTAMP.name]: 0,
|
||||
// Note: no m.asset!
|
||||
} as unknown as ILocationContent;
|
||||
expect(isSelfLocation(content)).toBe(true);
|
||||
});
|
||||
|
||||
it("Returns true for a missing m.asset type", () => {
|
||||
const content = {
|
||||
body: "",
|
||||
msgtype: "m.location",
|
||||
geo_uri: "",
|
||||
[M_LOCATION.name]: { uri: "" },
|
||||
[M_TEXT.name]: "",
|
||||
[M_TIMESTAMP.name]: 0,
|
||||
[M_ASSET.name]: {
|
||||
// Note: no type!
|
||||
},
|
||||
} as unknown as ILocationContent;
|
||||
expect(isSelfLocation(content)).toBe(true);
|
||||
});
|
||||
|
||||
it("Returns false for an unknown asset type", () => {
|
||||
const content = ContentHelpers.makeLocationContent(
|
||||
undefined /* text */,
|
||||
"geo:foo",
|
||||
0,
|
||||
undefined /* description */,
|
||||
"org.example.unknown" as unknown as LocationAssetType,
|
||||
);
|
||||
expect(isSelfLocation(content)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { mkMessage, makeLegacyLocationEvent, makeLocationEvent } from "test-utils";
|
||||
|
||||
import { createMapSiteLinkFromEvent } from "./links";
|
||||
|
||||
describe("createMapSiteLinkFromEvent", () => {
|
||||
it("returns null if event does not contain geouri", () => {
|
||||
expect(
|
||||
createMapSiteLinkFromEvent(
|
||||
mkMessage({
|
||||
room: "1",
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
}),
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("returns OpenStreetMap link if event contains m.location with valid uri", () => {
|
||||
expect(createMapSiteLinkFromEvent(makeLocationEvent("geo:51.5076,-0.1276"))).toEqual(
|
||||
"https://www.openstreetmap.org/" + "?mlat=51.5076&mlon=-0.1276" + "#map=16/51.5076/-0.1276",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null if event contains m.location with invalid uri", () => {
|
||||
expect(createMapSiteLinkFromEvent(makeLocationEvent("123 Sesame St"))).toBeNull();
|
||||
});
|
||||
|
||||
it("returns OpenStreetMap link if event contains geo_uri", () => {
|
||||
expect(createMapSiteLinkFromEvent(makeLegacyLocationEvent("geo:51.5076,-0.1276"))).toEqual(
|
||||
"https://www.openstreetmap.org/" + "?mlat=51.5076&mlon=-0.1276" + "#map=16/51.5076/-0.1276",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null if event contains an invalid geo_uri", () => {
|
||||
expect(createMapSiteLinkFromEvent(makeLegacyLocationEvent("123 Sesame St"))).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
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 { describe, it, expect } from "vitest";
|
||||
import { makeLegacyLocationEvent, makeLocationEvent } from "test-utils/location";
|
||||
|
||||
import { locationEventGeoUri } from "./locationEventGeoUri";
|
||||
|
||||
describe("locationEventGeoUri()", () => {
|
||||
it("returns m.location uri when available", () => {
|
||||
expect(locationEventGeoUri(makeLocationEvent("geo:51.5076,-0.1276"))).toEqual("geo:51.5076,-0.1276");
|
||||
});
|
||||
|
||||
it("returns legacy uri when m.location content not found", () => {
|
||||
expect(locationEventGeoUri(makeLegacyLocationEvent("geo:51.5076,-0.1276"))).toEqual("geo:51.5076,-0.1276");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
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 { describe, it, expect } from "vitest";
|
||||
|
||||
import { parseGeoUri } from "./parseGeoUri";
|
||||
|
||||
describe("parseGeoUri", () => {
|
||||
it("fails if the supplied URI is empty", () => {
|
||||
expect(parseGeoUri("")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("returns undefined if latitude is not a number", () => {
|
||||
expect(parseGeoUri("geo:ABCD,16.3695,183")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined if longitude is not a number", () => {
|
||||
expect(parseGeoUri("geo:48.2010,EFGH,183")).toBeUndefined();
|
||||
});
|
||||
|
||||
// We use some examples from the spec, but don't check semantics
|
||||
// like two textually-different URIs being equal, since we are
|
||||
// just a humble parser.
|
||||
|
||||
// Note: we do not understand geo URIs with percent-encoded coords
|
||||
// or accuracy. It is RECOMMENDED in the spec never to percent-encode
|
||||
// these, but it is permitted, and we will fail to parse in that case.
|
||||
|
||||
it("rfc5870 6.1 Simple 3-dimensional", () => {
|
||||
expect(parseGeoUri("geo:48.2010,16.3695,183")).toEqual({
|
||||
latitude: 48.201,
|
||||
longitude: 16.3695,
|
||||
altitude: 183,
|
||||
accuracy: undefined,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("rfc5870 6.2 Explicit CRS and accuracy", () => {
|
||||
expect(parseGeoUri("geo:48.198634,16.371648;crs=wgs84;u=40")).toEqual({
|
||||
latitude: 48.198634,
|
||||
longitude: 16.371648,
|
||||
altitude: null,
|
||||
accuracy: 40,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("rfc5870 6.4 Negative longitude and explicit CRS", () => {
|
||||
expect(parseGeoUri("geo:90,-22.43;crs=WGS84")).toEqual({
|
||||
latitude: 90,
|
||||
longitude: -22.43,
|
||||
altitude: null,
|
||||
accuracy: undefined,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("rfc5870 6.4 Integer lat and lon", () => {
|
||||
expect(parseGeoUri("geo:90,46")).toEqual({
|
||||
latitude: 90,
|
||||
longitude: 46,
|
||||
altitude: null,
|
||||
accuracy: undefined,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("rfc5870 6.4 Percent-encoded param value", () => {
|
||||
expect(parseGeoUri("geo:66,30;u=6.500;FOo=this%2dthat")).toEqual({
|
||||
latitude: 66,
|
||||
longitude: 30,
|
||||
altitude: null,
|
||||
accuracy: 6.5,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("rfc5870 6.4 Unknown param", () => {
|
||||
expect(parseGeoUri("geo:66.0,30;u=6.5;foo=this-that>")).toEqual({
|
||||
latitude: 66.0,
|
||||
longitude: 30,
|
||||
altitude: null,
|
||||
accuracy: 6.5,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("rfc5870 6.4 Multiple unknown params", () => {
|
||||
expect(parseGeoUri("geo:70,20;foo=1.00;bar=white")).toEqual({
|
||||
latitude: 70,
|
||||
longitude: 20,
|
||||
altitude: null,
|
||||
accuracy: undefined,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("Negative latitude", () => {
|
||||
expect(parseGeoUri("geo:-7.5,20")).toEqual({
|
||||
latitude: -7.5,
|
||||
longitude: 20,
|
||||
altitude: null,
|
||||
accuracy: undefined,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("Zero altitude is not unknown", () => {
|
||||
expect(parseGeoUri("geo:-7.5,-20,0")).toEqual({
|
||||
latitude: -7.5,
|
||||
longitude: -20,
|
||||
altitude: 0,
|
||||
accuracy: undefined,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
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 { describe, it, expect } from "vitest";
|
||||
|
||||
import { positionFailureMessage } from "./positionFailureMessage";
|
||||
|
||||
describe("positionFailureMessage()", () => {
|
||||
// error codes from GeolocationPositionError
|
||||
// see: https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError
|
||||
// 1: PERMISSION_DENIED
|
||||
// 2: POSITION_UNAVAILABLE
|
||||
// 3: TIMEOUT
|
||||
type TestCase = [number, string | undefined];
|
||||
it.each<TestCase>([
|
||||
[
|
||||
1,
|
||||
"Element was denied permission to fetch your location. Please allow location access in your browser settings.",
|
||||
],
|
||||
[2, "Failed to fetch your location. Please try again later."],
|
||||
[3, "Timed out trying to fetch your location. Please try again later."],
|
||||
[4, "Unknown error fetching location. Please try again later."],
|
||||
[5, undefined],
|
||||
])("returns correct message for error code %s", (code, expectedMessage) => {
|
||||
expect(positionFailureMessage(code)).toEqual(expectedMessage);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { JoinRule, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "test-utils";
|
||||
|
||||
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../settings/UIFeature";
|
||||
import { canInviteTo } from "./canInviteTo";
|
||||
|
||||
vi.mock("../../customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("canInviteTo()", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const userId = "@alice:server.org";
|
||||
const roomId = "!room:server.org";
|
||||
|
||||
const makeRoom = (): Room => {
|
||||
const client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
});
|
||||
const room = new Room(roomId, client, userId);
|
||||
vi.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
|
||||
vi.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
vi.spyOn(room, "canInvite").mockReturnValue(true);
|
||||
return room;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(shouldShowComponent).mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe("when user has permissions to issue an invite for this room", () => {
|
||||
// aka when Room.canInvite is true
|
||||
|
||||
it("should return false when current user membership is not joined", () => {
|
||||
const room = makeRoom();
|
||||
vi.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Invite);
|
||||
|
||||
expect(canInviteTo(room)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false when UIComponent.InviteUsers customisation hides invite", () => {
|
||||
const room = makeRoom();
|
||||
vi.mocked(shouldShowComponent).mockReturnValue(false);
|
||||
|
||||
expect(canInviteTo(room)).toEqual(false);
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.InviteUsers);
|
||||
});
|
||||
|
||||
it("should return true when user can invite and is a room member", () => {
|
||||
const room = makeRoom();
|
||||
|
||||
expect(canInviteTo(room)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when user does not have permissions to issue an invite for this room", () => {
|
||||
// aka when Room.canInvite is false
|
||||
|
||||
it("should return false when room is a private space", () => {
|
||||
const room = makeRoom();
|
||||
vi.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite);
|
||||
vi.spyOn(room, "isSpaceRoom").mockReturnValue(true);
|
||||
vi.spyOn(room, "canInvite").mockReturnValue(false);
|
||||
|
||||
expect(canInviteTo(room)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false when room is just a room", () => {
|
||||
const room = makeRoom();
|
||||
vi.spyOn(room, "canInvite").mockReturnValue(false);
|
||||
|
||||
expect(canInviteTo(room)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true when room is a public space", () => {
|
||||
const room = makeRoom();
|
||||
// default join rule is public
|
||||
vi.spyOn(room, "isSpaceRoom").mockReturnValue(true);
|
||||
vi.spyOn(room, "canInvite").mockReturnValue(false);
|
||||
|
||||
expect(canInviteTo(room)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
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 { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import { type MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { getFunctionalMembers } from "./getFunctionalMembers";
|
||||
import { getJoinedNonFunctionalMembers } from "./getJoinedNonFunctionalMembers";
|
||||
|
||||
vi.mock("./getFunctionalMembers", () => ({
|
||||
getFunctionalMembers: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("getJoinedNonFunctionalMembers", () => {
|
||||
let room: Room;
|
||||
let roomMember1: RoomMember;
|
||||
let roomMember2: RoomMember;
|
||||
|
||||
beforeEach(() => {
|
||||
room = new Room("!room:example.com", {} as unknown as MatrixClient, "@user:example.com");
|
||||
room.getJoinedMembers = vi.fn();
|
||||
|
||||
roomMember1 = new RoomMember(room.roomId, "@user1:example.com");
|
||||
roomMember2 = new RoomMember(room.roomId, "@user2:example.com");
|
||||
});
|
||||
|
||||
describe("if there are no members", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(room.getJoinedMembers).mockReturnValue([]);
|
||||
vi.mocked(getFunctionalMembers).mockReturnValue([]);
|
||||
});
|
||||
|
||||
it("should return an empty list", () => {
|
||||
expect(getJoinedNonFunctionalMembers(room)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("if there are only regular room members", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(room.getJoinedMembers).mockReturnValue([roomMember1, roomMember2]);
|
||||
vi.mocked(getFunctionalMembers).mockReturnValue([]);
|
||||
});
|
||||
|
||||
it("should return the room members", () => {
|
||||
const members = getJoinedNonFunctionalMembers(room);
|
||||
expect(members).toContain(roomMember1);
|
||||
expect(members).toContain(roomMember2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("if there are only functional room members", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(room.getJoinedMembers).mockReturnValue([]);
|
||||
vi.mocked(getFunctionalMembers).mockReturnValue(["@functional:example.com"]);
|
||||
});
|
||||
|
||||
it("should return an empty list", () => {
|
||||
expect(getJoinedNonFunctionalMembers(room)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("if there are some functional room members", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(room.getJoinedMembers).mockReturnValue([roomMember1, roomMember2]);
|
||||
vi.mocked(getFunctionalMembers).mockReturnValue([roomMember1.userId]);
|
||||
});
|
||||
|
||||
it("should only return the non-functional members", () => {
|
||||
const members = getJoinedNonFunctionalMembers(room);
|
||||
expect(members).not.toContain(roomMember1);
|
||||
expect(members).toContain(roomMember2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { Room, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "matrix-js-sdk/src/matrix";
|
||||
import { createTestClient, mkEvent } from "test-utils";
|
||||
|
||||
import { getFunctionalMembers } from "./getFunctionalMembers";
|
||||
|
||||
describe("getRoomFunctionalMembers", () => {
|
||||
const client = createTestClient();
|
||||
const room = new Room("!room:example.com", client, client.getUserId()!);
|
||||
|
||||
it("should return an empty array if no functional members state event exists", () => {
|
||||
expect(getFunctionalMembers(room)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should return an empty array if functional members state event does not have a service_members field", () => {
|
||||
room.currentState.setStateEvents([
|
||||
mkEvent({
|
||||
event: true,
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name,
|
||||
user: "@user:example.com)",
|
||||
room: room.roomId,
|
||||
skey: "",
|
||||
content: {},
|
||||
}),
|
||||
]);
|
||||
expect(getFunctionalMembers(room)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should return service_members field of the functional users state event", () => {
|
||||
room.currentState.setStateEvents([
|
||||
mkEvent({
|
||||
event: true,
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name,
|
||||
user: "@user:example.com)",
|
||||
room: room.roomId,
|
||||
skey: "",
|
||||
content: { service_members: ["@user:example.com"] },
|
||||
}),
|
||||
]);
|
||||
expect(getFunctionalMembers(room)).toEqual(["@user:example.com"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect } from "vitest";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { DefaultTagID } from "../../stores/room-list-v3/skip-list/tag";
|
||||
import { CUSTOM_SECTION_TAG_PREFIX } from "../../stores/room-list-v3/section";
|
||||
import { getSectionTagForRoom } from "./getSectionTagForRoom";
|
||||
import { getTagsForRoom } from "./getTagsForRoom";
|
||||
|
||||
vi.mock("./getTagsForRoom");
|
||||
|
||||
const mockGetTagsForRoom = vi.mocked(getTagsForRoom);
|
||||
|
||||
describe("getSectionTagForRoom", () => {
|
||||
const room = {} as Room;
|
||||
|
||||
it("should return null when room has no tags", () => {
|
||||
mockGetTagsForRoom.mockReturnValue([]);
|
||||
expect(getSectionTagForRoom(room)).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when room only has a non-section tag", () => {
|
||||
mockGetTagsForRoom.mockReturnValue([DefaultTagID.Untagged]);
|
||||
expect(getSectionTagForRoom(room)).toBeNull();
|
||||
});
|
||||
|
||||
it.each([DefaultTagID.Favourite, DefaultTagID.LowPriority, `${CUSTOM_SECTION_TAG_PREFIX}abc-123`])(
|
||||
"should return section tag %s when present",
|
||||
(tag) => {
|
||||
mockGetTagsForRoom.mockReturnValue([tag]);
|
||||
expect(getSectionTagForRoom(room)).toBe(tag);
|
||||
},
|
||||
);
|
||||
|
||||
it("should return the first section tag when multiple are present", () => {
|
||||
const customTag = `${CUSTOM_SECTION_TAG_PREFIX}abc-123`;
|
||||
mockGetTagsForRoom.mockReturnValue([DefaultTagID.Favourite, customTag]);
|
||||
expect(getSectionTagForRoom(room)).toBe(DefaultTagID.Favourite);
|
||||
});
|
||||
|
||||
it("should ignore non-section tags and return the section tag", () => {
|
||||
const customTag = `${CUSTOM_SECTION_TAG_PREFIX}abc-123`;
|
||||
mockGetTagsForRoom.mockReturnValue([DefaultTagID.Untagged, customTag]);
|
||||
expect(getSectionTagForRoom(room)).toBe(customTag);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import { JoinRule, type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { createTestClient, mkRoom } from "test-utils";
|
||||
|
||||
import { DefaultTagID } from "../../stores/room-list-v3/skip-list/tag";
|
||||
import { getTagsForRoom } from "./getTagsForRoom";
|
||||
import DMRoomMap from "../DMRoomMap";
|
||||
|
||||
describe("getTagsForRoom", () => {
|
||||
let client: MatrixClient;
|
||||
let rooms: Room[];
|
||||
|
||||
beforeEach(() => {
|
||||
client = createTestClient();
|
||||
rooms = [];
|
||||
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as DMRoomMap;
|
||||
DMRoomMap.setShared(dmRoomMap);
|
||||
});
|
||||
|
||||
function makeRoom(roomId: string): Room {
|
||||
mkRoom(client, roomId, rooms);
|
||||
vi.mocked(client).getRoom.mockImplementation((id) => rooms.find((r) => r.roomId === id) ?? null);
|
||||
vi.mocked(client).getRooms.mockImplementation(() => rooms);
|
||||
return client.getRoom(roomId)!;
|
||||
}
|
||||
|
||||
it("should return [Invite] for a room where the user is invited", () => {
|
||||
const room = makeRoom("!invited:server");
|
||||
vi.mocked(room.getMyMembership).mockReturnValue(KnownMembership.Invite);
|
||||
|
||||
const tags = getTagsForRoom(room);
|
||||
expect(tags).toEqual([DefaultTagID.Invite]);
|
||||
});
|
||||
|
||||
it.each([KnownMembership.Leave, KnownMembership.Ban])(
|
||||
"should return [Archived] for a room where the user has %s",
|
||||
(membership) => {
|
||||
const room = makeRoom(`!${membership.toLowerCase()}:server`);
|
||||
vi.mocked(room.getMyMembership).mockReturnValue(membership);
|
||||
|
||||
const tags = getTagsForRoom(room);
|
||||
expect(tags).toEqual([DefaultTagID.Archived]);
|
||||
},
|
||||
);
|
||||
|
||||
describe("joined rooms", () => {
|
||||
describe("with no user-defined tags and not a DM", () => {
|
||||
it("should return [Untagged] when the room has no tags and is not a DM", () => {
|
||||
const room = makeRoom("!plain:server");
|
||||
vi.mocked(room.getMyMembership).mockReturnValue(KnownMembership.Join);
|
||||
(room as any).tags = {};
|
||||
|
||||
const tags = getTagsForRoom(room);
|
||||
expect(tags).toEqual([DefaultTagID.Untagged]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return [DM] when the room is a DM", () => {
|
||||
const room = makeRoom("!dm:server");
|
||||
vi.mocked(room.getMyMembership).mockReturnValue(KnownMembership.Join);
|
||||
(room as any).tags = {};
|
||||
|
||||
vi.mocked(DMRoomMap.shared().getUserIdForRoomId).mockReturnValue("@alice:server");
|
||||
|
||||
const tags = getTagsForRoom(room);
|
||||
expect(tags).toContain(DefaultTagID.DM);
|
||||
expect(tags).not.toContain(DefaultTagID.Untagged);
|
||||
});
|
||||
|
||||
describe("rooms with user-defined tags", () => {
|
||||
it("should return the user-defined tags", () => {
|
||||
const room = makeRoom("!tagged:server");
|
||||
vi.mocked(room.getMyMembership).mockReturnValue(KnownMembership.Join);
|
||||
(room as any).tags = { "m.favourite": {}, "u.alice": {} };
|
||||
|
||||
const tags = getTagsForRoom(room);
|
||||
expect(tags).toContain("m.favourite");
|
||||
expect(tags).toContain("u.alice");
|
||||
expect(tags).not.toContain(DefaultTagID.Untagged);
|
||||
});
|
||||
|
||||
it("should not check DM status when user-defined tags are already present", () => {
|
||||
const room = makeRoom("!tagged-dm:server");
|
||||
vi.mocked(room.getMyMembership).mockReturnValue(KnownMembership.Join);
|
||||
(room as any).tags = { "m.lowpriority": {} };
|
||||
|
||||
// Even if the room is a DM, user-defined tags take priority
|
||||
vi.mocked(DMRoomMap.shared().getUserIdForRoomId).mockReturnValue("@alice:server");
|
||||
|
||||
const tags = getTagsForRoom(room);
|
||||
expect(tags).toContain("m.lowpriority");
|
||||
expect(tags).not.toContain(DefaultTagID.DM);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("conference (call) rooms", () => {
|
||||
it.each([JoinRule.Public, JoinRule.Knock])(
|
||||
"should include Conference tag for a call room with %s join rule",
|
||||
(joinRule) => {
|
||||
const room = makeRoom(`!call:${joinRule}:server`);
|
||||
vi.mocked(room.getMyMembership).mockReturnValue(KnownMembership.Join);
|
||||
vi.mocked(room.isCallRoom).mockReturnValue(true);
|
||||
vi.mocked(room.getJoinRule).mockReturnValue(joinRule);
|
||||
|
||||
const tags = getTagsForRoom(room);
|
||||
expect(tags).toContain(DefaultTagID.Conference);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([JoinRule.Invite, JoinRule.Private])(
|
||||
"should not include Conference tag for a call room with %s join rule",
|
||||
(joinRule) => {
|
||||
const room = makeRoom(`!call:${joinRule}:server`);
|
||||
vi.mocked(room.getMyMembership).mockReturnValue(KnownMembership.Join);
|
||||
vi.mocked(room.isCallRoom).mockReturnValue(true);
|
||||
vi.mocked(room.getJoinRule).mockReturnValue(joinRule);
|
||||
|
||||
const tags = getTagsForRoom(room);
|
||||
expect(tags).not.toContain(DefaultTagID.Conference);
|
||||
},
|
||||
);
|
||||
|
||||
it("should include Conference alongside Untagged for a public call room with no other tags", () => {
|
||||
const room = makeRoom("!callPublicPlain:server");
|
||||
vi.mocked(room.getMyMembership).mockReturnValue(KnownMembership.Join);
|
||||
(room as any).tags = {};
|
||||
vi.mocked(room.isCallRoom).mockReturnValue(true);
|
||||
vi.mocked(room.getJoinRule).mockReturnValue(JoinRule.Public);
|
||||
|
||||
const tags = getTagsForRoom(room);
|
||||
// Conference is added to the tag list before the Untagged fallback check,
|
||||
// so tags.length is already 1 — Untagged is not appended.
|
||||
expect(tags).toContain(DefaultTagID.Conference);
|
||||
expect(tags).not.toContain(DefaultTagID.Untagged);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { getMockClientWithEventEmitter } from "test-utils";
|
||||
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { inviteToRoom } from "./inviteToRoom";
|
||||
|
||||
describe("inviteToRoom()", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const roomId = "!room:server.org";
|
||||
|
||||
const makeRoom = (): Room => {
|
||||
const client = getMockClientWithEventEmitter({
|
||||
isGuest: vi.fn(),
|
||||
});
|
||||
const room = new Room(roomId, client, userId);
|
||||
return room;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// stub
|
||||
vi.spyOn(defaultDispatcher, "dispatch").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("requires registration when a guest tries to invite to a room", () => {
|
||||
const room = makeRoom();
|
||||
|
||||
vi.spyOn(room.client, "isGuest").mockReturnValue(true);
|
||||
|
||||
inviteToRoom(room);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "require_registration" });
|
||||
});
|
||||
|
||||
it("opens the room inviter", () => {
|
||||
const room = makeRoom();
|
||||
|
||||
vi.spyOn(room.client, "isGuest").mockReturnValue(false);
|
||||
|
||||
inviteToRoom(room);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "view_invite", roomId });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeAll, beforeEach } from "vitest";
|
||||
import { type MatrixClient, type MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { mkRoomMemberJoinEvent, mkThirdPartyInviteEvent, stubClient } from "test-utils";
|
||||
|
||||
import DMRoomMap from "../DMRoomMap";
|
||||
import { shouldEncryptRoomWithSingle3rdPartyInvite } from "./shouldEncryptRoomWithSingle3rdPartyInvite";
|
||||
import { privateShouldBeEncrypted } from "../rooms";
|
||||
|
||||
vi.mock("../rooms", () => ({
|
||||
privateShouldBeEncrypted: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("shouldEncryptRoomWithSingle3rdPartyInvite", () => {
|
||||
let client: MatrixClient;
|
||||
let thirdPartyInviteEvent: MatrixEvent;
|
||||
let roomWithOneThirdPartyInvite: Room;
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
DMRoomMap.makeShared(client);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
roomWithOneThirdPartyInvite = new Room("!room1:example.com", client, client.getSafeUserId());
|
||||
thirdPartyInviteEvent = mkThirdPartyInviteEvent(
|
||||
client.getSafeUserId(),
|
||||
"user@example.com",
|
||||
roomWithOneThirdPartyInvite.roomId,
|
||||
);
|
||||
|
||||
roomWithOneThirdPartyInvite.currentState.setStateEvents([
|
||||
mkRoomMemberJoinEvent(client.getSafeUserId(), roomWithOneThirdPartyInvite.roomId),
|
||||
thirdPartyInviteEvent,
|
||||
]);
|
||||
vi.spyOn(DMRoomMap.shared(), "getRoomIds").mockReturnValue(new Set([roomWithOneThirdPartyInvite.roomId]));
|
||||
});
|
||||
|
||||
describe("when well-known promotes encryption", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(privateShouldBeEncrypted).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should return true + invite event for a DM room with one third-party invite", () => {
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: true,
|
||||
inviteEvent: thirdPartyInviteEvent,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false for a non-DM room with one third-party invite", () => {
|
||||
vi.mocked(DMRoomMap.shared().getRoomIds).mockReturnValue(new Set());
|
||||
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false for a DM room with two members", () => {
|
||||
roomWithOneThirdPartyInvite.currentState.setStateEvents([
|
||||
mkRoomMemberJoinEvent("@user2:example.com", roomWithOneThirdPartyInvite.roomId),
|
||||
]);
|
||||
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false for a DM room with two third-party invites", () => {
|
||||
roomWithOneThirdPartyInvite.currentState.setStateEvents([
|
||||
mkThirdPartyInviteEvent(
|
||||
client.getSafeUserId(),
|
||||
"user2@example.com",
|
||||
roomWithOneThirdPartyInvite.roomId,
|
||||
),
|
||||
]);
|
||||
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when well-known does not promote encryption", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(privateShouldBeEncrypted).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should return false for a DM room with one third-party invite", () => {
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { createTestClient } from "test-utils";
|
||||
|
||||
import { getMockedRooms } from "../../../test/unit-tests/stores/room-list-v3/skip-list/getMockedRooms";
|
||||
import { DefaultTagID } from "../../stores/room-list-v3/skip-list/tag";
|
||||
import { compareRoomsByRecency, sortRoomsByRecency } from "./sortRoomsByRecency";
|
||||
|
||||
describe("sortRoomsByRecency", () => {
|
||||
let userId: string;
|
||||
let rooms: Room[];
|
||||
|
||||
beforeEach(() => {
|
||||
const client = createTestClient();
|
||||
userId = client.getSafeUserId();
|
||||
rooms = getMockedRooms(client);
|
||||
});
|
||||
|
||||
describe("sortRoomsByRecency", () => {
|
||||
it("sorts an arbitrary list by recency without mutating the input", () => {
|
||||
const input = [rooms[0], rooms[5], rooms[2]];
|
||||
const inputCopy = [...input];
|
||||
|
||||
const sorted = sortRoomsByRecency(input, userId);
|
||||
|
||||
// ts: room5 (6) > room2 (3) > room0 (1)
|
||||
expect(sorted).toEqual([rooms[5], rooms[2], rooms[0]]);
|
||||
// The input array is not mutated.
|
||||
expect(input).toEqual(inputCopy);
|
||||
});
|
||||
|
||||
it("does not move muted or low-priority rooms (pure recency)", () => {
|
||||
const recent = rooms[99]; // highest ts
|
||||
recent.tags = { [DefaultTagID.LowPriority]: { order: 0 } };
|
||||
|
||||
const sorted = sortRoomsByRecency([rooms[0], rooms[50], recent], userId);
|
||||
|
||||
// A pure recency sort keeps the most recent room first even though it is
|
||||
// low priority (the full RecencySorter would sink it).
|
||||
expect(sorted[0]).toBe(recent);
|
||||
});
|
||||
});
|
||||
|
||||
describe("compareRoomsByRecency", () => {
|
||||
it("orders the more recent room first", () => {
|
||||
// rooms[10] (ts 11) is more recent than rooms[3] (ts 4)
|
||||
expect(compareRoomsByRecency(rooms[10], rooms[3], userId)).toBeLessThan(0);
|
||||
expect(compareRoomsByRecency(rooms[3], rooms[10], userId)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { getMockClientWithEventEmitter } from "test-utils";
|
||||
|
||||
import RoomListActions from "../../actions/RoomListActions";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { DefaultTagID, type TagID } from "../../stores/room-list-v3/skip-list/tag";
|
||||
import { CHATS_TAG, CUSTOM_SECTION_TAG_PREFIX } from "../../stores/room-list-v3/section";
|
||||
import { tagRoom } from "./tagRoom";
|
||||
import * as getSectionTagForRoomUtils from "./getSectionTagForRoom";
|
||||
|
||||
describe("tagRoom()", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const roomId = "!room:server.org";
|
||||
const customTag = `${CUSTOM_SECTION_TAG_PREFIX}my-section`;
|
||||
|
||||
const makeRoom = (currentSectionTag: TagID | null = null): Room => {
|
||||
const client = getMockClientWithEventEmitter({
|
||||
isGuest: vi.fn(),
|
||||
});
|
||||
const room = new Room(roomId, client, userId);
|
||||
|
||||
vi.spyOn(getSectionTagForRoomUtils, "getSectionTagForRoom").mockReturnValue(currentSectionTag);
|
||||
|
||||
return room;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// stub
|
||||
vi.spyOn(defaultDispatcher, "dispatch").mockImplementation(() => {});
|
||||
vi.spyOn(RoomListActions, "tagRoom").mockReturnValue({ action: "mocked_tag_room_action", fn: () => {} });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("does nothing when room tag is not allowed", () => {
|
||||
const room = makeRoom();
|
||||
|
||||
tagRoom(room, DefaultTagID.ServerNotice);
|
||||
|
||||
expect(defaultDispatcher.dispatch).not.toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("when a room has no section tag", () => {
|
||||
it("should tag a room as favourite", () => {
|
||||
const room = makeRoom();
|
||||
|
||||
tagRoom(room, DefaultTagID.Favourite);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
null, // remove
|
||||
DefaultTagID.Favourite, // add
|
||||
);
|
||||
});
|
||||
|
||||
it("should tag a room low priority", () => {
|
||||
const room = makeRoom();
|
||||
|
||||
tagRoom(room, DefaultTagID.LowPriority);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
null, // remove
|
||||
DefaultTagID.LowPriority, // add
|
||||
);
|
||||
});
|
||||
|
||||
it("should tag a room with a custom section", () => {
|
||||
const room = makeRoom();
|
||||
|
||||
tagRoom(room, customTag);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
null, // remove
|
||||
customTag, // add
|
||||
);
|
||||
});
|
||||
|
||||
it("should do nothing meaningful when applying CHATS_TAG", () => {
|
||||
const room = makeRoom();
|
||||
|
||||
tagRoom(room, CHATS_TAG);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
null, // remove
|
||||
null, // add
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a room is tagged as favourite", () => {
|
||||
it("should unfavourite a room", () => {
|
||||
const room = makeRoom(DefaultTagID.Favourite);
|
||||
|
||||
tagRoom(room, DefaultTagID.Favourite);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
DefaultTagID.Favourite, // remove
|
||||
null, // add
|
||||
);
|
||||
});
|
||||
|
||||
it("should tag a room low priority", () => {
|
||||
const room = makeRoom(DefaultTagID.Favourite);
|
||||
|
||||
tagRoom(room, DefaultTagID.LowPriority);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
DefaultTagID.Favourite, // remove
|
||||
DefaultTagID.LowPriority, // add
|
||||
);
|
||||
});
|
||||
|
||||
it("should remove the favourite tag when applying CHATS_TAG", () => {
|
||||
const room = makeRoom(DefaultTagID.Favourite);
|
||||
|
||||
tagRoom(room, CHATS_TAG);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
DefaultTagID.Favourite, // remove
|
||||
null, // add
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a room is tagged as low priority", () => {
|
||||
it("should favourite a room", () => {
|
||||
const room = makeRoom(DefaultTagID.LowPriority);
|
||||
|
||||
tagRoom(room, DefaultTagID.Favourite);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
DefaultTagID.LowPriority, // remove
|
||||
DefaultTagID.Favourite, // add
|
||||
);
|
||||
});
|
||||
|
||||
it("should untag a room low priority", () => {
|
||||
const room = makeRoom(DefaultTagID.LowPriority);
|
||||
|
||||
tagRoom(room, DefaultTagID.LowPriority);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
DefaultTagID.LowPriority, // remove
|
||||
null, // add
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a room is tagged with a custom section", () => {
|
||||
const otherCustomTag = `${CUSTOM_SECTION_TAG_PREFIX}other-section`;
|
||||
|
||||
it.each([
|
||||
{ label: "untag the custom section", applyTag: customTag, expectedAdd: null },
|
||||
{ label: "replace with favourite", applyTag: DefaultTagID.Favourite, expectedAdd: DefaultTagID.Favourite },
|
||||
{ label: "replace with another custom section", applyTag: otherCustomTag, expectedAdd: otherCustomTag },
|
||||
{ label: "remove section tag when applying CHATS_TAG", applyTag: CHATS_TAG, expectedAdd: null },
|
||||
])("should $label", ({ applyTag, expectedAdd }) => {
|
||||
const room = makeRoom(customTag);
|
||||
|
||||
tagRoom(room, applyTag);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
customTag, // remove
|
||||
expectedAdd, // add
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user