feat: show call participants in room list (Discord-style)
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled

This commit is contained in:
sorB
2026-05-10 14:25:35 +02:00
parent b797925316
commit 3da363517f
4610 changed files with 827237 additions and 1 deletions
@@ -0,0 +1,57 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`/lenny should match snapshot with args 1`] = `
{
"body": "( ͡° ͜ʖ ͡°) this is a test message",
"msgtype": "m.text",
}
`;
exports[`/lenny should match snapshot with no args 1`] = `
{
"body": "( ͡° ͜ʖ ͡°)",
"msgtype": "m.text",
}
`;
exports[`/shrug should match snapshot with args 1`] = `
{
"body": "¯\\_(ツ)_/¯ this is a test message",
"msgtype": "m.text",
}
`;
exports[`/shrug should match snapshot with no args 1`] = `
{
"body": "¯\\_(ツ)_/¯",
"msgtype": "m.text",
}
`;
exports[`/tableflip should match snapshot with args 1`] = `
{
"body": "(╯°□°)╯︵ ┻━┻ this is a test message",
"msgtype": "m.text",
}
`;
exports[`/tableflip should match snapshot with no args 1`] = `
{
"body": "(╯°□°)╯︵ ┻━┻",
"msgtype": "m.text",
}
`;
exports[`/unflip should match snapshot with args 1`] = `
{
"body": "┬──┬ ( ゜-゜ノ) this is a test message",
"msgtype": "m.text",
}
`;
exports[`/unflip should match snapshot with no args 1`] = `
{
"body": "┬──┬ ( ゜-゜ノ)",
"msgtype": "m.text",
}
`;
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`/rainbow should make things rainbowy 1`] = `
{
"body": "this is a test message",
"format": "org.matrix.custom.html",
"formatted_body": "<span data-mx-color="#ff00be">t</span><span data-mx-color="#ff0080">h</span><span data-mx-color="#ff0041">i</span><span data-mx-color="#ff5f00">s</span> <span data-mx-color="#faa900">i</span><span data-mx-color="#c3bf00">s</span> <span data-mx-color="#00d800">a</span> <span data-mx-color="#00e371">t</span><span data-mx-color="#00e6b6">e</span><span data-mx-color="#00e7f8">s</span><span data-mx-color="#00e7ff">t</span> <span data-mx-color="#00deff">m</span><span data-mx-color="#00d2ff">e</span><span data-mx-color="#00c0ff">s</span><span data-mx-color="#44a4ff">s</span><span data-mx-color="#e87dff">a</span><span data-mx-color="#ff42ff">g</span><span data-mx-color="#ff00fe">e</span>",
"msgtype": "m.text",
}
`;
exports[`/rainbowme should make things rainbowy 1`] = `
{
"body": "this is a test message",
"format": "org.matrix.custom.html",
"formatted_body": "<span data-mx-color="#ff00be">t</span><span data-mx-color="#ff0080">h</span><span data-mx-color="#ff0041">i</span><span data-mx-color="#ff5f00">s</span> <span data-mx-color="#faa900">i</span><span data-mx-color="#c3bf00">s</span> <span data-mx-color="#00d800">a</span> <span data-mx-color="#00e371">t</span><span data-mx-color="#00e6b6">e</span><span data-mx-color="#00e7f8">s</span><span data-mx-color="#00e7ff">t</span> <span data-mx-color="#00deff">m</span><span data-mx-color="#00d2ff">e</span><span data-mx-color="#00c0ff">s</span><span data-mx-color="#44a4ff">s</span><span data-mx-color="#e87dff">a</span><span data-mx-color="#ff42ff">g</span><span data-mx-color="#ff00fe">e</span>",
"msgtype": "m.emote",
}
`;
@@ -0,0 +1,39 @@
/*
* Copyright 2026 Element Creations Ltd.
* 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 { waitFor } from "jest-matrix-react";
import WidgetUtils from "../../../src/utils/WidgetUtils";
import { setUpCommandTest } from "./utils";
import { WidgetType } from "../../../src/widgets/WidgetType";
describe("/addwidget", () => {
const roomId = "!room:example.com";
it("should parse html iframe snippets", async () => {
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
const spy = jest.spyOn(WidgetUtils, "setRoomWidget");
const { client, command } = setUpCommandTest(roomId, `/addwidget`);
command.run(client, roomId, null, '<iframe src="https://element.io"></iframe>');
await waitFor(() =>
expect(spy).toHaveBeenCalledWith(
client,
roomId,
expect.any(String),
WidgetType.CUSTOM,
"https://element.io",
"Custom",
{},
),
);
});
});
@@ -0,0 +1,33 @@
/*
* 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.
*/
import { setUpCommandTest } from "./utils";
describe("/ban", () => {
const roomId = "!room:example.com";
it("should return usage if no args", () => {
const { client, command } = setUpCommandTest(roomId, `/ban`);
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
});
it("should ban the user we specify from this room", async () => {
const { client, command, args } = setUpCommandTest(roomId, `/ban @u:s.co`);
await command.run(client, roomId, null, args).promise;
expect(client.ban).toHaveBeenCalledWith(roomId, "@u:s.co", undefined);
});
it("should provide the ban reason if we supply it", async () => {
const { client, command, args } = setUpCommandTest(roomId, `/ban @u:s.co They were quite nasty`);
await command.run(client, roomId, null, args).promise;
expect(client.ban).toHaveBeenCalledWith(roomId, "@u:s.co", "They were quite nasty");
});
});
@@ -0,0 +1,48 @@
/*
* Copyright 2026 Element Creations Ltd.
* 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 { setUpCommandTest } from "./utils";
describe("SlashCommands", () => {
const roomId = "!room:example.com";
describe.each([
["myroomnick"],
["roomavatar"],
["myroomavatar"],
["topic"],
["roomname"],
["invite"],
["part"],
["remove"],
["ban"],
["unban"],
["op"],
["deop"],
["addwidget"],
["discardsession"],
["whois"],
["holdcall"],
["unholdcall"],
["converttodm"],
["converttoroom"],
])("/%s", (commandName: string) => {
describe("isEnabled", () => {
it("should return true for Room", () => {
const { client, command } = setUpCommandTest(roomId, `/${commandName}`);
expect(command.isEnabled(client, roomId)).toBe(true);
});
it("should return false for LocalRoom", () => {
const { client, command } = setUpCommandTest(roomId, `/${commandName}`, true);
expect(command.isEnabled(client, roomId)).toBe(false);
});
});
});
});
@@ -0,0 +1,30 @@
/*
* Copyright 2026 Element Creations Ltd.
* 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 { setUpCommandTest } from "./utils";
describe.each(["shrug", "tableflip", "unflip", "lenny"])("/%s", (commandName: string) => {
const roomId = "!room:example.com";
it("should match snapshot with no args", async () => {
const { client, command } = setUpCommandTest(roomId, `/${commandName}`);
await expect(command.run(client, roomId, null).promise).resolves.toMatchSnapshot();
});
it("should match snapshot with args", async () => {
const { client, command } = setUpCommandTest(roomId, `/${commandName}`);
const initialResult = await command.run(client, roomId, null, "this is a test message").promise;
expect(initialResult).toMatchSnapshot();
// We run this twice to ensure it doesn't stack the buffer
await expect(command.run(client, roomId, null, "this is a test message").promise).resolves.toEqual(
initialResult,
);
});
});
@@ -0,0 +1,43 @@
/*
* 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.
*/
import Modal, { type ComponentType, type IHandle } from "../../../src/Modal";
import { setUpCommandTest } from "./utils";
describe("/invite", () => {
const roomId = "!room:example.com";
it("should return usage if no args", () => {
const { client, command } = setUpCommandTest(roomId, `/invite`);
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
});
it("should invite the user we specify to this room", async () => {
const spy = jest.spyOn(Modal, "createDialog");
spy.mockReturnValue({ close: () => {} } as unknown as IHandle<ComponentType>);
const { client, command, args } = setUpCommandTest(roomId, `/invite @u:s.co`);
await command.run(client, roomId, null, args).promise;
expect(client.invite).toHaveBeenCalledWith(roomId, "@u:s.co", { shareEncryptedHistory: true });
});
it("should provide the invite reason if we supply it", async () => {
const spy = jest.spyOn(Modal, "createDialog");
spy.mockReturnValue({ close: () => {} } as unknown as IHandle<ComponentType>);
const { client, command, args } = setUpCommandTest(roomId, `/invite @u:s.co They are a very nice person`);
await command.run(client, roomId, null, args).promise;
expect(client.invite).toHaveBeenCalledWith(roomId, "@u:s.co", {
reason: "They are a very nice person",
shareEncryptedHistory: true,
});
});
});
@@ -0,0 +1,79 @@
/*
* Copyright 2026 Element Creations Ltd.
* 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 { setUpCommandTest } from "./utils";
import dispatcher from "../../../src/dispatcher/dispatcher";
describe("/join", () => {
const roomId = "!room:example.com";
it("should return usage if no args", () => {
const { client, command } = setUpCommandTest(roomId, `/join`);
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
});
it("should handle matrix.org permalinks", async () => {
const { client, command } = setUpCommandTest(roomId, `/join`);
jest.spyOn(dispatcher, "dispatch");
await command.run(client, roomId, null, "https://matrix.to/#/!roomId:server/$eventId").promise;
expect(dispatcher.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
action: "view_room",
room_id: "!roomId:server",
event_id: "$eventId",
highlighted: true,
}),
);
});
it("should handle room aliases", async () => {
const { client, command } = setUpCommandTest(roomId, `/join`);
jest.spyOn(dispatcher, "dispatch");
await command.run(client, roomId, null, "#test:server").promise;
expect(dispatcher.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
action: "view_room",
room_alias: "#test:server",
}),
);
});
it("should handle room aliases with no server component", async () => {
const { client, command } = setUpCommandTest(roomId, `/join`);
jest.spyOn(dispatcher, "dispatch");
await command.run(client, roomId, null, "#test").promise;
expect(dispatcher.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
action: "view_room",
room_alias: `#test:${client.getDomain()}`,
}),
);
});
it("should handle room IDs and via servers", async () => {
const { client, command } = setUpCommandTest(roomId, `/join`);
jest.spyOn(dispatcher, "dispatch");
await command.run(client, roomId, null, "!foo:bar serv1.com serv2.com").promise;
expect(dispatcher.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
action: "view_room",
room_id: "!foo:bar",
via_servers: ["serv1.com", "serv2.com"],
}),
);
});
});
@@ -0,0 +1,46 @@
/*
* 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.
*/
import { setUpCommandTest } from "./utils";
import dispatcher from "../../../src/dispatcher/dispatcher";
import DMRoomMap from "../../../src/utils/DMRoomMap";
describe("/msg", () => {
const roomId = "!room:example.com";
it("should return usage if no args", () => {
const { client, command } = setUpCommandTest(roomId, `/msg`);
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
});
it("should message the user and switch to the relevant DM", async () => {
// Given there is no DM room with the user
jest.spyOn(DMRoomMap, "shared").mockReturnValue({
getDMRoomsForUserId: jest.fn().mockReturnValue([]),
getRoomIds: jest.fn().mockReturnValue([roomId]),
} as unknown as DMRoomMap);
jest.spyOn(dispatcher, "dispatch");
// When we send a message to that user
const { client, command, args } = setUpCommandTest(roomId, `/msg @u:s.co Hello there`);
await command.run(client, roomId, null, args).promise;
// Then we create a room and send the message in there
expect(client.sendTextMessage).toHaveBeenCalledWith("!1:example.org", "Hello there");
// And tell the UI to switch to that room
expect(dispatcher.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
action: "view_room",
metricsTrigger: "SlashCommand",
metricsViaKeyboard: true,
room_id: "!1:example.org",
}),
);
});
});
@@ -0,0 +1,77 @@
/*
* Copyright 2026 Element Creations Ltd.
* 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 { KnownMembership, RoomMember } from "matrix-js-sdk/src/matrix";
import { setUpCommandTest } from "./utils";
import { warnSelfDemote } from "../../../src/components/views/right_panel/UserInfo";
jest.mock("../../../src/components/views/right_panel/UserInfo");
describe("/op", () => {
const roomId = "!room:example.com";
it("should return usage if no args", () => {
const { client, command, args } = setUpCommandTest(roomId, "/op");
expect(command.run(client, roomId, null, args).error).toBe(command.getUsage());
});
it("should reject with usage if given an invalid power level value", () => {
const { client, command, args } = setUpCommandTest(roomId, "/op @bob:server Admin");
expect(command.run(client, roomId, null, args).error).toBe(command.getUsage());
});
it("should reject with usage for invalid input", () => {
const { client, command } = setUpCommandTest(roomId, "/op");
expect(command.run(client, roomId, null, " ").error).toBe(command.getUsage());
});
it("should warn about self demotion", async () => {
const { client, command, room } = setUpCommandTest(roomId, "/op");
const member = new RoomMember(roomId, client.getSafeUserId());
member.membership = KnownMembership.Join;
member.powerLevel = 100;
room.getMember = () => member;
command.run(client, roomId, null, `${client.getUserId()} 0`);
expect(warnSelfDemote).toHaveBeenCalled();
});
it("should default to 50 if no powerlevel specified", async () => {
const { client, command, room } = setUpCommandTest(roomId, "/op");
const member = new RoomMember(roomId, "@user:server");
member.membership = KnownMembership.Join;
room.getMember = () => member;
command.run(client, roomId, null, member.userId);
expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, member.userId, 50);
});
});
describe("/deop", () => {
const roomId = "!room:example.com";
it("should return usage if no args", () => {
const { client, command } = setUpCommandTest(roomId, "/deop");
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
});
it("should warn about self demotion", async () => {
const { client, command, room } = setUpCommandTest(roomId, "/deop");
const member = new RoomMember(roomId, client.getSafeUserId());
member.membership = KnownMembership.Join;
member.powerLevel = 100;
room.getMember = () => member;
await command.run(client, roomId, null, client.getSafeUserId()).promise;
expect(warnSelfDemote).toHaveBeenCalled();
});
it("should reject with usage for invalid input", () => {
const { client, command } = setUpCommandTest(roomId, "/deop");
expect(command.run(client, roomId, null, " ").error).toBe(command.getUsage());
});
});
@@ -0,0 +1,22 @@
/*
* 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.
*/
import { parseCommandString } from "../../../src/slash-commands/SlashCommands";
describe("parseCommandString", () => {
it("should be able to split arguments at the first whitespace", () => {
expect(parseCommandString("/a b")).toEqual({ cmd: "a", args: "b" });
expect(parseCommandString("/cmd And more stuff")).toEqual({ cmd: "cmd", args: "And more stuff" });
expect(parseCommandString("/cmd And more stuff")).toEqual({ cmd: "cmd", args: "And more stuff" });
expect(parseCommandString("/cmd And more\nstuff")).toEqual({ cmd: "cmd", args: "And more\nstuff" });
expect(parseCommandString("/cmd \t\n And more stuff")).toEqual({ cmd: "cmd", args: "And more stuff" });
expect(parseCommandString("/a")).toEqual({ cmd: "a" });
expect(parseCommandString("/cmd")).toEqual({ cmd: "cmd" });
expect(parseCommandString("/cmd ")).toEqual({ cmd: "cmd" });
expect(parseCommandString(" /cmd ")).toEqual({ cmd: "cmd" });
});
});
@@ -0,0 +1,72 @@
/*
* Copyright 2026 Element Creations Ltd.
* 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 { type MatrixClient, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import Modal, { type ComponentType, type IHandle } from "../../../src/Modal";
import { setUpCommandTest } from "./utils";
import { type Command } from "../../../src/slash-commands/command";
describe("/part", () => {
const roomId = "!room:example.com";
function setUp(): {
client: MatrixClient;
command: Command;
args?: string;
room1: Room;
room2: Room;
} {
const spy = jest.spyOn(Modal, "createDialog");
spy.mockReturnValue({ close: () => {} } as unknown as IHandle<ComponentType>);
const { client, command, args } = setUpCommandTest(roomId, "/part #foo:bar");
expect(args).toBeDefined();
const room1 = new Room("!room-id", client, client.getSafeUserId(), {
pendingEventOrdering: PendingEventOrdering.Detached,
});
const room2 = new Room("!other-room", client, client.getSafeUserId());
mocked(client.getRoom).mockImplementation((rId: string): Room | null => {
if (rId === room1.roomId) {
return room1;
} else if (rId === room2.roomId) {
return room2;
} else {
return null;
}
});
mocked(client.getRooms).mockReturnValue([room1, room2]);
return { client, command, args, room1, room2 };
}
it("should part room matching alias if found", async () => {
const { client, command, args, room1, room2 } = setUp();
room1.getCanonicalAlias = jest.fn().mockReturnValue("#foo:bar");
room2.getCanonicalAlias = jest.fn().mockReturnValue("#baz:bar");
await command.run(client, room1.roomId, null, args).promise;
expect(client.leaveRoomChain).toHaveBeenCalledWith(room1.roomId, expect.anything());
});
it("should part room matching alt alias if found", async () => {
const { client, command, args, room1, room2 } = setUp();
room1.getAltAliases = jest.fn().mockReturnValue(["#foo:bar"]);
room2.getAltAliases = jest.fn().mockReturnValue(["#baz:bar"]);
await command.run(client, room1.roomId, null, args).promise;
expect(client.leaveRoomChain).toHaveBeenCalledWith(room1.roomId, expect.anything());
});
});
@@ -0,0 +1,25 @@
/*
* Copyright 2026 Element Creations Ltd.
* 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 { setUpCommandTest } from "./utils";
describe.each(["rainbow", "rainbowme"])("/%s", (commandName: string) => {
const roomId = "!room:example.com";
it("should return usage if no args", () => {
const { client, command } = setUpCommandTest(roomId, `/${commandName}`);
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
});
it("should make things rainbowy", async () => {
const { client, command } = setUpCommandTest(roomId, `/${commandName}`);
await expect(command.run(client, roomId, null, "this is a test message").promise).resolves.toMatchSnapshot();
});
});
@@ -0,0 +1,33 @@
/*
* 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.
*/
import { setUpCommandTest } from "./utils";
describe("/remove", () => {
const roomId = "!room:example.com";
it("should return usage if no args", () => {
const { client, command } = setUpCommandTest(roomId, `/remove`);
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
});
it("should kick the user we specify from this room", async () => {
const { client, command, args } = setUpCommandTest(roomId, `/remove @u:s.co`);
await command.run(client, roomId, null, args).promise;
expect(client.kick).toHaveBeenCalledWith(roomId, "@u:s.co", undefined);
});
it("should provide the kick reason if we supply it", async () => {
const { client, command, args } = setUpCommandTest(roomId, `/remove @u:s.co They were not very nice`);
await command.run(client, roomId, null, args).promise;
expect(client.kick).toHaveBeenCalledWith(roomId, "@u:s.co", "They were not very nice");
});
});
@@ -0,0 +1,22 @@
/*
* 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.
*/
import { splitAtFirstSpace } from "../../../src/slash-commands/SlashCommands";
describe("splitAtFirstSpace", () => {
it("should be able to split arguments at the first whitespace", () => {
expect(splitAtFirstSpace("a b")).toEqual(["a", "b"]);
expect(splitAtFirstSpace("arg1 Followed by more stuff")).toEqual(["arg1", "Followed by more stuff"]);
expect(splitAtFirstSpace("arg1 Followed by more\nstuff")).toEqual(["arg1", "Followed by more\nstuff"]);
expect(splitAtFirstSpace(" arg1 Followed by more stuff ")).toEqual(["arg1", "Followed by more stuff"]);
expect(splitAtFirstSpace("arg1 \t\n Followed by more stuff")).toEqual(["arg1", "Followed by more stuff"]);
expect(splitAtFirstSpace("a")).toEqual(["a"]);
expect(splitAtFirstSpace("arg1")).toEqual(["arg1"]);
expect(splitAtFirstSpace("arg1 ")).toEqual(["arg1"]);
expect(splitAtFirstSpace(" arg1 ")).toEqual(["arg1"]);
});
});
@@ -0,0 +1,31 @@
/*
* Copyright 2026 Element Creations Ltd.
* 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 Modal from "../../../src/Modal";
import { setUpCommandTest } from "./utils";
describe("/topic", () => {
const roomId = "!room:example.com";
it("sets topic", async () => {
const { client, command, args } = setUpCommandTest(roomId, "/topic pizza");
expect(args).toBeDefined();
command.run(client, "room-id", null, args);
expect(client.setRoomTopic).toHaveBeenCalledWith("room-id", "pizza", undefined);
});
it("should show topic modal if no args passed", async () => {
const spy = jest.spyOn(Modal, "createDialog");
const { client, command } = setUpCommandTest(roomId, "/topic");
await command.run(client, roomId, null).promise;
expect(spy).toHaveBeenCalled();
});
});
@@ -0,0 +1,124 @@
/*
* Copyright 2026 Element Creations Ltd.
* 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 { type MatrixClient } from "matrix-js-sdk/src/matrix";
import RoomUpgradeWarningDialog, {
type IFinishedOpts,
} from "../../../src/components/views/dialogs/RoomUpgradeWarningDialog";
import { type Command } from "../../../src/slash-commands/SlashCommands";
import { parseUpgradeRoomArgs } from "../../../src/slash-commands/upgraderoom/parseUpgradeRoomArgs";
import Modal from "../../../src/Modal";
import { setUpCommandTest } from "./utils";
describe("/upgraderoom", () => {
const roomId = "!room:example.com";
/**
* Set up an upgraderoom test.
*
* @param continueUpgrade if true, simulates the user clicking Continue in
* the "Upgrade Room" dialog. If false, simulates the
* user clicking Cancel.
*/
function setUp(continueUpgrade: boolean): {
command: Command;
client: MatrixClient;
createDialog: unknown;
upgradeRoom: unknown;
} {
jest.clearAllMocks();
const { command, client } = setUpCommandTest(roomId, "/upgraderoom");
const createDialog = jest.spyOn(Modal, "createDialog");
const upgradeRoom = jest.fn().mockResolvedValue({ replacement_room: "!newroom" });
const resp: IFinishedOpts = { continue: continueUpgrade, invite: false };
createDialog.mockReturnValue({
finished: Promise.resolve([resp]),
close: jest.fn(),
});
client.upgradeRoom = upgradeRoom;
return { command, client, createDialog, upgradeRoom };
}
it("should be enabled by default", () => {
const { command, client } = setUp(false);
expect(command.isEnabled(client, roomId)).toBe(true);
});
it("should return usage if given no args", () => {
const { command, client } = setUp(false);
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
expect(command.run(client, roomId, null, "").error).toBe(command.getUsage());
expect(command.run(client, roomId, null, " ").error).toBe(command.getUsage());
});
it("should accept arguments of a room version with no additional creators", () => {
expect(parseUpgradeRoomArgs("12")).toEqual({ targetVersion: "12" });
});
it("should accept arguments of a room version and additional creators", () => {
expect(parseUpgradeRoomArgs("13 @u:s.co")).toEqual({
targetVersion: "13",
additionalCreators: ["@u:s.co"],
});
expect(parseUpgradeRoomArgs("14 @u:s.co @v:s.co @w:z.uk")).toEqual({
targetVersion: "14",
additionalCreators: ["@u:s.co", "@v:s.co", "@w:z.uk"],
});
});
it("should upgrade the room when given valid arguments", async () => {
// Given the user clicks continue in the Upgrade Room dialog
const { command, client, createDialog, upgradeRoom } = setUp(true);
// When we type /upgraderoom ...
const result = command.run(client, roomId, null, "12 @foo:bar.com @baz:qux.uk");
expect(result.promise).toBeDefined();
await result.promise;
// Then we warned the user
expect(createDialog).toHaveBeenCalledWith(
RoomUpgradeWarningDialog,
{ roomId: "!room:example.com", targetVersion: "12" },
undefined,
false,
true,
);
// And when they said yes, we called into upgradeRoom
expect(upgradeRoom).toHaveBeenCalledWith("!room:example.com", "12", ["@foo:bar.com", "@baz:qux.uk"]);
});
it("should not upgrade the room if the user changes their mind", async () => {
// Given the user cancels the upgrade dialog
const { command, client, createDialog, upgradeRoom } = setUp(false);
// When we type /upgraderoom ...
const result = command.run(client, roomId, null, "12 @foo:bar.com @baz:qux.uk");
expect(result.promise).toBeDefined();
await result.promise;
// Then we warned the user
expect(createDialog).toHaveBeenCalledWith(
RoomUpgradeWarningDialog,
{ roomId: "!room:example.com", targetVersion: "12" },
undefined,
false,
true,
);
// And when they said no, we did not call into upgradeRoom
expect(upgradeRoom).not.toHaveBeenCalledWith("!room:example.com", "12", ["@foo:bar.com", "@baz:qux.uk"]);
});
});
@@ -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.
*/
import { Room, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import { type Command } from "../../../src/slash-commands/command";
import { getCommand } from "../../../src/slash-commands/SlashCommands";
import { stubClient } from "../../test-utils";
import { SdkContextClass } from "../../../src/contexts/SDKContext";
import { LocalRoom } from "../../../src/models/LocalRoom";
export function setUpCommandTest(
roomId: string,
input: string,
roomIsLocal?: boolean,
): {
command: Command;
args?: string;
client: MatrixClient;
room: Room;
} {
jest.clearAllMocks();
// TODO: if getCommand took a MatrixClient argument, we could use
// createTestClient here instead of stubClient (i.e. avoid setting
// MatrixClientPeg.)
const client = stubClient();
const { cmd: command, args } = getCommand(roomId, input);
let room: Room;
if (roomIsLocal) {
room = new LocalRoom(roomId, client, client.getSafeUserId());
} else {
room = new Room(roomId, client, client.getSafeUserId());
}
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(roomId);
mocked(client.getRoom).mockImplementation((rId: string): Room | null => {
if (rId === roomId) {
return room;
} else {
return null;
}
});
return { command: command!, args, client, room };
}
@@ -0,0 +1,64 @@
/*
* Copyright 2026 Element Creations Ltd.
* 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 { act, waitFor } from "jest-matrix-react";
import Modal, { type ComponentType, type IHandle } from "../../../src/Modal";
import { setUpCommandTest } from "./utils";
import QuestionDialog from "../../../src/components/views/dialogs/QuestionDialog";
import ErrorDialog from "../../../src/components/views/dialogs/ErrorDialog";
describe("/verify", () => {
const roomId = "!room:example.com";
it("should return usage if no args", () => {
const { client, command } = setUpCommandTest(roomId, `/verify`);
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
});
it("should attempt manual verification after confirmation", async () => {
// Given we say yes to prompt
const spy = jest.spyOn(Modal, "createDialog");
spy.mockReturnValue({ finished: Promise.resolve([true]) } as unknown as IHandle<ComponentType>);
// When we run the command
const { client, command } = setUpCommandTest(roomId, `/verify`);
await act(() => command.run(client, roomId, null, "mydeviceid myfingerprint"));
// Then the prompt is displayed
expect(spy).toHaveBeenCalledWith(
QuestionDialog,
expect.objectContaining({ title: "Caution: manual device verification" }),
);
// And then we attempt the verification
await waitFor(() =>
expect(spy).toHaveBeenCalledWith(ErrorDialog, expect.objectContaining({ title: "Verification failed" })),
);
});
it("should not do manual verification if cancelled", async () => {
// Given we say no to prompt
const spy = jest.spyOn(Modal, "createDialog");
spy.mockReturnValue({ finished: Promise.resolve([false]) } as unknown as IHandle<ComponentType>);
// When we run the command
const { client, command } = setUpCommandTest(roomId, `/verify`);
command.run(client, roomId, null, "mydeviceid myfingerprint");
// Then the prompt is displayed
expect(spy).toHaveBeenCalledWith(
QuestionDialog,
expect.objectContaining({ title: "Caution: manual device verification" }),
);
// But nothing else happens
expect(spy).not.toHaveBeenCalledWith(ErrorDialog, expect.anything());
});
});