Move SlashCommands and SlashCommands-test into subdirs (#31979)
* Move SlashCommands into slash-commands * Move SlashCommands test prep into a function that can be re-used from multiple files * Move slash command tests into separate files * Fix super-linear regexes and test some more slash commands * Move splitAtFirstSpace tests into a separate file * Test for parseCommandString * Make parseCommandString able to handle leading whitespace and tabs * Implement parseCommandString using splitAtFirstSpace * Extract emoticons slash commands into a separate file and share their code
This commit is contained in:
@@ -351,6 +351,10 @@ export function createTestClient(): MatrixClient {
|
||||
},
|
||||
search: jest.fn().mockResolvedValue({}),
|
||||
processRoomEventsSearch: jest.fn().mockResolvedValue({ highlights: [], results: [] }),
|
||||
invite: jest.fn(),
|
||||
kick: jest.fn(),
|
||||
ban: jest.fn(),
|
||||
sendTextMessage: jest.fn(),
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
client.reEmitter = new ReEmitter(client);
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
/*
|
||||
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, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { mocked } from "jest-mock";
|
||||
import { act, waitFor } from "jest-matrix-react";
|
||||
|
||||
import { type Command, Commands, getCommand } from "../../src/SlashCommands";
|
||||
import { createTestClient } from "../test-utils";
|
||||
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../src/models/LocalRoom";
|
||||
import { SdkContextClass } from "../../src/contexts/SDKContext";
|
||||
import Modal, { type ComponentType, type IHandle } from "../../src/Modal";
|
||||
import WidgetUtils from "../../src/utils/WidgetUtils";
|
||||
import { WidgetType } from "../../src/widgets/WidgetType";
|
||||
import { warnSelfDemote } from "../../src/components/views/right_panel/UserInfo";
|
||||
import dispatcher from "../../src/dispatcher/dispatcher";
|
||||
import QuestionDialog from "../../src/components/views/dialogs/QuestionDialog";
|
||||
import ErrorDialog from "../../src/components/views/dialogs/ErrorDialog";
|
||||
|
||||
jest.mock("../../src/components/views/right_panel/UserInfo");
|
||||
|
||||
describe("SlashCommands", () => {
|
||||
let client: MatrixClient;
|
||||
const roomId = "!room:example.com";
|
||||
let room: Room;
|
||||
const localRoomId = LOCAL_ROOM_ID_PREFIX + "test";
|
||||
let localRoom: LocalRoom;
|
||||
let command: Command;
|
||||
|
||||
const findCommand = (cmd: string): Command | undefined => {
|
||||
return Commands.find((command: Command) => command.command === cmd);
|
||||
};
|
||||
|
||||
const setCurrentRoom = (): void => {
|
||||
mocked(SdkContextClass.instance.roomViewStore.getRoomId).mockReturnValue(roomId);
|
||||
mocked(client.getRoom).mockImplementation((rId: string): Room | null => {
|
||||
if (rId === roomId) return room;
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
const setCurrentLocalRoom = (): void => {
|
||||
mocked(SdkContextClass.instance.roomViewStore.getRoomId).mockReturnValue(localRoomId);
|
||||
mocked(client.getRoom).mockImplementation((rId: string): Room | null => {
|
||||
if (rId === localRoomId) return localRoom;
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
client = createTestClient();
|
||||
|
||||
room = new Room(roomId, client, client.getSafeUserId());
|
||||
localRoom = new LocalRoom(localRoomId, client, client.getSafeUserId());
|
||||
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId");
|
||||
});
|
||||
|
||||
describe("/topic", () => {
|
||||
it("sets topic", async () => {
|
||||
const command = getCommand(roomId, "/topic pizza");
|
||||
expect(command.cmd).toBeDefined();
|
||||
expect(command.args).toBeDefined();
|
||||
await command.cmd!.run(client, "room-id", null, command.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 command = getCommand(roomId, "/topic")!;
|
||||
await command.cmd!.run(client, roomId, null);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
["myroomnick"],
|
||||
["roomavatar"],
|
||||
["myroomavatar"],
|
||||
["topic"],
|
||||
["roomname"],
|
||||
["invite"],
|
||||
["part"],
|
||||
["remove"],
|
||||
["ban"],
|
||||
["unban"],
|
||||
["op"],
|
||||
["deop"],
|
||||
["addwidget"],
|
||||
["discardsession"],
|
||||
["whois"],
|
||||
["holdcall"],
|
||||
["unholdcall"],
|
||||
["converttodm"],
|
||||
["converttoroom"],
|
||||
])("/%s", (commandName: string) => {
|
||||
beforeEach(() => {
|
||||
command = findCommand(commandName)!;
|
||||
});
|
||||
|
||||
describe("isEnabled", () => {
|
||||
it("should return true for Room", () => {
|
||||
setCurrentRoom();
|
||||
expect(command.isEnabled(client, roomId)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for LocalRoom", () => {
|
||||
setCurrentLocalRoom();
|
||||
expect(command.isEnabled(client, roomId)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("/op", () => {
|
||||
beforeEach(() => {
|
||||
command = findCommand("op")!;
|
||||
});
|
||||
|
||||
it("should return usage if no args", () => {
|
||||
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
|
||||
});
|
||||
|
||||
it("should reject with usage if given an invalid power level value", () => {
|
||||
expect(command.run(client, roomId, null, "@bob:server Admin").error).toBe(command.getUsage());
|
||||
});
|
||||
|
||||
it("should reject with usage for invalid input", () => {
|
||||
expect(command.run(client, roomId, null, " ").error).toBe(command.getUsage());
|
||||
});
|
||||
|
||||
it("should warn about self demotion", async () => {
|
||||
setCurrentRoom();
|
||||
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 () => {
|
||||
setCurrentRoom();
|
||||
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", () => {
|
||||
beforeEach(() => {
|
||||
command = findCommand("deop")!;
|
||||
});
|
||||
|
||||
it("should return usage if no args", () => {
|
||||
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
|
||||
});
|
||||
|
||||
it("should warn about self demotion", async () => {
|
||||
setCurrentRoom();
|
||||
const member = new RoomMember(roomId, client.getSafeUserId());
|
||||
member.membership = KnownMembership.Join;
|
||||
member.powerLevel = 100;
|
||||
room.getMember = () => member;
|
||||
command.run(client, roomId, null, client.getSafeUserId());
|
||||
expect(warnSelfDemote).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should reject with usage for invalid input", () => {
|
||||
expect(command.run(client, roomId, null, " ").error).toBe(command.getUsage());
|
||||
});
|
||||
});
|
||||
|
||||
describe("/part", () => {
|
||||
it("should part room matching alias if found", async () => {
|
||||
const room1 = new Room("room-id", client, client.getSafeUserId());
|
||||
room1.getCanonicalAlias = jest.fn().mockReturnValue("#foo:bar");
|
||||
const room2 = new Room("other-room", client, client.getSafeUserId());
|
||||
room2.getCanonicalAlias = jest.fn().mockReturnValue("#baz:bar");
|
||||
mocked(client.getRooms).mockReturnValue([room1, room2]);
|
||||
|
||||
const command = getCommand(room1.roomId, "/part #foo:bar");
|
||||
expect(command.cmd).toBeDefined();
|
||||
expect(command.args).toBeDefined();
|
||||
await command.cmd!.run(client, room1.roomId, null, command.args);
|
||||
expect(client.leaveRoomChain).toHaveBeenCalledWith(room1.roomId, expect.anything());
|
||||
});
|
||||
|
||||
it("should part room matching alt alias if found", async () => {
|
||||
const room1 = new Room("room-id", client, client.getSafeUserId());
|
||||
room1.getAltAliases = jest.fn().mockReturnValue(["#foo:bar"]);
|
||||
const room2 = new Room("other-room", client, client.getSafeUserId());
|
||||
room2.getAltAliases = jest.fn().mockReturnValue(["#baz:bar"]);
|
||||
mocked(client.getRooms).mockReturnValue([room1, room2]);
|
||||
|
||||
const command = getCommand(room1.roomId, "/part #foo:bar");
|
||||
expect(command.cmd).toBeDefined();
|
||||
expect(command.args).toBeDefined();
|
||||
await command.cmd!.run(client, room1.roomId, null, command.args!);
|
||||
expect(client.leaveRoomChain).toHaveBeenCalledWith(room1.roomId, expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(["rainbow", "rainbowme"])("/%s", (commandName: string) => {
|
||||
const command = findCommand(commandName)!;
|
||||
|
||||
it("should return usage if no args", () => {
|
||||
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
|
||||
});
|
||||
|
||||
it("should make things rainbowy", () => {
|
||||
return expect(
|
||||
command.run(client, roomId, null, "this is a test message").promise,
|
||||
).resolves.toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(["shrug", "tableflip", "unflip", "lenny"])("/%s", (commandName: string) => {
|
||||
const command = findCommand(commandName)!;
|
||||
|
||||
it("should match snapshot with no args", () => {
|
||||
return expect(command.run(client, roomId, null).promise).resolves.toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot with args", () => {
|
||||
return expect(
|
||||
command.run(client, roomId, null, "this is a test message").promise,
|
||||
).resolves.toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("/verify", () => {
|
||||
it("should return usage if no args", () => {
|
||||
const command = findCommand("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 command = findCommand("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 command = findCommand("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());
|
||||
});
|
||||
});
|
||||
|
||||
describe("/addwidget", () => {
|
||||
it("should parse html iframe snippets", async () => {
|
||||
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
|
||||
const spy = jest.spyOn(WidgetUtils, "setRoomWidget");
|
||||
const command = findCommand("addwidget")!;
|
||||
await command.run(client, roomId, null, '<iframe src="https://element.io"></iframe>');
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
client,
|
||||
roomId,
|
||||
expect.any(String),
|
||||
WidgetType.CUSTOM,
|
||||
"https://element.io",
|
||||
"Custom",
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("/join", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(dispatcher, "dispatch");
|
||||
command = findCommand(KnownMembership.Join)!;
|
||||
});
|
||||
|
||||
it("should return usage if no args", () => {
|
||||
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
|
||||
});
|
||||
|
||||
it("should handle matrix.org permalinks", () => {
|
||||
command.run(client, roomId, null, "https://matrix.to/#/!roomId:server/$eventId");
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "view_room",
|
||||
room_id: "!roomId:server",
|
||||
event_id: "$eventId",
|
||||
highlighted: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle room aliases", () => {
|
||||
command.run(client, roomId, null, "#test:server");
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "view_room",
|
||||
room_alias: "#test:server",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle room aliases with no server component", () => {
|
||||
command.run(client, roomId, null, "#test");
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "view_room",
|
||||
room_alias: `#test:${client.getDomain()}`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle room IDs and via servers", () => {
|
||||
command.run(client, roomId, null, "!foo:bar serv1.com serv2.com");
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "view_room",
|
||||
room_id: "!foo:bar",
|
||||
via_servers: ["serv1.com", "serv2.com"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ import { stubClient } from "../../test-utils";
|
||||
import { Command } from "../../../src/slash-commands/command";
|
||||
import { CommandCategories } from "../../../src/slash-commands/interface";
|
||||
import { _td } from "../../../src/languageHandler";
|
||||
import * as SlashCommands from "../../../src/SlashCommands";
|
||||
import * as SlashCommands from "../../../src/slash-commands/SlashCommands";
|
||||
|
||||
describe("CommandProvider", () => {
|
||||
let room: Room;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { stubClient } from "../../../../test-utils";
|
||||
import { Command } from "../../../../../src/slash-commands/command";
|
||||
import { CommandCategories } from "../../../../../src/slash-commands/interface";
|
||||
import { _t, _td } from "../../../../../src/languageHandler";
|
||||
import * as SlashCommands from "../../../../../src/SlashCommands";
|
||||
import * as SlashCommands from "../../../../../src/slash-commands/SlashCommands";
|
||||
|
||||
describe("SlashCommandHelpDialog", () => {
|
||||
const roomId = "!room:server";
|
||||
|
||||
@@ -19,7 +19,7 @@ import SettingsStore from "../../../../../../../src/settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../../../../../src/settings/SettingLevel";
|
||||
import EditorStateTransfer from "../../../../../../../src/utils/EditorStateTransfer";
|
||||
import * as ConfirmRedactDialog from "../../../../../../../src/components/views/dialogs/ConfirmRedactDialog";
|
||||
import * as SlashCommands from "../../../../../../../src/SlashCommands";
|
||||
import * as SlashCommands from "../../../../../../../src/slash-commands/SlashCommands";
|
||||
import * as Commands from "../../../../../../../src/editor/commands";
|
||||
import * as Reply from "../../../../../../../src/utils/Reply";
|
||||
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
`;
|
||||
@@ -1,20 +1,6 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`SlashCommands /lenny should match snapshot with args 1`] = `
|
||||
{
|
||||
"body": "( ͡° ͜ʖ ͡°) this is a test message",
|
||||
"msgtype": "m.text",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`SlashCommands /lenny should match snapshot with no args 1`] = `
|
||||
{
|
||||
"body": "( ͡° ͜ʖ ͡°)",
|
||||
"msgtype": "m.text",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`SlashCommands /rainbow should make things rainbowy 1`] = `
|
||||
exports[`/rainbow should make things rainbowy 1`] = `
|
||||
{
|
||||
"body": "this is a test message",
|
||||
"format": "org.matrix.custom.html",
|
||||
@@ -23,7 +9,7 @@ exports[`SlashCommands /rainbow should make things rainbowy 1`] = `
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`SlashCommands /rainbowme should make things rainbowy 1`] = `
|
||||
exports[`/rainbowme should make things rainbowy 1`] = `
|
||||
{
|
||||
"body": "this is a test message",
|
||||
"format": "org.matrix.custom.html",
|
||||
@@ -31,45 +17,3 @@ exports[`SlashCommands /rainbowme should make things rainbowy 1`] = `
|
||||
"msgtype": "m.emote",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`SlashCommands /shrug should match snapshot with args 1`] = `
|
||||
{
|
||||
"body": "¯\\_(ツ)_/¯ this is a test message",
|
||||
"msgtype": "m.text",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`SlashCommands /shrug should match snapshot with no args 1`] = `
|
||||
{
|
||||
"body": "¯\\_(ツ)_/¯",
|
||||
"msgtype": "m.text",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`SlashCommands /tableflip should match snapshot with args 1`] = `
|
||||
{
|
||||
"body": "(╯°□°)╯︵ ┻━┻ this is a test message",
|
||||
"msgtype": "m.text",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`SlashCommands /tableflip should match snapshot with no args 1`] = `
|
||||
{
|
||||
"body": "(╯°□°)╯︵ ┻━┻",
|
||||
"msgtype": "m.text",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`SlashCommands /unflip should match snapshot with args 1`] = `
|
||||
{
|
||||
"body": "┬──┬ ノ( ゜-゜ノ) this is a test message",
|
||||
"msgtype": "m.text",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`SlashCommands /unflip should match snapshot with no args 1`] = `
|
||||
{
|
||||
"body": "┬──┬ ノ( ゜-゜ノ)",
|
||||
"msgtype": "m.text",
|
||||
}
|
||||
`;
|
||||
39
test/unit-tests/slash-commands/addwidget-test.ts
Normal file
39
test/unit-tests/slash-commands/addwidget-test.ts
Normal file
@@ -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",
|
||||
{},
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
33
test/unit-tests/slash-commands/ban-test.ts
Normal file
33
test/unit-tests/slash-commands/ban-test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
25
test/unit-tests/slash-commands/emoticons-test.ts
Normal file
25
test/unit-tests/slash-commands/emoticons-test.ts
Normal file
@@ -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(["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}`);
|
||||
|
||||
await expect(command.run(client, roomId, null, "this is a test message").promise).resolves.toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
40
test/unit-tests/slash-commands/invite-test.ts
Normal file
40
test/unit-tests/slash-commands/invite-test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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", {});
|
||||
});
|
||||
|
||||
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" });
|
||||
});
|
||||
});
|
||||
79
test/unit-tests/slash-commands/join-test.ts
Normal file
79
test/unit-tests/slash-commands/join-test.ts
Normal file
@@ -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"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
46
test/unit-tests/slash-commands/msg-test.ts
Normal file
46
test/unit-tests/slash-commands/msg-test.ts
Normal file
@@ -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",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
77
test/unit-tests/slash-commands/op-test.ts
Normal file
77
test/unit-tests/slash-commands/op-test.ts
Normal file
@@ -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());
|
||||
});
|
||||
});
|
||||
22
test/unit-tests/slash-commands/parse-command-string-test.ts
Normal file
22
test/unit-tests/slash-commands/parse-command-string-test.ts
Normal file
@@ -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" });
|
||||
});
|
||||
});
|
||||
72
test/unit-tests/slash-commands/part-test.ts
Normal file
72
test/unit-tests/slash-commands/part-test.ts
Normal file
@@ -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());
|
||||
});
|
||||
});
|
||||
25
test/unit-tests/slash-commands/rainbow-test.ts
Normal file
25
test/unit-tests/slash-commands/rainbow-test.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
33
test/unit-tests/slash-commands/remove-test.ts
Normal file
33
test/unit-tests/slash-commands/remove-test.ts
Normal file
@@ -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");
|
||||
});
|
||||
});
|
||||
22
test/unit-tests/slash-commands/split-at-first-space-test.ts
Normal file
22
test/unit-tests/slash-commands/split-at-first-space-test.ts
Normal file
@@ -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"]);
|
||||
});
|
||||
});
|
||||
31
test/unit-tests/slash-commands/topic-test.ts
Normal file
31
test/unit-tests/slash-commands/topic-test.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -1,29 +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 { mocked } from "jest-mock";
|
||||
import { type MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import RoomUpgradeWarningDialog, {
|
||||
type IFinishedOpts,
|
||||
} from "../../../src/components/views/dialogs/RoomUpgradeWarningDialog";
|
||||
import { type Command, Commands } from "../../../src/SlashCommands";
|
||||
import { SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||
import { createTestClient } from "../../test-utils";
|
||||
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";
|
||||
|
||||
function findCommand(cmd: string): Command | undefined {
|
||||
return Commands.find((command: Command) => command.command === cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up an upgraderoom test.
|
||||
*
|
||||
@@ -39,15 +35,7 @@ describe("/upgraderoom", () => {
|
||||
} {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const command = findCommand("upgraderoom")!;
|
||||
const client = createTestClient();
|
||||
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId");
|
||||
mocked(SdkContextClass.instance.roomViewStore.getRoomId).mockReturnValue(roomId);
|
||||
mocked(client.getRoom).mockImplementation((rId: string): Room | null => {
|
||||
if (rId === roomId) return new Room(roomId, client, client.getSafeUserId());
|
||||
return null;
|
||||
});
|
||||
const { command, client } = setUpCommandTest(roomId, "/upgraderoom");
|
||||
|
||||
const createDialog = jest.spyOn(Modal, "createDialog");
|
||||
const upgradeRoom = jest.fn().mockResolvedValue({ replacement_room: "!newroom" });
|
||||
|
||||
54
test/unit-tests/slash-commands/utils.ts
Normal file
54
test/unit-tests/slash-commands/utils.ts
Normal file
@@ -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 };
|
||||
}
|
||||
64
test/unit-tests/slash-commands/verify-test.ts
Normal file
64
test/unit-tests/slash-commands/verify-test.ts
Normal file
@@ -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());
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user