mirror of
https://github.com/vector-im/element-web.git
synced 2025-11-09 20:51:41 +01:00
* InviteDialog: show some words and a spinner while invites are being sent * MultiInviter-test: avoid building unhandled rejected promises If we don't handle rejected promises, jest gets confused by them. Instead, let's create them on-demand. * Open a "progress" dialog while invites are being sent * Inhibit invite progress dialog when RoomUpgradeWarning dialog is kept open ... otherwise the `RoomUpgradeWarning` dialog disappears during the invites, and the tests that assert that it is showing the correct thing fail. enter the commit message for your changes. Lines starting * Switch to compound CSS variables instead of old pcss vars * update playwright screenshots * Revert "update playwright screenshots" This reverts commit b0a15d97f35a088fe5b67009085eab46be1316fd. * Another go at updating screenshots * Address review comments * remove redundant Props
270 lines
11 KiB
TypeScript
270 lines
11 KiB
TypeScript
/*
|
|
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 { EventType, type MatrixClient, MatrixError, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
|
import { KnownMembership } from "matrix-js-sdk/src/types";
|
|
|
|
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
|
import Modal, { type ComponentType, type ComponentProps } from "../../../src/Modal";
|
|
import SettingsStore from "../../../src/settings/SettingsStore";
|
|
import MultiInviter, { type CompletionStates } from "../../../src/utils/MultiInviter";
|
|
import * as TestUtilsMatrix from "../../test-utils";
|
|
import AskInviteAnywayDialog from "../../../src/components/views/dialogs/AskInviteAnywayDialog";
|
|
import ConfirmUserActionDialog from "../../../src/components/views/dialogs/ConfirmUserActionDialog";
|
|
|
|
const ROOMID = "!room:server";
|
|
|
|
const MXID1 = "@user1:server";
|
|
const MXID2 = "@user2:server";
|
|
const MXID3 = "@user3:server";
|
|
|
|
const MXID_PROFILE_STATES: Record<string, () => {}> = {
|
|
[MXID1]: () => ({}),
|
|
[MXID2]: () => {
|
|
throw new MatrixError({ errcode: "M_FORBIDDEN" });
|
|
},
|
|
[MXID3]: () => {
|
|
throw new MatrixError({ errcode: "M_NOT_FOUND" });
|
|
},
|
|
};
|
|
|
|
jest.mock("../../../src/Modal", () => ({
|
|
createDialog: jest.fn(),
|
|
}));
|
|
|
|
jest.mock("../../../src/settings/SettingsStore", () => ({
|
|
getValue: jest.fn(),
|
|
monitorSetting: jest.fn(),
|
|
watchSetting: jest.fn(),
|
|
}));
|
|
|
|
const mockPromptBeforeInviteUnknownUsers = (value: boolean) => {
|
|
mocked(SettingsStore.getValue).mockImplementation(
|
|
(settingName: string, roomId: string, _excludeDefault = false): any => {
|
|
if (settingName === "promptBeforeInviteUnknownUsers" && roomId === ROOMID) {
|
|
return value;
|
|
}
|
|
},
|
|
);
|
|
};
|
|
|
|
const mockCreateTrackedDialog = (callbackName: "onInviteAnyways" | "onGiveUp") => {
|
|
mocked(Modal.createDialog).mockImplementation((Element: ComponentType, props?: ComponentProps<ComponentType>) => {
|
|
if (Element === AskInviteAnywayDialog) {
|
|
(props as ComponentProps<typeof AskInviteAnywayDialog>)[callbackName]();
|
|
}
|
|
return { close: jest.fn(), finished: new Promise(() => {}) };
|
|
});
|
|
};
|
|
|
|
const expectAllInvitedResult = (result: CompletionStates) => {
|
|
expect(result).toEqual({
|
|
[MXID1]: "invited",
|
|
[MXID2]: "invited",
|
|
[MXID3]: "invited",
|
|
});
|
|
};
|
|
|
|
describe("MultiInviter", () => {
|
|
let client: jest.Mocked<MatrixClient>;
|
|
let inviter: MultiInviter;
|
|
|
|
beforeEach(() => {
|
|
jest.resetAllMocks();
|
|
mocked(Modal.createDialog).mockReturnValue({ close: jest.fn(), finished: new Promise(() => {}) });
|
|
|
|
TestUtilsMatrix.stubClient();
|
|
client = MatrixClientPeg.safeGet() as jest.Mocked<MatrixClient>;
|
|
|
|
client.invite = jest.fn();
|
|
client.invite.mockResolvedValue({});
|
|
|
|
client.getProfileInfo = jest.fn();
|
|
client.getProfileInfo.mockImplementation(async (userId: string) => {
|
|
const m = MXID_PROFILE_STATES[userId];
|
|
if (m) return m();
|
|
throw new Error();
|
|
});
|
|
client.unban = jest.fn();
|
|
|
|
inviter = new MultiInviter(client, ROOMID);
|
|
});
|
|
|
|
describe("invite", () => {
|
|
it("should show a progress dialog while the invite happens", async () => {
|
|
const mockModalHandle = { close: jest.fn(), finished: new Promise<[]>(() => {}) };
|
|
mocked(Modal.createDialog).mockReturnValue(mockModalHandle);
|
|
|
|
const invitePromise = Promise.withResolvers<{}>();
|
|
client.invite.mockReturnValue(invitePromise.promise);
|
|
|
|
const resultPromise = inviter.invite([MXID1]);
|
|
expect(Modal.createDialog).toHaveBeenCalledTimes(1);
|
|
expect(mockModalHandle.close).not.toHaveBeenCalled();
|
|
|
|
invitePromise.resolve({});
|
|
await resultPromise;
|
|
expect(mockModalHandle.close).toHaveBeenCalled();
|
|
});
|
|
|
|
describe("with promptBeforeInviteUnknownUsers = false", () => {
|
|
beforeEach(() => mockPromptBeforeInviteUnknownUsers(false));
|
|
|
|
it("should invite all users", async () => {
|
|
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
|
|
|
expect(client.invite).toHaveBeenCalledTimes(3);
|
|
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, {});
|
|
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, {});
|
|
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, {});
|
|
|
|
expectAllInvitedResult(result);
|
|
});
|
|
});
|
|
|
|
describe("with promptBeforeInviteUnknownUsers = true and", () => {
|
|
beforeEach(() => mockPromptBeforeInviteUnknownUsers(true));
|
|
|
|
describe("confirming the unknown user dialog", () => {
|
|
beforeEach(() => mockCreateTrackedDialog("onInviteAnyways"));
|
|
|
|
it("should invite all users", async () => {
|
|
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
|
|
|
expect(client.invite).toHaveBeenCalledTimes(3);
|
|
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, {});
|
|
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, {});
|
|
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, {});
|
|
|
|
expectAllInvitedResult(result);
|
|
});
|
|
});
|
|
|
|
describe("declining the unknown user dialog", () => {
|
|
beforeEach(() => mockCreateTrackedDialog("onGiveUp"));
|
|
|
|
it("should only invite existing users", async () => {
|
|
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
|
|
|
expect(client.invite).toHaveBeenCalledTimes(1);
|
|
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, {});
|
|
|
|
// The resolved state is 'invited' for all users.
|
|
// With the above client expectations, the test ensures that only the first user is invited.
|
|
expectAllInvitedResult(result);
|
|
});
|
|
});
|
|
});
|
|
|
|
it("should show sensible error when attempting 3pid invite with no identity server", async () => {
|
|
client.inviteByEmail = jest.fn().mockRejectedValueOnce(
|
|
new MatrixError({
|
|
errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM",
|
|
}),
|
|
);
|
|
await inviter.invite(["foo@bar.com"]);
|
|
expect(inviter.getErrorText("foo@bar.com")).toMatchInlineSnapshot(
|
|
`"Cannot invite user by email without an identity server. You can connect to one under "Settings"."`,
|
|
);
|
|
});
|
|
|
|
it("should ask if user wants to unban user if they have permission", async () => {
|
|
mocked(Modal.createDialog).mockImplementation(
|
|
(Element: ComponentType, props?: ComponentProps<ComponentType>): any => {
|
|
// We stub out the modal with an immediate affirmative (proceed) return
|
|
return { finished: Promise.resolve([true]) };
|
|
},
|
|
);
|
|
|
|
const room = new Room(ROOMID, client, client.getSafeUserId());
|
|
mocked(client.getRoom).mockReturnValue(room);
|
|
const ourMember = new RoomMember(ROOMID, client.getSafeUserId());
|
|
ourMember.membership = KnownMembership.Join;
|
|
ourMember.powerLevel = 100;
|
|
const member = new RoomMember(ROOMID, MXID1);
|
|
member.membership = KnownMembership.Ban;
|
|
member.powerLevel = 0;
|
|
room.getMember = (userId: string) => {
|
|
if (userId === client.getSafeUserId()) return ourMember;
|
|
if (userId === MXID1) return member;
|
|
return null;
|
|
};
|
|
|
|
await inviter.invite([MXID1]);
|
|
expect(Modal.createDialog).toHaveBeenCalledWith(ConfirmUserActionDialog, {
|
|
member,
|
|
title: "User cannot be invited until they are unbanned",
|
|
action: "Unban",
|
|
});
|
|
expect(client.unban).toHaveBeenCalledWith(ROOMID, MXID1);
|
|
});
|
|
|
|
it("should show sensible error when attempting to invite over federation with m.federate=false", async () => {
|
|
mocked(client.invite).mockRejectedValueOnce(
|
|
new MatrixError({
|
|
errcode: "M_FORBIDDEN",
|
|
}),
|
|
);
|
|
const room = new Room(ROOMID, client, client.getSafeUserId());
|
|
room.currentState.setStateEvents([
|
|
new MatrixEvent({
|
|
type: EventType.RoomCreate,
|
|
state_key: "",
|
|
content: {
|
|
"m.federate": false,
|
|
},
|
|
room_id: ROOMID,
|
|
}),
|
|
]);
|
|
mocked(client.getRoom).mockReturnValue(room);
|
|
|
|
await inviter.invite(["@user:other_server"]);
|
|
expect(inviter.getErrorText("@user:other_server")).toMatchInlineSnapshot(
|
|
`"This room is unfederated. You cannot invite people from external servers."`,
|
|
);
|
|
});
|
|
|
|
it("should show sensible error when attempting to invite over federation with m.federate=false to space", async () => {
|
|
mocked(client.invite).mockRejectedValueOnce(
|
|
new MatrixError({
|
|
errcode: "M_FORBIDDEN",
|
|
}),
|
|
);
|
|
const room = new Room(ROOMID, client, client.getSafeUserId());
|
|
room.currentState.setStateEvents([
|
|
new MatrixEvent({
|
|
type: EventType.RoomCreate,
|
|
state_key: "",
|
|
content: {
|
|
"m.federate": false,
|
|
"type": "m.space",
|
|
},
|
|
room_id: ROOMID,
|
|
}),
|
|
]);
|
|
mocked(client.getRoom).mockReturnValue(room);
|
|
|
|
await inviter.invite(["@user:other_server"]);
|
|
expect(inviter.getErrorText("@user:other_server")).toMatchInlineSnapshot(
|
|
`"This space is unfederated. You cannot invite people from external servers."`,
|
|
);
|
|
});
|
|
|
|
it("should set shareEncryptedHistory if that setting is enabled", async () => {
|
|
mocked(SettingsStore.getValue).mockImplementation((settingName, roomId, value) => {
|
|
return settingName === "feature_share_history_on_invite"; // this is enabled, everything else is disabled.
|
|
});
|
|
await inviter.invite([MXID1]);
|
|
|
|
expect(client.invite).toHaveBeenCalledTimes(1);
|
|
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, { shareEncryptedHistory: true });
|
|
});
|
|
});
|
|
});
|