diff --git a/src/components/viewmodels/right_panel/UserInfoPowerlevelViewModel.tsx b/src/components/viewmodels/right_panel/UserInfoPowerlevelViewModel.tsx
new file mode 100644
index 00000000000..f56356ff411
--- /dev/null
+++ b/src/components/viewmodels/right_panel/UserInfoPowerlevelViewModel.tsx
@@ -0,0 +1,108 @@
+/*
+Copyright 2025 New Vector Ltd.
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
+Please see LICENSE files in the repository root for full details.
+*/
+
+import React, { useContext, useEffect, useState, useCallback } from "react";
+import { logger } from "@sentry/browser";
+import { type RoomMember, type Room } from "matrix-js-sdk/src/matrix";
+
+import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import { _t } from "../../../languageHandler";
+import Modal from "../../../Modal";
+import ErrorDialog from "../../views/dialogs/ErrorDialog";
+import QuestionDialog from "../../views/dialogs/QuestionDialog";
+import { warnSelfDemote } from "../../views/right_panel/UserInfo";
+
+/**
+ *
+ */
+export interface UserInfoPowerLevelState {
+    /**
+     * default power level value of the selected user
+     */
+    powerLevelUsersDefault: number;
+    /**
+     * The new power level to apply
+     */
+    selectedPowerLevel: number;
+    /**
+     * Method to call When power level selection change
+     */
+    onPowerChange: (powerLevel: number) => void;
+}
+
+export const useUserInfoPowerlevelViewModel = (user: RoomMember, room: Room): UserInfoPowerLevelState => {
+    const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
+
+    useEffect(() => {
+        setSelectedPowerLevel(user.powerLevel);
+    }, [user]);
+
+    const cli = useContext(MatrixClientContext);
+    const onPowerChange = useCallback(
+        async (powerLevel: number) => {
+            setSelectedPowerLevel(powerLevel);
+
+            const applyPowerChange = (roomId: string, target: string, powerLevel: number): Promise<unknown> => {
+                return cli.setPowerLevel(roomId, target, powerLevel).then(
+                    function () {
+                        logger.info("Power change success");
+                    },
+                    function (err) {
+                        logger.error("Failed to change power level " + err);
+                        Modal.createDialog(ErrorDialog, {
+                            title: _t("common|error"),
+                            description: _t("error|update_power_level"),
+                        });
+                    },
+                );
+            };
+
+            const roomId = user.roomId;
+            const target = user.userId;
+
+            const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
+            if (!powerLevelEvent) return;
+
+            const myUserId = cli.getUserId();
+            const myPower = powerLevelEvent.getContent().users[myUserId || ""];
+            if (myPower && parseInt(myPower) <= powerLevel && myUserId !== target) {
+                const { finished } = Modal.createDialog(QuestionDialog, {
+                    title: _t("common|warning"),
+                    description: (
+                        <div>
+                            {_t("user_info|promote_warning")}
+                            <br />
+                            {_t("common|are_you_sure")}
+                        </div>
+                    ),
+                    button: _t("action|continue"),
+                });
+
+                const [confirmed] = await finished;
+                if (!confirmed) return;
+            } else if (myUserId === target && myPower && parseInt(myPower) > powerLevel) {
+                // If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
+                try {
+                    if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
+                } catch (e) {
+                    logger.error("Failed to warn about self demotion: " + e);
+                }
+            }
+
+            await applyPowerChange(roomId, target, powerLevel);
+        },
+        [user.roomId, user.userId, cli, room],
+    );
+
+    const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
+    const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
+
+    return {
+        powerLevelUsersDefault,
+        onPowerChange,
+        selectedPowerLevel,
+    };
+};
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index 542e6421dee..970f8a22786 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -43,7 +43,6 @@ import { type ButtonEvent } from "../elements/AccessibleButton";
 import SdkConfig from "../../../SdkConfig";
 import MultiInviter from "../../../utils/MultiInviter";
 import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
-import { textualPowerLevel } from "../../../Roles";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
 import EncryptionPanel from "./EncryptionPanel";
@@ -54,7 +53,6 @@ import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
 import BaseCard from "./BaseCard";
 import ImageView from "../elements/ImageView";
 import Spinner from "../elements/Spinner";
