Migrate more jest tests to vitest (#33922)

* Migrate more jest tests to vitest

* Fix jest config

* Fix jest config

* Make remaining jest tests type-happy

* Iterate

* Fix Notifier import cycle

* Fix tests

* Delint

* Iterate

* Handle SDKContextClass `client` initialisation internally

Rather than via MatrixChat - this is predominantly for Lifecycle tests as they don't use a MatrixChat and it doesn't make much sense for this component to own this state.

* Fix tests

* Iterate

* Simplify diff

* Improve coverage

* Improve coverage

* Iterate
This commit is contained in:
Michael Telatynski
2026-07-07 09:50:40 +00:00
committed by GitHub
parent 30c02ab5d7
commit 6fda0ad60f
10 changed files with 120 additions and 88 deletions
+177
View File
@@ -0,0 +1,177 @@
/*
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 { type ReactElement } from "react";
import { render } from "test-utils-rtl";
import { describe, it, expect } from "vitest";
import { MatrixError, ConnectionError } from "matrix-js-sdk/src/matrix";
import {
adminContactStrings,
messageForConnectionError,
messageForLoginError,
messageForResourceLimitError,
messageForSyncError,
resourceLimitStrings,
} from "./ErrorUtils";
describe("messageForResourceLimitError", () => {
it("should match snapshot for monthly_active_user", () => {
const { asFragment } = render(
messageForResourceLimitError("monthly_active_user", "some@email", resourceLimitStrings) as ReactElement,
);
expect(asFragment()).toMatchSnapshot();
});
it("should match snapshot for admin contact links", () => {
const { asFragment } = render(
messageForResourceLimitError("", "some@email", adminContactStrings) as ReactElement,
);
expect(asFragment()).toMatchSnapshot();
});
});
describe("messageForSyncError", () => {
it("should match snapshot for M_RESOURCE_LIMIT_EXCEEDED", () => {
const err = new MatrixError({
errcode: "M_RESOURCE_LIMIT_EXCEEDED",
data: {
limit_type: "monthly_active_user",
admin_contact: "some@email",
},
});
const { asFragment } = render(messageForSyncError(err) as ReactElement);
expect(asFragment()).toMatchSnapshot();
});
it("should match snapshot for other errors", () => {
const err = new MatrixError({
errcode: "OTHER_ERROR",
});
const { asFragment } = render(messageForSyncError(err) as ReactElement);
expect(asFragment()).toMatchSnapshot();
});
});
describe("messageForLoginError", () => {
it("should match snapshot for M_RESOURCE_LIMIT_EXCEEDED", () => {
const err = new MatrixError({
errcode: "M_RESOURCE_LIMIT_EXCEEDED",
data: {
limit_type: "monthly_active_user",
admin_contact: "some@email",
},
});
const { asFragment } = render(
messageForLoginError(err, {
hsUrl: "hsUrl",
hsName: "hsName",
}) as ReactElement,
);
expect(asFragment()).toMatchSnapshot();
});
it("should match snapshot for M_USER_DEACTIVATED", () => {
const err = new MatrixError(
{
errcode: "M_USER_DEACTIVATED",
},
403,
);
const { asFragment } = render(
messageForLoginError(err, {
hsUrl: "hsUrl",
hsName: "hsName",
}) as ReactElement,
);
expect(asFragment()).toMatchSnapshot();
});
it("should match snapshot for 401", () => {
const err = new MatrixError(
{
errcode: "UNKNOWN",
},
401,
);
const { asFragment } = render(
messageForLoginError(err, {
hsUrl: "hsUrl",
hsName: "hsName",
}) as ReactElement,
);
expect(asFragment()).toMatchSnapshot();
});
it("should match snapshot for unknown error", () => {
const err = new MatrixError({}, 400);
const { asFragment } = render(
messageForLoginError(err, {
hsUrl: "hsUrl",
hsName: "hsName",
}) as ReactElement,
);
expect(asFragment()).toMatchSnapshot();
});
});
describe("messageForConnectionError", () => {
it("should match snapshot for ConnectionError", () => {
const err = new ConnectionError("Internal Server Error", new MatrixError({}, 500));
const { asFragment } = render(
messageForConnectionError(err, {
hsUrl: "hsUrl",
hsName: "hsName",
}) as ReactElement,
);
expect(asFragment()).toMatchSnapshot();
});
it("should match snapshot for MatrixError M_NOT_FOUND", () => {
const err = new MatrixError(
{
errcode: "M_NOT_FOUND",
},
404,
);
const { asFragment } = render(
messageForConnectionError(err, {
hsUrl: "hsUrl",
hsName: "hsName",
}) as ReactElement,
);
expect(asFragment()).toMatchSnapshot();
});
it("should match snapshot for unknown error", () => {
const err = new Error("What even");
const { asFragment } = render(
messageForConnectionError(err, {
hsUrl: "hsUrl",
hsName: "hsName",
}) as ReactElement,
);
expect(asFragment()).toMatchSnapshot();
});
it("should match snapshot for mixed content error", () => {
const err = new ConnectionError("Mixed content maybe?");
Object.defineProperty(window, "location", { value: { protocol: "https:" } });
const { asFragment } = render(
messageForConnectionError(err, {
hsUrl: "http://server.com",
hsName: "hsName",
}) as ReactElement,
);
expect(asFragment()).toMatchSnapshot();
});
});
+468
View File
@@ -0,0 +1,468 @@
/*
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 {
M_LOCATION,
EventStatus,
EventType,
type IEvent,
type MatrixClient,
MatrixEvent,
MsgType,
PendingEventOrdering,
RelationType,
Room,
Thread,
} from "matrix-js-sdk/src/matrix";
import { vi, describe, it, expect, beforeEach, afterAll } from "vitest";
import { MatrixClientPeg } from "../MatrixClientPeg";
import {
canCancel,
canEditContent,
canEditOwnEvent,
fetchInitialEvent,
findEditableEvent,
highlightEvent,
isContentActionable,
isLocationEvent,
isVoiceMessage,
} from "./EventUtils";
import {
getMockClientWithEventEmitter,
makeBeaconInfoEvent,
makePollStartEvent,
stubClient,
} from "../../test/test-utils";
import dis from "../dispatcher/dispatcher";
import { Action } from "../dispatcher/actions";
vi.mock("../dispatcher/dispatcher");
describe("EventUtils", () => {
const userId = "@user:server";
const roomId = "!room:server";
const mockClient = getMockClientWithEventEmitter({
getUserId: vi.fn().mockReturnValue(userId),
});
beforeEach(() => {
mockClient.getUserId.mockClear().mockReturnValue(userId);
});
afterAll(() => {
vi.spyOn(MatrixClientPeg, "get").mockRestore();
});
// setup events
const unsentEvent = new MatrixEvent({
type: EventType.RoomMessage,
sender: userId,
});
unsentEvent.status = EventStatus.ENCRYPTING;
const redactedEvent = new MatrixEvent({
type: EventType.RoomMessage,
sender: userId,
});
redactedEvent.makeRedacted(
redactedEvent,
new Room(redactedEvent.getRoomId()!, mockClient, mockClient.getUserId()!),
);
const stateEvent = new MatrixEvent({
type: EventType.RoomTopic,
state_key: "",
});
const beaconInfoEvent = makeBeaconInfoEvent(userId, roomId);
const roomMemberEvent = new MatrixEvent({
type: EventType.RoomMember,
sender: userId,
});
const stickerEvent = new MatrixEvent({
type: EventType.Sticker,
sender: userId,
});
const pollStartEvent = makePollStartEvent("What?", userId);
const notDecryptedEvent = new MatrixEvent({
type: EventType.RoomMessage,
sender: userId,
content: {
msgtype: "m.bad.encrypted",
},
});
const noMsgType = new MatrixEvent({
type: EventType.RoomMessage,
sender: userId,
content: {
msgtype: undefined,
},
});
const noContentBody = new MatrixEvent({
type: EventType.RoomMessage,
sender: userId,
content: {
msgtype: MsgType.Image,
},
});
const emptyContentBody = new MatrixEvent({
type: EventType.RoomMessage,
sender: userId,
content: {
msgtype: MsgType.Text,
body: "",
},
});
const objectContentBody = new MatrixEvent({
type: EventType.RoomMessage,
sender: userId,
content: {
msgtype: MsgType.File,
body: {},
},
});
const niceTextMessage = new MatrixEvent({
type: EventType.RoomMessage,
sender: userId,
content: {
msgtype: MsgType.Text,
body: "Hello",
},
});
const bobsTextMessage = new MatrixEvent({
type: EventType.RoomMessage,
sender: "@bob:server",
content: {
msgtype: MsgType.Text,
body: "Hello from Bob",
},
});
describe("isContentActionable()", () => {
type TestCase = [string, MatrixEvent];
it.each<TestCase>([
["unsent event", unsentEvent],
["redacted event", redactedEvent],
["state event", stateEvent],
["undecrypted event", notDecryptedEvent],
["room member event", roomMemberEvent],
["event without msgtype", noMsgType],
["event without content body property", noContentBody],
])("returns false for %s", (_description, event) => {
expect(isContentActionable(event)).toBe(false);
});
it.each<TestCase>([
["sticker event", stickerEvent],
["poll start event", pollStartEvent],
["event with empty content body", emptyContentBody],
["event with a content body", niceTextMessage],
["beacon_info event", beaconInfoEvent],
])("returns true for %s", (_description, event) => {
expect(isContentActionable(event)).toBe(true);
});
});
describe("editable content helpers", () => {
const replaceRelationEvent = new MatrixEvent({
type: EventType.RoomMessage,
sender: userId,
content: {
"msgtype": MsgType.Text,
"body": "Hello",
["m.relates_to"]: {
rel_type: RelationType.Replace,
event_id: "1",
},
},
});
const referenceRelationEvent = new MatrixEvent({
type: EventType.RoomMessage,
sender: userId,
content: {
"msgtype": MsgType.Text,
"body": "Hello",
["m.relates_to"]: {
rel_type: RelationType.Reference,
event_id: "1",
},
},
});
const emoteEvent = new MatrixEvent({
type: EventType.RoomMessage,
sender: userId,
content: {
msgtype: MsgType.Emote,
body: "🧪",
},
});
type TestCase = [string, MatrixEvent];
const uneditableCases: TestCase[] = [
["redacted event", redactedEvent],
["state event", stateEvent],
["event that is not room message", roomMemberEvent],
["event without msgtype", noMsgType],
["event without content body property", noContentBody],
["event with empty content body property", emptyContentBody],
["event with non-string body", objectContentBody],
["event not sent by current user", bobsTextMessage],
["event with a replace relation", replaceRelationEvent],
];
const editableCases: TestCase[] = [
["event with reference relation", referenceRelationEvent],
["emote event", emoteEvent],
["poll start event", pollStartEvent],
["event with a content body", niceTextMessage],
];
describe("canEditContent()", () => {
it.each<TestCase>(uneditableCases)("returns false for %s", (_description, event) => {
expect(canEditContent(mockClient, event)).toBe(false);
});
it.each<TestCase>(editableCases)("returns true for %s", (_description, event) => {
expect(canEditContent(mockClient, event)).toBe(true);
});
});
describe("canEditOwnContent()", () => {
it.each<TestCase>(uneditableCases)("returns false for %s", (_description, event) => {
expect(canEditOwnEvent(mockClient, event)).toBe(false);
});
it.each<TestCase>(editableCases)("returns true for %s", (_description, event) => {
expect(canEditOwnEvent(mockClient, event)).toBe(true);
});
});
});
describe("isVoiceMessage()", () => {
it("returns true for an event with msc2516.voice content", () => {
const event = new MatrixEvent({
type: EventType.RoomMessage,
content: {
["org.matrix.msc2516.voice"]: {},
},
});
expect(isVoiceMessage(event)).toBe(true);
});
it("returns true for an event with msc3245.voice content", () => {
const event = new MatrixEvent({
type: EventType.RoomMessage,
content: {
["org.matrix.msc3245.voice"]: {},
},
});
expect(isVoiceMessage(event)).toBe(true);
});
it("returns false for an event with voice content", () => {
const event = new MatrixEvent({
type: EventType.RoomMessage,
content: {
body: "hello",
},
});
expect(isVoiceMessage(event)).toBe(false);
});
});
describe("isLocationEvent()", () => {
it("returns true for an event with m.location stable type", () => {
const event = new MatrixEvent({
type: M_LOCATION.altName,
});
expect(isLocationEvent(event)).toBe(true);
});
it("returns true for an event with m.location unstable prefixed type", () => {
const event = new MatrixEvent({
type: M_LOCATION.name,
});
expect(isLocationEvent(event)).toBe(true);
});
it("returns true for a room message with stable m.location msgtype", () => {
const event = new MatrixEvent({
type: EventType.RoomMessage,
content: {
msgtype: M_LOCATION.altName,
},
});
expect(isLocationEvent(event)).toBe(true);
});
it("returns true for a room message with unstable m.location msgtype", () => {
const event = new MatrixEvent({
type: EventType.RoomMessage,
content: {
msgtype: M_LOCATION.name,
},
});
expect(isLocationEvent(event)).toBe(true);
});
it("returns false for a non location event", () => {
const event = new MatrixEvent({
type: EventType.RoomMessage,
content: {
body: "Hello",
},
});
expect(isLocationEvent(event)).toBe(false);
});
});
describe("canCancel()", () => {
it.each([[EventStatus.QUEUED], [EventStatus.NOT_SENT], [EventStatus.ENCRYPTING]])(
"return true for status %s",
(status) => {
expect(canCancel(status)).toBe(true);
},
);
it.each([
[EventStatus.SENDING],
[EventStatus.CANCELLED],
[EventStatus.SENT],
["invalid-status" as unknown as EventStatus],
])("return false for status %s", (status) => {
expect(canCancel(status)).toBe(false);
});
});
describe("fetchInitialEvent", () => {
const ROOM_ID = "!roomId:example.org";
let room: Room;
let client: MatrixClient;
const NORMAL_EVENT = "$normalEvent";
const THREAD_ROOT = "$threadRoot";
const THREAD_REPLY = "$threadReply";
const events: Record<string, Partial<IEvent>> = {
[NORMAL_EVENT]: {
event_id: NORMAL_EVENT,
type: EventType.RoomMessage,
content: {
body: "Classic event",
msgtype: MsgType.Text,
},
},
[THREAD_ROOT]: {
event_id: THREAD_ROOT,
type: EventType.RoomMessage,
content: {
body: "Thread root",
msgtype: "m.text",
},
unsigned: {
"m.relations": {
[RelationType.Thread]: {
latest_event: {
event_id: THREAD_REPLY,
type: EventType.RoomMessage,
content: {
"body": "Thread reply",
"msgtype": MsgType.Text,
"m.relates_to": {
event_id: "$threadRoot",
rel_type: RelationType.Thread,
},
},
},
count: 1,
current_user_participated: false,
},
},
},
},
[THREAD_REPLY]: {
event_id: THREAD_REPLY,
type: EventType.RoomMessage,
content: {
"body": "Thread reply",
"msgtype": MsgType.Text,
"m.relates_to": {
event_id: THREAD_ROOT,
rel_type: RelationType.Thread,
},
},
},
};
beforeEach(() => {
vi.clearAllMocks();
stubClient();
client = MatrixClientPeg.safeGet();
room = new Room(ROOM_ID, client, client.getUserId()!, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
vi.spyOn(client, "supportsThreads").mockReturnValue(true);
vi.spyOn(client, "getRoom").mockReturnValue(room);
vi.spyOn(client, "fetchRoomEvent").mockImplementation(async (roomId, eventId) => {
return events[eventId] ?? Promise.reject();
});
});
it("returns null for unknown events", async () => {
expect(await fetchInitialEvent(client, room.roomId, "$UNKNOWN")).toBeNull();
expect(await fetchInitialEvent(client, room.roomId, NORMAL_EVENT)).toBeInstanceOf(MatrixEvent);
});
it("creates a thread when needed", async () => {
await fetchInitialEvent(client, room.roomId, THREAD_REPLY);
expect(room.getThread(THREAD_ROOT)).toBeInstanceOf(Thread);
});
});
describe("findEditableEvent", () => {
it("should not explode when given empty events array", () => {
expect(
findEditableEvent({
events: [],
isForward: true,
matrixClient: mockClient,
}),
).toBeUndefined();
});
});
describe("highlightEvent", () => {
const eventId = "$zLg9jResFQmMO_UKFeWpgLgOgyWrL8qIgLgZ5VywrCQ";
it("should dispatch an action to view the event", () => {
highlightEvent(roomId, eventId);
expect(dis.dispatch).toHaveBeenCalledWith({
action: Action.ViewRoom,
event_id: eventId,
highlighted: true,
room_id: roomId,
metricsTrigger: undefined,
});
});
});
});
+61
View File
@@ -0,0 +1,61 @@
/*
Copyright 2024 New Vector 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.
*/
import { type MediaEventContent } from "matrix-js-sdk/src/types";
import { describe, it, expect } from "vitest";
import { downloadLabelForFile } from "./FileUtils.ts";
describe("FileUtils", () => {
describe("downloadLabelForFile", () => {
it.each([
[
"File with size",
{
input: {
msgtype: "m.file",
body: "Test",
info: {
size: 102434566,
},
} as MediaEventContent,
output: "Download (97.69 MB)",
},
],
[
"Image",
{
input: {
msgtype: "m.image",
body: "Test",
} as MediaEventContent,
output: "Download",
},
],
[
"Video",
{
input: {
msgtype: "m.video",
body: "Test",
} as MediaEventContent,
output: "Download",
},
],
[
"Audio",
{
input: {
msgtype: "m.audio",
body: "Test",
} as MediaEventContent,
output: "Download",
},
],
])("should correctly label %s", (_d, { input, output }) => expect(downloadLabelForFile(input)).toBe(output));
});
});
@@ -0,0 +1,141 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`messageForConnectionError > should match snapshot for ConnectionError 1`] = `
<DocumentFragment>
<span>
<span>
Can't connect to homeserver - please check your connectivity, ensure your
<a
class="mx_ExternalLink"
href="hsUrl"
rel="noreferrer noopener"
target="_blank"
>
homeserver's SSL certificate
<svg
class="mx_ExternalLink_icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
/>
<path
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
/>
</svg>
</a>
is trusted, and that a browser extension is not blocking requests.
</span>
</span>
</DocumentFragment>
`;
exports[`messageForConnectionError > should match snapshot for MatrixError M_NOT_FOUND 1`] = `
<DocumentFragment>
There was a problem communicating with the homeserver, please try again later.(M_NOT_FOUND)
</DocumentFragment>
`;
exports[`messageForConnectionError > should match snapshot for mixed content error 1`] = `
<DocumentFragment>
<span>
<span>
Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or
<a
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
rel="noreferrer noopener"
target="_blank"
>
enable unsafe scripts
</a>
.
</span>
</span>
</DocumentFragment>
`;
exports[`messageForConnectionError > should match snapshot for unknown error 1`] = `
<DocumentFragment>
There was a problem communicating with the homeserver, please try again later.
</DocumentFragment>
`;
exports[`messageForLoginError > should match snapshot for 401 1`] = `
<DocumentFragment>
Incorrect username and/or password.
</DocumentFragment>
`;
exports[`messageForLoginError > should match snapshot for M_RESOURCE_LIMIT_EXCEEDED 1`] = `
<DocumentFragment>
<div>
<div>
This homeserver has exceeded one of its resource limits.
</div>
<div
class="mx_Login_smallError"
>
Please contact your service administrator to continue using this service.
</div>
</div>
</DocumentFragment>
`;
exports[`messageForLoginError > should match snapshot for M_USER_DEACTIVATED 1`] = `
<DocumentFragment>
This account has been deactivated.
</DocumentFragment>
`;
exports[`messageForLoginError > should match snapshot for unknown error 1`] = `
<DocumentFragment>
There was a problem communicating with the homeserver, please try again later. (HTTP 400)
</DocumentFragment>
`;
exports[`messageForResourceLimitError > should match snapshot for admin contact links 1`] = `
<DocumentFragment>
<span>
Please
<a
href="some@email"
rel="noreferrer noopener"
target="_blank"
>
contact your service administrator
</a>
to continue using this service.
</span>
</DocumentFragment>
`;
exports[`messageForResourceLimitError > should match snapshot for monthly_active_user 1`] = `
<DocumentFragment>
This homeserver has hit its Monthly Active User limit.
</DocumentFragment>
`;
exports[`messageForSyncError > should match snapshot for M_RESOURCE_LIMIT_EXCEEDED 1`] = `
<DocumentFragment>
<div>
<div>
This homeserver has exceeded one of its resource limits.
</div>
<div>
Please contact your service administrator to continue using this service.
</div>
</div>
</DocumentFragment>
`;
exports[`messageForSyncError > should match snapshot for other errors 1`] = `
<DocumentFragment>
<div>
Unable to connect to Homeserver. Retrying…
</div>
</DocumentFragment>
`;
+178
View File
@@ -0,0 +1,178 @@
/*
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, type Mocked } from "vitest";
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils";
import { mkRoom, resetAsyncStoreWithClient, setupAsyncStoreWithClient, stubClient } from "test-utils/test-utils";
import { MatrixClientPeg } from "../MatrixClientPeg";
import defaultDispatcher from "../dispatcher/dispatcher";
import { type ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../dispatcher/actions";
import { leaveRoomBehaviour } from "./leave-behaviour";
import { SDKContextClass } from "../contexts/SDKContextClass";
import DMRoomMap from "../utils/DMRoomMap";
import SpaceStore from "../stores/spaces/SpaceStore";
import { MetaSpace } from "../stores/spaces";
import { type ActionPayload } from "../dispatcher/payloads";
import SettingsStore from "../settings/SettingsStore";
import { CallStore } from "../stores/CallStore";
import { type Call } from "../models/Call";
import LegacyCallHandler from "../LegacyCallHandler";
vi.mock("../Modal.tsx");
describe("leaveRoomBehaviour", () => {
SDKContextClass.instance.constructEagerStores(); // Initialize RoomViewStore
let client: Mocked<MatrixClient>;
let room: Mocked<Room>;
let space: Mocked<Room>;
beforeEach(async () => {
stubClient();
client = vi.mocked(MatrixClientPeg.safeGet());
DMRoomMap.makeShared(client);
room = mkRoom(client, "!1:example.org");
space = mkRoom(client, "!2:example.org");
space.isSpaceRoom.mockReturnValue(true);
client.getRoom.mockImplementation((roomId) => {
switch (roomId) {
case room.roomId:
return room;
case space.roomId:
return space;
default:
return null;
}
});
await setupAsyncStoreWithClient(SpaceStore.instance, client);
});
afterEach(async () => {
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
await resetAsyncStoreWithClient(SpaceStore.instance);
vi.restoreAllMocks();
});
const viewRoom = (room: Room) =>
defaultDispatcher.dispatch<ViewRoomPayload>(
{
action: Action.ViewRoom,
room_id: room.roomId,
metricsTrigger: undefined,
},
true,
);
const expectDispatch = async <T extends ActionPayload>(payload: T) => {
const dispatcherSpy = vi.fn();
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
await sleep(0);
expect(dispatcherSpy).toHaveBeenCalledWith(payload);
defaultDispatcher.unregister(dispatcherRef);
};
it("hangs up legacy calls when leaving a room", async () => {
const hangupSpy = vi.spyOn(LegacyCallHandler.instance, "hangupOrReject").mockImplementation(() => {});
viewRoom(room);
await leaveRoomBehaviour(client, room.roomId);
expect(hangupSpy).toHaveBeenCalledWith(room.roomId);
});
it("disconnects widget-based calls when leaving a room", async () => {
const mockCall = {
disconnect: vi.fn().mockResolvedValue(undefined),
} as unknown as Call;
vi.spyOn(CallStore.instance, "getActiveCall").mockReturnValue(mockCall);
viewRoom(room);
await leaveRoomBehaviour(client, room.roomId);
expect(mockCall.disconnect).toHaveBeenCalled();
});
it("returns to the home page after leaving a room outside of a space that was being viewed", async () => {
viewRoom(room);
await leaveRoomBehaviour(client, room.roomId);
await expectDispatch({ action: Action.ViewHomePage });
});
it("returns to the parent space after leaving a room inside of a space that was being viewed", async () => {
vi.spyOn(SpaceStore.instance, "getCanonicalParent").mockImplementation((roomId) =>
roomId === room.roomId ? space : null,
);
viewRoom(room);
SpaceStore.instance.setActiveSpace(space.roomId, false);
await leaveRoomBehaviour(client, room.roomId);
await expectDispatch({
action: Action.ViewRoom,
room_id: space.roomId,
metricsTrigger: undefined,
});
});
it("returns to the home page after leaving a top-level space that was being viewed", async () => {
viewRoom(space);
SpaceStore.instance.setActiveSpace(space.roomId, false);
await leaveRoomBehaviour(client, space.roomId);
await expectDispatch({ action: Action.ViewHomePage });
});
it("returns to the parent space after leaving a subspace that was being viewed", async () => {
room.isSpaceRoom.mockReturnValue(true);
vi.spyOn(SpaceStore.instance, "getCanonicalParent").mockImplementation((roomId) =>
roomId === room.roomId ? space : null,
);
viewRoom(room);
SpaceStore.instance.setActiveSpace(room.roomId, false);
await leaveRoomBehaviour(client, room.roomId);
await expectDispatch({
action: Action.ViewRoom,
room_id: space.roomId,
metricsTrigger: undefined,
});
});
describe("If the feature_dynamic_room_predecessors is not enabled", () => {
beforeEach(() => {
vi.spyOn(SettingsStore, "getValue").mockReturnValue(false);
});
it("Passes through the dynamic predecessor setting", async () => {
await leaveRoomBehaviour(client, room.roomId);
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, false);
});
});
describe("If the feature_dynamic_room_predecessors is enabled", () => {
beforeEach(() => {
// Turn on feature_dynamic_room_predecessors setting
vi.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_dynamic_room_predecessors",
);
});
it("Passes through the dynamic predecessor setting", async () => {
await leaveRoomBehaviour(client, room.roomId);
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, true);
});
});
});