From 6d7def7b10d2bb2cb099757d83d026230dd7bc33 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 3 Feb 2026 15:02:54 +0000 Subject: [PATCH] Move the upgraderoom command into its own subdirectory of slash-commands (#31941) * Move the code for running the upgrade command into its subdir * Replace use of deprecated currentState property in runUpgradeRoomCommand * Move upgraderoom command options into their own file * Move upgraderoom tests into their own file and add a couple --- src/SlashCommands.tsx | 51 +------ .../upgraderoom/runUpgradeRoomCommand.ts | 64 +++++++++ src/slash-commands/upgraderoom/upgraderoom.ts | 29 ++++ test/unit-tests/SlashCommands-test.tsx | 65 --------- .../slash-commands/upgraderoom-test.tsx | 136 ++++++++++++++++++ 5 files changed, 231 insertions(+), 114 deletions(-) create mode 100644 src/slash-commands/upgraderoom/runUpgradeRoomCommand.ts create mode 100644 src/slash-commands/upgraderoom/upgraderoom.ts create mode 100644 test/unit-tests/slash-commands/upgraderoom-test.tsx diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index e41a719cb0..0169513635 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -44,9 +44,7 @@ import { UIComponent, UIFeature } from "./settings/UIFeature"; import { CHAT_EFFECTS } from "./effects"; import LegacyCallHandler from "./LegacyCallHandler"; import { guessAndSetDMRoom } from "./Rooms"; -import { upgradeRoom } from "./utils/RoomUpgrade"; import DevtoolsDialog from "./components/views/dialogs/DevtoolsDialog"; -import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarningDialog"; import InfoDialog from "./components/views/dialogs/InfoDialog"; import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog"; import { shouldShowComponent } from "./customisations/helpers/UIComponents"; @@ -61,7 +59,7 @@ import { CommandCategories } from "./slash-commands/interface"; import { Command } from "./slash-commands/command"; import { goto, join } from "./slash-commands/join"; import { manuallyVerifyDevice } from "./components/views/dialogs/ManualDeviceKeyVerificationDialog"; -import { parseUpgradeRoomArgs } from "./slash-commands/upgraderoom/parseUpgradeRoomArgs"; +import upgraderoom from "./slash-commands/upgraderoom/upgraderoom"; export { CommandCategories, Command }; @@ -145,52 +143,7 @@ export const Commands = [ }, category: CommandCategories.messages, }), - new Command({ - command: "upgraderoom", - args: " [ ...]", - description: _td("slash_command|upgraderoom"), - isEnabled: (cli) => !isCurrentLocalRoom(cli), - runFn: function (cli, roomId, threadId, args) { - if (!args) { - return reject(this.getUsage()); - } - const parsedArgs = parseUpgradeRoomArgs(args); - if (parsedArgs) { - const room = cli.getRoom(roomId); - if (!room?.currentState.mayClientSendStateEvent("m.room.tombstone", cli)) { - return reject(new UserFriendlyError("slash_command|upgraderoom_permission_error")); - } - - const { finished } = Modal.createDialog( - RoomUpgradeWarningDialog, - { roomId: roomId, targetVersion: parsedArgs.targetVersion }, - /*className=*/ undefined, - /*isPriority=*/ false, - /*isStatic=*/ true, - ); - - return success( - finished.then(async ([resp]): Promise => { - if (!resp?.continue) return; - await upgradeRoom( - room, - parsedArgs.targetVersion, - resp.invite, - true, - true, - false, - undefined, - false, - parsedArgs.additionalCreators, - ); - }), - ); - } - return reject(this.getUsage()); - }, - category: CommandCategories.admin, - renderingTypes: [TimelineRenderingType.Room], - }), + upgraderoom, new Command({ command: "jumptodate", args: "", diff --git a/src/slash-commands/upgraderoom/runUpgradeRoomCommand.ts b/src/slash-commands/upgraderoom/runUpgradeRoomCommand.ts new file mode 100644 index 0000000000..e7e24ded75 --- /dev/null +++ b/src/slash-commands/upgraderoom/runUpgradeRoomCommand.ts @@ -0,0 +1,64 @@ +/* + * 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 { EventTimeline, type MatrixClient } from "matrix-js-sdk/src/matrix"; + +import Modal from "../../Modal"; +import RoomUpgradeWarningDialog from "../../components/views/dialogs/RoomUpgradeWarningDialog"; +import { type Command } from "../command"; +import { UserFriendlyError } from "../../languageHandler"; +import { parseUpgradeRoomArgs } from "./parseUpgradeRoomArgs"; +import { reject, success } from "../utils"; +import { type RunResult } from "../interface"; +import { upgradeRoom } from "../../utils/RoomUpgrade"; + +export function runUpgradeRoomCommand( + command: Command, + cli: MatrixClient, + roomId: string, + _threadId: string | null, + args?: string, +): RunResult { + if (!args) { + return reject(command.getUsage()); + } + const parsedArgs = parseUpgradeRoomArgs(args); + if (parsedArgs) { + const room = cli.getRoom(roomId); + if ( + !room?.getLiveTimeline().getState(EventTimeline.FORWARDS)?.mayClientSendStateEvent("m.room.tombstone", cli) + ) { + return reject(new UserFriendlyError("slash_command|upgraderoom_permission_error")); + } + + const { finished } = Modal.createDialog( + RoomUpgradeWarningDialog, + { roomId: roomId, targetVersion: parsedArgs.targetVersion }, + /*className=*/ undefined, + /*isPriority=*/ false, + /*isStatic=*/ true, + ); + + return success( + finished.then(async ([resp]): Promise => { + if (!resp?.continue) return; + await upgradeRoom( + room, + parsedArgs.targetVersion, + resp.invite, + true, + true, + false, + undefined, + false, + parsedArgs.additionalCreators, + ); + }), + ); + } + return reject(command.getUsage()); +} diff --git a/src/slash-commands/upgraderoom/upgraderoom.ts b/src/slash-commands/upgraderoom/upgraderoom.ts new file mode 100644 index 0000000000..8d7b17ea4d --- /dev/null +++ b/src/slash-commands/upgraderoom/upgraderoom.ts @@ -0,0 +1,29 @@ +/* + * 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 { type MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { _td } from "../../languageHandler"; +import { isCurrentLocalRoom } from "../utils"; +import { runUpgradeRoomCommand } from "./runUpgradeRoomCommand"; +import { Command } from "../command"; +import { CommandCategories, type RunResult } from "../interface"; +import { TimelineRenderingType } from "../../contexts/RoomContext"; + +const upgraderoom = new Command({ + command: "upgraderoom", + args: " [ ...]", + description: _td("slash_command|upgraderoom"), + isEnabled: (cli: MatrixClient) => !isCurrentLocalRoom(cli), + runFn: function (cli: MatrixClient, roomId: string, threadId: string | null, args?: string): RunResult { + return runUpgradeRoomCommand(this, cli, roomId, threadId, args); + }, + category: CommandCategories.admin, + renderingTypes: [TimelineRenderingType.Room], +}); + +export default upgraderoom; diff --git a/test/unit-tests/SlashCommands-test.tsx b/test/unit-tests/SlashCommands-test.tsx index dbbc69a72e..76be3fb92f 100644 --- a/test/unit-tests/SlashCommands-test.tsx +++ b/test/unit-tests/SlashCommands-test.tsx @@ -22,10 +22,6 @@ 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"; -import RoomUpgradeWarningDialog, { - type IFinishedOpts, -} from "../../src/components/views/dialogs/RoomUpgradeWarningDialog"; -import { parseUpgradeRoomArgs } from "../../src/slash-commands/upgraderoom/parseUpgradeRoomArgs"; jest.mock("../../src/components/views/right_panel/UserInfo"); @@ -123,67 +119,6 @@ describe("SlashCommands", () => { }); }); - describe("/upgraderoom", () => { - beforeEach(() => { - command = findCommand("upgraderoom")!; - setCurrentRoom(); - }); - - it("should be enabled by default", () => { - expect(command.isEnabled(client, roomId)).toBe(true); - }); - - it("should return usage if given no args", () => { - expect(command.run(client, roomId, null, undefined).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 we mock out creating dialogs and upgrading rooms - const createDialog = jest.spyOn(Modal, "createDialog"); - const upgradeRoom = jest.fn().mockResolvedValue({ replacement_room: "!newroom" }); - const resp: IFinishedOpts = { continue: true, invite: false }; - createDialog.mockReturnValue({ - finished: Promise.resolve([resp]), - close: jest.fn(), - }); - client.upgradeRoom = upgradeRoom; - - // When we run a room upgrade - 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"]); - }); - }); - describe("/op", () => { beforeEach(() => { command = findCommand("op")!; diff --git a/test/unit-tests/slash-commands/upgraderoom-test.tsx b/test/unit-tests/slash-commands/upgraderoom-test.tsx new file mode 100644 index 0000000000..88aea8a833 --- /dev/null +++ b/test/unit-tests/slash-commands/upgraderoom-test.tsx @@ -0,0 +1,136 @@ +/* + * 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 { mocked } from "jest-mock"; +import { type MatrixClient, Room } 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 { parseUpgradeRoomArgs } from "../../../src/slash-commands/upgraderoom/parseUpgradeRoomArgs"; +import Modal from "../../../src/Modal"; + +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. + * + * @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 = 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 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"]); + }); +});