-import PowerSelector from "../elements/PowerSelector";
 import MemberAvatar from "../avatars/MemberAvatar";
 import PresenceLabel from "../rooms/PresenceLabel";
 import { ShareDialog } from "../dialogs/ShareDialog";
@@ -76,6 +74,7 @@ import { Flex } from "../../utils/Flex";
 import CopyableText from "../elements/CopyableText";
 import { useUserTimezone } from "../../../hooks/useUserTimezone";
 import { UserInfoAdminToolsContainer } from "./user_info/UserInfoAdminToolsContainer";
+import { PowerLevelSection } from "./user_info/UserInfoPowerLevels";
 
 export interface IDevice extends Device {
     ambiguous?: boolean;
@@ -437,7 +436,7 @@ const useHomeserverSupportsCrossSigning = (cli: MatrixClient): boolean => {
     );
 };
 
-interface IRoomPermissions {
+export interface IRoomPermissions {
     modifyLevelMax: number;
     canEdit: boolean;
     canInvite: boolean;
@@ -492,112 +491,6 @@ function useRoomPermissions(cli: MatrixClient, room: Room, user: RoomMember): IR
     return roomPermissions;
 }
 
-const PowerLevelSection: React.FC<{
-    user: RoomMember;
-    room: Room;
-    roomPermissions: IRoomPermissions;
-    powerLevels: IPowerLevelsContent;
-}> = ({ user, room, roomPermissions, powerLevels }) => {
-    if (roomPermissions.canEdit) {
-        return <PowerLevelEditor user={user} room={room} roomPermissions={roomPermissions} />;
-    } else {
-        const powerLevelUsersDefault = powerLevels.users_default || 0;
-        const powerLevel = user.powerLevel;
-        const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
-        return (
-            <div className="mx_UserInfo_profileField">
-                <div className="mx_UserInfo_roleDescription">{role}</div>
-            </div>
-        );
-    }
-};
-
-export const PowerLevelEditor: React.FC<{
-    user: RoomMember;
-    room: Room;
-    roomPermissions: IRoomPermissions;
-}> = ({ user, room, roomPermissions }) => {
-    const cli = useContext(MatrixClientContext);
-
-    const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
-    useEffect(() => {
-        setSelectedPowerLevel(user.powerLevel);
-    }, [user]);
-
-    const onPowerChange = useCallback(
-        async (powerLevel: number) => {
-            setSelectedPowerLevel(powerLevel);
-
-            const applyPowerChange = (roomId: string, target: string, powerLevel: number): Promise<unknown> => {
-                return cli.setPowerLevel(roomId, target, powerLevel).then(
-                    function () {
-                        // NO-OP; rely on the m.room.member event coming down else we could
-                        // get out of sync if we force setState here!
-                        logger.log("Power change success");
-                    },
-                    function (err) {
-                        logger.error("Failed to change power level " + err);
-                        Modal.createDialog(ErrorDialog, {
-                            title: _t("common|error"),
-                            description: _t("error|update_power_level"),
-                        });
-                    },
-                );
-            };
-
-            const roomId = user.roomId;
-            const target = user.userId;
-
-            const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
-            if (!powerLevelEvent) return;
-
-            const myUserId = cli.getUserId();
-            const myPower = powerLevelEvent.getContent().users[myUserId || ""];
-            if (myPower && parseInt(myPower) <= powerLevel && myUserId !== target) {
-                const { finished } = Modal.createDialog(QuestionDialog, {
-                    title: _t("common|warning"),
-                    description: (
-                        <div>
-                            {_t("user_info|promote_warning")}
-                            <br />
-                            {_t("common|are_you_sure")}
-                        </div>
-                    ),
-                    button: _t("action|continue"),
-                });
-
-                const [confirmed] = await finished;
-                if (!confirmed) return;
-            } else if (myUserId === target && myPower && parseInt(myPower) > powerLevel) {
-                // If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
-                try {
-                    if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
-                } catch (e) {
-                    logger.error("Failed to warn about self demotion: ", e);
-                }
-            }
-
-            await applyPowerChange(roomId, target, powerLevel);
-        },
-        [user.roomId, user.userId, cli, room],
-    );
-
-    const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
-    const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
-
-    return (
-        <div className="mx_UserInfo_profileField">
-            <PowerSelector
-                label={undefined}
-                value={selectedPowerLevel}
-                maxValue={roomPermissions.modifyLevelMax}
-                usersDefault={powerLevelUsersDefault}
-                onChange={onPowerChange}
-            />
-        </div>
-    );
-};
-
 async function getUserDeviceInfo(
     userId: string,
     cli: MatrixClient,
@@ -820,12 +713,7 @@ const BasicUserInfo: React.FC<{
         // hide the Roles section for DMs as it doesn't make sense there
         if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) {
             memberDetails = (
-                <PowerLevelSection
-                    powerLevels={powerLevels}
-                    user={member as RoomMember}
-                    room={room}
-                    roomPermissions={roomPermissions}
-                />
+                <PowerLevelSection user={member as RoomMember} room={room} roomPermissions={roomPermissions} />
             );
         }
 
diff --git a/src/components/views/right_panel/user_info/UserInfoPowerLevels.tsx b/src/components/views/right_panel/user_info/UserInfoPowerLevels.tsx
new file mode 100644
index 00000000000..c7c42da559d
--- /dev/null
+++ b/src/components/views/right_panel/user_info/UserInfoPowerLevels.tsx
@@ -0,0 +1,53 @@
+/*
+Copyright 2025 New Vector Ltd.
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
+Please see LICENSE files in the repository root for full details.
+*/
+
+import React from "react";
+import { type RoomMember, type Room } from "matrix-js-sdk/src/matrix";
+
+import { textualPowerLevel } from "../../../../Roles";
+import PowerSelector from "../../elements/PowerSelector";
+import { type IRoomPermissions } from "../UserInfo";
+import {
+    type UserInfoPowerLevelState,
+    useUserInfoPowerlevelViewModel,
+} from "../../../viewmodels/right_panel/UserInfoPowerlevelViewModel";
+
+export const PowerLevelSection: React.FC<{
+    user: RoomMember;
+    room: Room;
+    roomPermissions: IRoomPermissions;
+}> = ({ user, room, roomPermissions }) => {
+    const vm = useUserInfoPowerlevelViewModel(user, room);
+
+    if (roomPermissions.canEdit) {
+        return <PowerLevelEditor vm={vm} roomPermissions={roomPermissions} />;
+    }
+
+    const powerLevel = user.powerLevel;
+    const role = textualPowerLevel(powerLevel, vm.powerLevelUsersDefault);
+    return (
+        <div className="mx_UserInfo_profileField">
+            <div className="mx_UserInfo_roleDescription">{role}</div>
+        </div>
+    );
+};
+
+export const PowerLevelEditor: React.FC<{
+    vm: UserInfoPowerLevelState;
+    roomPermissions: IRoomPermissions;
+}> = ({ vm, roomPermissions }) => {
+    return (
+        <div className="mx_UserInfo_profileField">
+            <PowerSelector
+                label={undefined}
+                value={vm.selectedPowerLevel}
+                maxValue={roomPermissions.modifyLevelMax}
+                usersDefault={vm.powerLevelUsersDefault}
+                onChange={vm.onPowerChange}
+            />
+        </div>
+    );
+};
diff --git a/test/unit-tests/components/viewmodels/right_panel/user_info/UserInfoPowerLevelsViewModel-test.tsx b/test/unit-tests/components/viewmodels/right_panel/user_info/UserInfoPowerLevelsViewModel-test.tsx
new file mode 100644
index 00000000000..d69dc127798
--- /dev/null
+++ b/test/unit-tests/components/viewmodels/right_panel/user_info/UserInfoPowerLevelsViewModel-test.tsx
@@ -0,0 +1,222 @@
+/*
+Copyright 2025 New Vector Ltd.
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
+Please see LICENSE files in the repository root for full details.
+*/
+
+import { renderHook } from "jest-matrix-react";
+import { type Mocked, mocked } from "jest-mock";
+import { RoomMember, MatrixEvent, type Room, EventType, type MatrixClient } from "matrix-js-sdk/src/matrix";
+
+import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
+import { useUserInfoPowerlevelViewModel } from "../../../../../../src/components/viewmodels/right_panel/UserInfoPowerlevelViewModel";
+import { withClientContextRenderOptions } from "../../../../../test-utils";
+import { type IRoomPermissions } from "../../../../../../src/components/views/right_panel/UserInfo";
+import Modal from "../../../../../../src/Modal";
+import { warnSelfDemote } from "../../../../../../src/components/views/right_panel/UserInfo";
+
+jest.mock("../../../../../../src/Modal", () => ({
+    createDialog: jest.fn(),
+}));
+
+jest.mock("../../../../../../src/components/views/right_panel/UserInfo", () => ({
+    warnSelfDemote: jest.fn(),
+}));
+
+describe("UserInfoAdminPowerlevelViewModel", () => {
+    const defaultRoomId = "!fkfk";
+    const defaultUserId = "@user:example.com";
+    const defaultMeId = "@me:example.com";
+    const selfUser = new RoomMember(defaultRoomId, defaultMeId);
+    const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
+    const startPowerLevel = 50;
+    const changedPowerLevel = 100;
+
+    let mockClient: Mocked<MatrixClient>;
+    let mockRoom: Mocked<Room>;
+    let defaultProps: {
+        user: RoomMember;
+        room: Room;
+        roomPermissions: IRoomPermissions;
+    };
+
+    beforeEach(() => {
+        defaultProps = {
+            user: defaultMember,
+            room: mockRoom,
+            roomPermissions: {
+                modifyLevelMax: 100,
+                canEdit: false,
+                canInvite: false,
+            },
+        };
+
+        mockRoom = mocked({
+            roomId: defaultRoomId,
+            getType: jest.fn().mockReturnValue(undefined),
+            isSpaceRoom: jest.fn().mockReturnValue(false),
+            getMember: jest.fn().mockReturnValue(undefined),
+            getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"),
+            name: "test room",
+            on: jest.fn(),
+            off: jest.fn(),
+            currentState: {
+                getStateEvents: jest.fn(),
+                on: jest.fn(),
+                off: jest.fn(),
+            },
+            getEventReadUpTo: jest.fn(),
+        } as unknown as Room);
+
+        mockClient = mocked({
+            getUser: jest.fn(),
+            isGuest: jest.fn().mockReturnValue(false),
+            isUserIgnored: jest.fn(),
+            getIgnoredUsers: jest.fn(),
+            setIgnoredUsers: jest.fn(),
+            getUserId: jest.fn(),
+            getSafeUserId: jest.fn(),
+            getDomain: jest.fn(),
+            on: jest.fn(),
+            off: jest.fn(),
+            isSynapseAdministrator: jest.fn().mockResolvedValue(false),
+            doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
+            doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false),
+            getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")),
+            mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
+            removeListener: jest.fn(),
+            currentState: {
+                on: jest.fn(),
+            },
+            getRoom: jest.fn(),
+            credentials: {},
+            setPowerLevel: jest.fn().mockResolvedValueOnce({ event_id: "123" }),
+        } as unknown as MatrixClient);
+
+        jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
+        jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
+
+        (Modal.createDialog as jest.Mock).mockImplementation(() => ({
+            finished: Promise.resolve([true]),
+        }));
+        (warnSelfDemote as jest.Mock).mockResolvedValue(true);
+    });
+
+    const renderComponentHook = (props = defaultProps, client = mockClient) => {
+        return renderHook(
+            () => useUserInfoPowerlevelViewModel(props.user, props.room),
+            withClientContextRenderOptions(client),
+        );
+    };
+
+    afterEach(() => {
+        jest.clearAllMocks();
+    });
+
+    it("should give default power level", () => {
+        const defaultPowerLevel = 1;
+        const powerLevelEvent = new MatrixEvent({
+            type: EventType.RoomPowerLevels,
+            content: { users: { [defaultUserId]: defaultPowerLevel }, users_default: defaultPowerLevel },
+        });
+        mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent);
+
+        const { result } = renderComponentHook({ ...defaultProps, room: mockRoom });
+
+        expect(result.current.powerLevelUsersDefault).toBe(defaultPowerLevel);
+    });
+
+    it("handles successful power level change", async () => {
+        const powerLevelEvent = new MatrixEvent({
+            type: EventType.RoomPowerLevels,
+            content: { users: { [defaultUserId]: startPowerLevel }, users_default: 1 },
+        });
+        mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent);
+        mockClient.getSafeUserId.mockReturnValueOnce(defaultUserId);
+        mockClient.getUserId.mockReturnValueOnce(defaultUserId);
+
+        const { result } = renderComponentHook({ ...defaultProps, room: mockRoom }, mockClient);
+
+        await result.current.onPowerChange(changedPowerLevel);
+
+        expect(mockClient.setPowerLevel).toHaveBeenCalledTimes(1);
+        expect(mockClient.setPowerLevel).toHaveBeenCalledWith(mockRoom.roomId, defaultMember.userId, changedPowerLevel);
+    });
+
+    it("shows warning when promoting user to higher power level", async () => {
+        const powerLevelEvent = new MatrixEvent({
+            type: EventType.RoomPowerLevels,
+            content: {
+                users: {
+                    [defaultUserId]: startPowerLevel,
+                    [defaultMeId]: startPowerLevel,
+                },
+                users_default: 1,
+            },
+        });
+        mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent);
+        mockClient.getUserId.mockReturnValue(defaultMeId);
+
+        const { result } = renderComponentHook({ ...defaultProps, room: mockRoom }, mockClient);
+
+        await result.current.onPowerChange(changedPowerLevel);
+
+        expect(Modal.createDialog).toHaveBeenCalled();
+        expect(mockClient.setPowerLevel).toHaveBeenCalled();
+    });
+
+    it("shows warning when self-demoting", async () => {
+        const powerLevelEvent = new MatrixEvent({
+            type: EventType.RoomPowerLevels,
+            content: {
+                users: { [defaultMeId]: changedPowerLevel },
+                users_default: 1,
+            },
+        });
+        mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent);
+        mockClient.getUserId.mockReturnValue(defaultMeId);
+
+        const { result } = renderComponentHook({ ...defaultProps, room: mockRoom, user: selfUser }, mockClient);
+
+        await result.current.onPowerChange(startPowerLevel);
+
+        expect(warnSelfDemote).toHaveBeenCalled();
+        expect(mockClient.setPowerLevel).toHaveBeenCalled();
+    });
+
+    it("cancels power level change when user declines warning", async () => {
+        (Modal.createDialog as jest.Mock).mockImplementation(() => ({
+            finished: Promise.resolve([false]),
+        }));
+
+        const powerLevelEvent = new MatrixEvent({
+            type: EventType.RoomPowerLevels,
+            content: {
+                users: {
+                    [defaultUserId]: startPowerLevel,
+                    "@me:example.com": startPowerLevel,
+                },
+                users_default: 1,
+            },
+        });
+        mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent);
+        mockClient.getUserId.mockReturnValue(defaultMeId);
+
+        const { result } = renderComponentHook({ ...defaultProps, room: mockRoom }, mockClient);
+
+        await result.current.onPowerChange(changedPowerLevel);
+
+        expect(Modal.createDialog).toHaveBeenCalled();
+        expect(mockClient.setPowerLevel).not.toHaveBeenCalled();
+    });
+
+    it("handles missing power level event", async () => {
+        mockRoom.currentState.getStateEvents.mockReturnValue(null);
+
+        const { result } = renderComponentHook({ ...defaultProps, room: mockRoom }, mockClient);
+
+        await result.current.onPowerChange(changedPowerLevel);
+
+        expect(mockClient.setPowerLevel).not.toHaveBeenCalled();
+    });
+});
diff --git a/test/unit-tests/components/views/right_panel/UserInfo-test.tsx b/test/unit-tests/components/views/right_panel/UserInfo-test.tsx
index ce0edca0ac0..d019e2147b5 100644
--- a/test/unit-tests/components/views/right_panel/UserInfo-test.tsx
+++ b/test/unit-tests/components/views/right_panel/UserInfo-test.tsx
@@ -7,18 +7,10 @@ Please see LICENSE files in the repository root for full details.
 */
 
 import React from "react";
