mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-22 22:29:30 +00:00
A knock call room, created over the CS API, walks through the lobby's join states: request to join, the waiting copy with its withdrawal link, a second request, and an invite from the room's moderator that lands the user in the call. A decline, a ban with its reason, and a room whose join rule the app cannot act on are covered too. An invite-only room asserts what happens today: Synapse serves no summary for it, so the refused join is a full-screen error rather than a lobby state. The `spaUser` fixture registers over the admin API and logs in through the form, and serves `config.devenv.json` itself, since Playwright reuses a dev server started with another config. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
178 lines
5.6 KiB
TypeScript
178 lines
5.6 KiB
TypeScript
/*
|
|
Copyright 2026 Element Creations Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import { expect } from "@playwright/test";
|
|
|
|
import { spaTest as test, type SpaUser } from "./fixtures/spa-user.ts";
|
|
import {
|
|
banUser,
|
|
createCallRoom,
|
|
inviteUser,
|
|
kickUser,
|
|
membershipOf,
|
|
} from "./utils/matrix-api.ts";
|
|
|
|
/** The app URL for a call known only by its room ID. */
|
|
const callUrl = (roomId: string): string =>
|
|
`/room/#?roomId=${encodeURIComponent(roomId)}`;
|
|
|
|
/** Reads the membership the server holds for `user`, as a moderator sees it. */
|
|
const membershipPoll =
|
|
(moderator: SpaUser, roomId: string, user: SpaUser) =>
|
|
async (): Promise<string | undefined> =>
|
|
await membershipOf(moderator.accessToken, roomId, user.mxId);
|
|
|
|
test("Ask to join a call, withdraw, ask again and be let in", async ({
|
|
page,
|
|
spaUser,
|
|
registerUser,
|
|
}) => {
|
|
test.slow();
|
|
const moderator = await registerUser("moderator");
|
|
const roomId = await createCallRoom({
|
|
accessToken: moderator.accessToken,
|
|
name: "Knock call",
|
|
joinRule: "knock",
|
|
});
|
|
const membership = membershipPoll(moderator, roomId, spaUser);
|
|
|
|
await page.goto(callUrl(roomId));
|
|
|
|
const joinButton = page.getByTestId("lobby_joinCall");
|
|
await expect(joinButton).toHaveText("Request to join call");
|
|
await expect(joinButton).toBeEnabled();
|
|
await expect(page.locator("video")).toBeVisible();
|
|
|
|
await joinButton.click();
|
|
|
|
await expect(joinButton).toHaveText("Request to join sent");
|
|
await expect(joinButton).toBeDisabled();
|
|
await expect(page.getByTestId("lobby_joinMessage")).toContainText(
|
|
"You will receive an invite to join the call if your request is accepted.",
|
|
);
|
|
await expect.poll(membership).toBe("knock");
|
|
|
|
await page.getByTestId("lobby_cancelRequest").click();
|
|
|
|
await expect(joinButton).toHaveText("Request to join call");
|
|
await expect(joinButton).toBeEnabled();
|
|
await expect(page.getByTestId("lobby_joinMessage")).toBeHidden();
|
|
await expect.poll(membership).toBe("leave");
|
|
|
|
await joinButton.click();
|
|
|
|
await expect(joinButton).toHaveText("Request to join sent");
|
|
await expect.poll(membership).toBe("knock");
|
|
|
|
await inviteUser(moderator.accessToken, roomId, spaUser.mxId);
|
|
|
|
// The accepted request joins the room and skips a second lobby.
|
|
await expect(page.getByTestId("incall_leave")).toBeVisible({
|
|
timeout: 60_000,
|
|
});
|
|
await expect(joinButton).toBeHidden();
|
|
});
|
|
|
|
test("A declined request is shown in the lobby", async ({
|
|
page,
|
|
spaUser,
|
|
registerUser,
|
|
}) => {
|
|
const moderator = await registerUser("moderator");
|
|
const roomId = await createCallRoom({
|
|
accessToken: moderator.accessToken,
|
|
name: "Declining call",
|
|
joinRule: "knock",
|
|
});
|
|
|
|
await page.goto(callUrl(roomId));
|
|
await page.getByTestId("lobby_joinCall").click();
|
|
await expect.poll(membershipPoll(moderator, roomId, spaUser)).toBe("knock");
|
|
|
|
await kickUser(moderator.accessToken, roomId, spaUser.mxId);
|
|
|
|
const message = page.getByTestId("lobby_joinMessage");
|
|
await expect(message).toContainText("Access denied");
|
|
await expect(message).toContainText("Your request to join was declined.");
|
|
await expect(page.getByTestId("lobby_joinCall")).toBeHidden();
|
|
// The camera preview outlives the refusal.
|
|
await expect(page.locator("video")).toBeVisible();
|
|
});
|
|
|
|
test("A ban is shown in the lobby, with its reason", async ({
|
|
page,
|
|
spaUser,
|
|
registerUser,
|
|
}) => {
|
|
const moderator = await registerUser("moderator");
|
|
const roomId = await createCallRoom({
|
|
accessToken: moderator.accessToken,
|
|
name: "Banning call",
|
|
joinRule: "knock",
|
|
});
|
|
|
|
await page.goto(callUrl(roomId));
|
|
await page.getByTestId("lobby_joinCall").click();
|
|
await expect.poll(membershipPoll(moderator, roomId, spaUser)).toBe("knock");
|
|
|
|
await banUser(moderator.accessToken, roomId, spaUser.mxId, "Wrong call");
|
|
|
|
const message = page.getByTestId("lobby_joinMessage");
|
|
await expect(message).toContainText("Banned");
|
|
await expect(message).toContainText("You have been banned from the room.");
|
|
await expect(message).toContainText("Reason: Wrong call");
|
|
await expect(page.getByTestId("lobby_joinCall")).toBeHidden();
|
|
});
|
|
|
|
test("A call that takes no requests says so in the lobby", async ({
|
|
page,
|
|
spaUser,
|
|
registerUser,
|
|
}) => {
|
|
const moderator = await registerUser("moderator");
|
|
// Synapse serves an MSC3266 summary for knock and knock_restricted rooms
|
|
// only, so a `knock_restricted` room, whose rule Element Call cannot act on
|
|
// without knowing the user's other rooms, is the one shape that reaches the
|
|
// lobby with nothing to offer.
|
|
const roomId = await createCallRoom({
|
|
accessToken: moderator.accessToken,
|
|
name: "Space call",
|
|
joinRule: "knock_restricted",
|
|
});
|
|
|
|
await page.goto(callUrl(roomId));
|
|
|
|
await expect(page.getByTestId("lobby_joinMessage")).toContainText(
|
|
"You need an invite to join this call.",
|
|
);
|
|
await expect(page.getByTestId("lobby_joinCall")).toBeHidden();
|
|
await expect(page.locator("video")).toBeVisible();
|
|
});
|
|
|
|
test("An invite-only call cannot be reached at all", async ({
|
|
page,
|
|
spaUser,
|
|
registerUser,
|
|
}) => {
|
|
const moderator = await registerUser("moderator");
|
|
const roomId = await createCallRoom({
|
|
accessToken: moderator.accessToken,
|
|
name: "Private call",
|
|
joinRule: "invite",
|
|
});
|
|
|
|
await page.goto(callUrl(roomId));
|
|
|
|
// Synapse serves no room summary for an invite-only room, so Element Call
|
|
// falls back to joining as if the room were public and the refusal lands on
|
|
// the error page rather than in the lobby.
|
|
await expect(
|
|
page.getByRole("heading", { name: "Something went wrong" }),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("lobby_joinCall")).toBeHidden();
|
|
});
|