Files
element-call-Github/playwright/utils/matrix-api.ts
T
Quentin GliechandClaude Fable 5.1 58b9e2d983 Test the standalone knock flow in Playwright
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>
2026-09-09 22:44:59 +02:00

147 lines
3.7 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.
*/
/** The homeserver of the Playwright backend, as `config.devenv.json` names it. */
export const HOMESERVER_URL = "https://synapse.m.localhost";
/** MSC3417: the room type Element Call gives its calls. */
const CALL_ROOM_TYPE = "org.matrix.msc3417.call";
/** The state event every call participant sends to announce their membership. */
const RTC_MEMBER_EVENT_TYPE = "org.matrix.msc3401.call.member";
async function csApiRequest(
accessToken: string,
method: "GET" | "POST",
path: string,
body?: unknown,
): Promise<Response> {
const init: RequestInit = {
method,
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
};
if (body !== undefined) init.body = JSON.stringify(body);
return await fetch(`${HOMESERVER_URL}/_matrix/client/v3/${path}`, init);
}
async function csApi<T>(
accessToken: string,
method: "GET" | "POST",
path: string,
body?: unknown,
): Promise<T> {
const response = await csApiRequest(accessToken, method, path, body);
if (!response.ok)
throw new Error(
`${method} ${path} failed: ${response.status} ${await response.text()}`,
);
return (await response.json()) as T;
}
/**
* Create a room that Element Call sees as a call, and that anyone who gets in
* may participate in. The creator is its only moderator, and `joinRule` is
* written as `m.room.join_rules` over whatever the preset would set.
*
* @returns The room's ID
*/
export async function createCallRoom({
accessToken,
name,
joinRule,
}: {
accessToken: string;
name: string;
joinRule: string;
}): Promise<string> {
const created = await csApi<{ room_id: string }>(
accessToken,
"POST",
"createRoom",
{
name,
preset: "private_chat",
creation_content: { type: CALL_ROOM_TYPE },
initial_state: [
{
type: "m.room.join_rules",
state_key: "",
content: { join_rule: joinRule },
},
],
power_level_content_override: {
events: { [RTC_MEMBER_EVENT_TYPE]: 0 },
},
},
);
return created.room_id;
}
export async function inviteUser(
accessToken: string,
roomId: string,
userId: string,
): Promise<void> {
await csApi(
accessToken,
"POST",
`rooms/${encodeURIComponent(roomId)}/invite`,
{
user_id: userId,
},
);
}
export async function kickUser(
accessToken: string,
roomId: string,
userId: string,
): Promise<void> {
await csApi(accessToken, "POST", `rooms/${encodeURIComponent(roomId)}/kick`, {
user_id: userId,
});
}
export async function banUser(
accessToken: string,
roomId: string,
userId: string,
reason?: string,
): Promise<void> {
await csApi(accessToken, "POST", `rooms/${encodeURIComponent(roomId)}/ban`, {
user_id: userId,
reason,
});
}
/**
* Read a user's membership as the server reports it, from the room state, so
* that knocks and leaves are visible and not just joins.
*
* @returns The membership, or undefined while the user has no member event
*/
export async function membershipOf(
accessToken: string,
roomId: string,
userId: string,
): Promise<string | undefined> {
const response = await csApiRequest(
accessToken,
"GET",
`rooms/${encodeURIComponent(roomId)}/state/m.room.member/${encodeURIComponent(userId)}`,
);
if (response.status === 404) return undefined;
if (!response.ok)
throw new Error(
`Reading the membership of ${userId} failed: ${response.status} ${await response.text()}`,
);
return ((await response.json()) as { membership: string }).membership;
}