-import { fireEvent, render, screen, act, waitForElementToBeRemoved, waitFor } from "jest-matrix-react";
+import { render, screen, act, waitForElementToBeRemoved, waitFor } from "jest-matrix-react";
 import userEvent from "@testing-library/user-event";
 import { type Mocked, mocked } from "jest-mock";
-import {
-    type Room,
-    User,
-    type MatrixClient,
-    RoomMember,
-    MatrixEvent,
-    EventType,
-    Device,
-} from "matrix-js-sdk/src/matrix";
+import { type Room, User, type MatrixClient, RoomMember, Device } from "matrix-js-sdk/src/matrix";
 import { EventEmitter } from "events";
 import {
     UserVerificationStatus,
@@ -31,7 +23,6 @@ import {
 import UserInfo, {
     disambiguateDevices,
     getPowerLevels,
-    PowerLevelEditor,
     UserInfoHeader,
     UserOptionsSection,
 } from "../../../../../src/components/views/right_panel/UserInfo";
@@ -717,65 +708,6 @@ describe("<UserOptionsSection />", () => {
     );
 });
 
-describe("<PowerLevelEditor />", () => {
-    const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
-
-    let defaultProps: Parameters<typeof PowerLevelEditor>[0];
-    beforeEach(() => {
-        defaultProps = {
-            user: defaultMember,
-            room: mockRoom,
-            roomPermissions: {
-                modifyLevelMax: 100,
-                canEdit: false,
-                canInvite: false,
-            },
-        };
-    });
-
-    const renderComponent = (props = {}) => {
-        const Wrapper = (wrapperProps = {}) => {
-            return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
-        };
-
-        return render(<PowerLevelEditor {...defaultProps} {...props} />, {
-            wrapper: Wrapper,
-        });
-    };
-
-    it("renders a power level combobox", () => {
-        renderComponent();
-
-        expect(screen.getByRole("combobox", { name: "Power level" })).toBeInTheDocument();
-    });
-
-    it("renders a combobox and attempts to change power level on change of the combobox", async () => {
-        const startPowerLevel = 999;
-        const powerLevelEvent = new MatrixEvent({
-            type: EventType.RoomPowerLevels,
-            content: { users: { [defaultUserId]: startPowerLevel }, users_default: 1 },
-        });
-        mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent);
-        mockClient.getSafeUserId.mockReturnValueOnce(defaultUserId);
-        mockClient.getUserId.mockReturnValueOnce(defaultUserId);
-        mockClient.setPowerLevel.mockResolvedValueOnce({ event_id: "123" });
-        renderComponent();
-
-        const changedPowerLevel = 100;
-
-        fireEvent.change(screen.getByRole("combobox", { name: "Power level" }), {
-            target: { value: changedPowerLevel },
-        });
-
-        await screen.findByText("Demote", { exact: true });
-
-        // firing the event will raise a dialog warning about self demotion, wait for this to appear then click on it
-        await userEvent.click(await screen.findByText("Demote", { exact: true }));
-        expect(mockClient.setPowerLevel).toHaveBeenCalledTimes(1);
-        expect(mockClient.setPowerLevel).toHaveBeenCalledWith(mockRoom.roomId, defaultMember.userId, changedPowerLevel);
-    });
-});
-
 describe("disambiguateDevices", () => {
     it("does not add ambiguous key to unique names", () => {
         const initialDevices = [
diff --git a/test/unit-tests/components/views/right_panel/user_info/UserInfoPowerLevels-test.tsx b/test/unit-tests/components/views/right_panel/user_info/UserInfoPowerLevels-test.tsx
new file mode 100644
index 00000000000..2b871dd4693
--- /dev/null
+++ b/test/unit-tests/components/views/right_panel/user_info/UserInfoPowerLevels-test.tsx
@@ -0,0 +1,164 @@
+/*
+Copyright 2025 New Vector Ltd.
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
+Please see LICENSE files in the repository root for full details.
+*/
+
+import React from "react";
+import userEvent from "@testing-library/user-event";
+import { fireEvent, render, screen } from "jest-matrix-react";
+import { type Mocked, mocked } from "jest-mock";
+import { MatrixEvent, type MatrixClient, RoomMember, type Room, EventType } from "matrix-js-sdk/src/matrix";
+
+import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
+import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
+import { type IRoomPermissions } from "../../../../../../src/components/views/right_panel/UserInfo";
+import { PowerLevelSection } from "../../../../../../src/components/views/right_panel/user_info/UserInfoPowerLevels";
+
+describe("<PowerLevelEditor />", () => {
+    const defaultRoomId = "!fkfk";
+    const defaultUserId = "@user:example.com";
+    const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
+
+    let mockClient: Mocked<MatrixClient>;
+    let mockRoom: Mocked<Room>;
+    let defaultProps: {
+        user: RoomMember;
+        room: Room;
+        roomPermissions: IRoomPermissions;
+    };
+
+    beforeEach(() => {
+        defaultProps = {
+            user: defaultMember,
+            room: mockRoom,
+            roomPermissions: {
+                modifyLevelMax: 100,
+                canEdit: false,
+                canInvite: false,
+            },
+        };
+
+        mockRoom = mocked({
+            roomId: defaultRoomId,
+            getType: jest.fn().mockReturnValue(undefined),
+            isSpaceRoom: jest.fn().mockReturnValue(false),
+            getMember: jest.fn().mockReturnValue(undefined),
+            getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"),
+            name: "test room",
+            on: jest.fn(),
+            off: jest.fn(),
+            currentState: {
+                getStateEvents: jest.fn(),
+                on: jest.fn(),
+                off: jest.fn(),
+            },
+            getEventReadUpTo: jest.fn(),
+        } as unknown as Room);
+
+        mockClient = mocked({
+            getUser: jest.fn(),
+            isGuest: jest.fn().mockReturnValue(false),
+            isUserIgnored: jest.fn(),
+            getIgnoredUsers: jest.fn(),
+            setIgnoredUsers: jest.fn(),
+            getUserId: jest.fn(),
+            getSafeUserId: jest.fn(),
+            getDomain: jest.fn(),
+            on: jest.fn(),
+            off: jest.fn(),
+            isSynapseAdministrator: jest.fn().mockResolvedValue(false),
+            doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
+            doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false),
+            getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")),
+            mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
+            removeListener: jest.fn(),
+            currentState: {
+                on: jest.fn(),
+            },
+            getRoom: jest.fn(),
+            credentials: {},
+            setPowerLevel: jest.fn().mockResolvedValueOnce({ event_id: "123" }),
+        } as unknown as MatrixClient);
+
+        jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
+        jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
+    });
+
+    afterAll(() => {
+        defaultProps = {
+            user: defaultMember,
+            room: mockRoom,
+            roomPermissions: {
+                modifyLevelMax: 100,
+                canEdit: false,
+                canInvite: false,
+            },
+        };
+        jest.clearAllMocks();
+    });
+
+    const renderComponent = (props = defaultProps) => {
+        const Wrapper = (wrapperProps = {}) => {
+            return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
+        };
+
+        return render(<PowerLevelSection {...props} />, {
+            wrapper: Wrapper,
+        });
+    };
+
+    it("renders a power level combobox if can edit is true", () => {
+        const startPowerLevel = 999;
+        const powerLevelEvent = new MatrixEvent({
+            type: EventType.RoomPowerLevels,
+            content: { users: { [defaultUserId]: startPowerLevel }, users_default: 1 },
+        });
+        mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent);
+
+        renderComponent({
+            ...defaultProps,
+            room: mockRoom,
+            roomPermissions: { ...defaultProps.roomPermissions, canEdit: true },
+        });
+
+        expect(screen.getByRole("combobox", { name: "Power level" })).toBeInTheDocument();
+    });
+
+    it("renders a user role if can edit is false", () => {
+        const member = new RoomMember(defaultRoomId, defaultUserId);
+        member.powerLevel = 100;
+        renderComponent({ ...defaultProps, user: member });
+
+        expect(screen.getByText("Admin")).toBeInTheDocument();
+    });
+
+    it("renders a combobox and attempts to change power level on change of the combobox", async () => {
+        const startPowerLevel = 999;
+        const powerLevelEvent = new MatrixEvent({
+            type: EventType.RoomPowerLevels,
+            content: { users: { [defaultUserId]: startPowerLevel }, users_default: 1 },
+        });
+        mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent);
+        mockClient.getSafeUserId.mockReturnValueOnce(defaultUserId);
+        mockClient.getUserId.mockReturnValueOnce(defaultUserId);
+        renderComponent({
+            ...defaultProps,
+            room: mockRoom,
+            roomPermissions: { ...defaultProps.roomPermissions, canEdit: true },
+        });
+
+        const changedPowerLevel = 100;
+
+        fireEvent.change(screen.getByRole("combobox", { name: "Power level" }), {
+            target: { value: changedPowerLevel },
+        });
+
+        await screen.findByText("Demote", { exact: true });
+
+        // firing the event will raise a dialog warning about self demotion, wait for this to appear then click on it
+        await userEvent.click(await screen.findByText("Demote", { exact: true }));
+        expect(mockClient.setPowerLevel).toHaveBeenCalledTimes(1);
+        expect(mockClient.setPowerLevel).toHaveBeenCalledWith(mockRoom.roomId, defaultMember.userId, changedPowerLevel);
+    });
+});