element-web-Github/playwright/pages/client.ts

528 lines
18 KiB
TypeScript
Raw Normal View History

/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { JSHandle, Page } from "@playwright/test";
import { PageFunctionOn } from "playwright-core/types/structs";
import { Network } from "./network";
import type {
IContent,
ICreateRoomOpts,
ISendEventResponse,
MatrixClient,
Room,
MatrixEvent,
ReceiptType,
IRoomDirectoryOptions,
KnockRoomOpts,
Visibility,
UploadOpts,
Upload,
StateEvents,
2024-03-25 20:48:48 +08:00
TimelineEvents,
} from "matrix-js-sdk/src/matrix";
2024-03-25 20:48:48 +08:00
import type { RoomMessageEventContent } from "matrix-js-sdk/src/types";
import { Credentials } from "../plugins/homeserver";
export class Client {
public network: Network;
protected client: JSHandle<MatrixClient>;
protected getClientHandle(): Promise<JSHandle<MatrixClient>> {
return this.page.evaluateHandle(() => window.mxMatrixClientPeg.get());
}
public async prepareClient(): Promise<JSHandle<MatrixClient>> {
if (!this.client) {
this.client = await this.getClientHandle();
}
return this.client;
}
public constructor(protected readonly page: Page) {
page.on("framenavigated", async () => {
this.client = null;
});
this.network = new Network(page, this);
}
public evaluate<R, Arg, O extends MatrixClient = MatrixClient>(
pageFunction: PageFunctionOn<O, Arg, R>,
arg: Arg,
): Promise<R>;
public evaluate<R, O extends MatrixClient = MatrixClient>(
pageFunction: PageFunctionOn<O, void, R>,
arg?: any,
): Promise<R>;
public async evaluate<T>(fn: (client: MatrixClient) => T, arg?: any): Promise<T> {
await this.prepareClient();
return this.client.evaluate(fn, arg);
}
public evaluateHandle<R, Arg, O extends MatrixClient = MatrixClient>(
pageFunction: PageFunctionOn<O, Arg, R>,
arg: Arg,
): Promise<JSHandle<R>>;
public evaluateHandle<R, O extends MatrixClient = MatrixClient>(
pageFunction: PageFunctionOn<O, void, R>,
arg?: any,
): Promise<JSHandle<R>>;
public async evaluateHandle<T>(fn: (client: MatrixClient) => T, arg?: any): Promise<JSHandle<T>> {
await this.prepareClient();
return this.client.evaluateHandle(fn, arg);
}
/**
* @param roomId ID of the room to send the event into
* @param threadId ID of the thread to send into or null for main timeline
* @param eventType type of event to send
* @param content the event content to send
*/
public async sendEvent(
roomId: string,
threadId: string | null,
eventType: string,
content: IContent,
): Promise<ISendEventResponse> {
const client = await this.prepareClient();
return client.evaluate(
async (client, { roomId, threadId, eventType, content }) => {
2024-03-25 20:48:48 +08:00
return client.sendEvent(
roomId,
threadId,
eventType as keyof TimelineEvents,
content as TimelineEvents[keyof TimelineEvents],
);
},
{ roomId, threadId, eventType, content },
);
}
/**
* Send a message into a room
* @param roomId ID of the room to send the message into
* @param content the event content to send
* @param threadId optional thread id
*/
public async sendMessage(
roomId: string,
content: IContent | string,
threadId: string | null = null,
): Promise<ISendEventResponse> {
if (typeof content === "string") {
content = {
msgtype: "m.text",
body: content,
};
}
const client = await this.prepareClient();
return client.evaluate(
(client, { roomId, content, threadId }) => {
2024-03-25 20:48:48 +08:00
return client.sendMessage(roomId, threadId, content as RoomMessageEventContent);
},
{
roomId,
content,
threadId,
},
);
}
public async redactEvent(roomId: string, eventId: string, reason?: string): Promise<ISendEventResponse> {
return this.evaluate(
async (client, { roomId, eventId, reason }) => {
return client.redactEvent(roomId, eventId, reason);
},
{ roomId, eventId, reason },
);
}
Add reactions to html export (#28210) * Absorb the matrix-react-sdk repository (#28192) Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> Co-authored-by: github-merge-queue <github-merge-queue@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Florian Duros <florian.duros@ormaz.fr> Co-authored-by: Kim Brose <kim.brose@nordeck.net> Co-authored-by: Florian Duros <florianduros@element.io> Co-authored-by: R Midhun Suresh <hi@midhun.dev> Co-authored-by: dbkr <986903+dbkr@users.noreply.github.com> Co-authored-by: ElementRobot <releases@riot.im> Co-authored-by: dbkr <dbkr@users.noreply.github.com> Co-authored-by: David Baker <dbkr@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: David Langley <davidl@element.io> Co-authored-by: Michael Weimann <michaelw@matrix.org> Co-authored-by: Timshel <Timshel@users.noreply.github.com> Co-authored-by: Sahil Silare <32628578+sahil9001@users.noreply.github.com> Co-authored-by: Will Hunt <will@half-shot.uk> Co-authored-by: Hubert Chathi <hubert@uhoreg.ca> Co-authored-by: Andrew Ferrazzutti <andrewf@element.io> Co-authored-by: Robin <robin@robin.town> Co-authored-by: Tulir Asokan <tulir@maunium.net> * Update dependency @sentry/browser to v8.33.0 [SECURITY] (#28194) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update babel monorepo (#28196) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency @types/react to v17.0.83 (#28138) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency @matrix-org/spec to v1.12.0 (#28200) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency @formatjs/intl-segmenter to v11.5.9 (#28197) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Remove references to `MatrixClient.crypto` (#28204) * Remove `VerificationExplorer` * Remove `remakeolm` slash command * Remove call to `crypto.cancelAndResendAllOutgoingKeyRequests` * Remove crypto mock in `LoginWithQR-test.tsx` * Remove `StopGadWidgetDriver.sendToDevice` * Remove remaining mock * Update dependency typescript to v5.6.3 (#28198) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency eslint-plugin-unicorn to v56 (#28202) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency stylelint to v16.10.0 (#28201) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update browserslist (#28199) * Update browserslist * Update tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> * Add reactions to html export and add test * Add reaction to snapshot test * Update snapshot output * Remove logging * Add reaction to html export screenshot test. * lint * Update reference screenshot. --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> Co-authored-by: github-merge-queue <github-merge-queue@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Florian Duros <florian.duros@ormaz.fr> Co-authored-by: Kim Brose <kim.brose@nordeck.net> Co-authored-by: Florian Duros <florianduros@element.io> Co-authored-by: R Midhun Suresh <hi@midhun.dev> Co-authored-by: dbkr <986903+dbkr@users.noreply.github.com> Co-authored-by: ElementRobot <releases@riot.im> Co-authored-by: dbkr <dbkr@users.noreply.github.com> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: Michael Weimann <michaelw@matrix.org> Co-authored-by: Timshel <Timshel@users.noreply.github.com> Co-authored-by: Sahil Silare <32628578+sahil9001@users.noreply.github.com> Co-authored-by: Will Hunt <will@half-shot.uk> Co-authored-by: Hubert Chathi <hubert@uhoreg.ca> Co-authored-by: Andrew Ferrazzutti <andrewf@element.io> Co-authored-by: Robin <robin@robin.town> Co-authored-by: Tulir Asokan <tulir@maunium.net>
2024-10-18 20:56:08 +08:00
/**
* Send a reaction to to a message
* @param roomId ID of the room to send the reaction into
* @param threadId ID of the thread to send into or null for main timeline
* @param eventId Event ID of the message you are reacting to
* @param reaction The reaction text to send
* @returns
*/
public async reactToMessage(
roomId: string,
threadId: string | null,
eventId: string,
reaction: string,
): Promise<ISendEventResponse> {
return this.sendEvent(roomId, threadId ?? null, "m.reaction", {
"m.relates_to": {
rel_type: "m.annotation",
event_id: eventId,
key: reaction,
},
});
}
/**
* Create a room with given options.
* @param options the options to apply when creating the room
* @return the ID of the newly created room
*/
public async createRoom(options: ICreateRoomOpts): Promise<string> {
const client = await this.prepareClient();
return await client.evaluate(async (cli, options) => {
const resp = await cli.createRoom(options);
const roomId = resp.room_id;
if (!cli.getRoom(roomId)) {
await new Promise<void>((resolve) => {
const onRoom = (room: Room) => {
if (room.roomId === roomId) {
cli.off(window.matrixcs.ClientEvent.Room, onRoom);
resolve();
}
};
cli.on(window.matrixcs.ClientEvent.Room, onRoom);
});
}
return roomId;
}, options);
}
Migrate right-panel/* from Cypress to Playwright (#11954) * Migrate file-panel.spec.ts from Cypress to Playwright Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Migrate right-panel.spec.ts from Cypress to Playwright Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Migrate notification-panel.spec.ts from Cypress to Playwright Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update screenshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update screenshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix test flakes Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update screenshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update screenshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Try stabilise test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * sleep Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Handle both cases Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix assertion Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Flip Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-12-01 20:24:49 +08:00
/**
* Create a space with given options.
* @param options the options to apply when creating the space
* @return the ID of the newly created space (room)
*/
public async createSpace(options: ICreateRoomOpts): Promise<string> {
return this.createRoom({
...options,
creation_content: {
...options.creation_content,
type: "m.space",
},
});
}
/**
* Joins the given room by alias or ID
* @param roomIdOrAlias the id or alias of the room to join
*/
public async joinRoom(roomIdOrAlias: string): Promise<void> {
const client = await this.prepareClient();
await client.evaluate(async (client, roomIdOrAlias) => {
return await client.joinRoom(roomIdOrAlias);
}, roomIdOrAlias);
}
/**
* Make this bot join a room by name
* @param roomName Name of the room to join
*/
public async joinRoomByName(roomName: string): Promise<string> {
const client = await this.prepareClient();
return client.evaluate(
async (client, { roomName }) => {
const room = client.getRooms().find((r) => r.getDefaultRoomName(client.getUserId()) === roomName);
if (room) {
await client.joinRoom(room.roomId);
return room.roomId;
}
throw new Error(`Bot room join failed. Cannot find room '${roomName}'`);
},
{
roomName,
},
);
}
/**
* Wait until next sync from this client
*/
public async waitForNextSync(): Promise<void> {
await this.page.waitForResponse(async (response) => {
const accessToken = await this.evaluate((client) => client.getAccessToken());
const authHeader = await response.request().headerValue("authorization");
return response.url().includes("/sync") && authHeader.includes(accessToken);
});
}
/**
* Invites the given user to the given room.
* @param roomId the id of the room to invite to
* @param userId the id of the user to invite
*/
public async inviteUser(roomId: string, userId: string): Promise<void> {
const client = await this.prepareClient();
await client.evaluate((client, { roomId, userId }) => client.invite(roomId, userId), {
roomId,
userId,
});
}
/**
* Knocks the given room.
* @param roomId the id of the room to knock
* @param opts the options to use when knocking
*/
public async knockRoom(roomId: string, opts?: KnockRoomOpts): Promise<void> {
const client = await this.prepareClient();
await client.evaluate((client, { roomId, opts }) => client.knockRoom(roomId, opts), { roomId, opts });
}
/**
* Kicks the given user from the given room.
* @param roomId the id of the room to kick from
* @param userId the id of the user to kick
* @param reason the reason for the kick
*/
public async kick(roomId: string, userId: string, reason?: string): Promise<void> {
const client = await this.prepareClient();
await client.evaluate((client, { roomId, userId, reason }) => client.kick(roomId, userId, reason), {
roomId,
userId,
reason,
});
}
/**
* Bans the given user from the given room.
* @param roomId the id of the room to ban from
* @param userId the id of the user to ban
* @param reason the reason for the ban
*/
public async ban(roomId: string, userId: string, reason?: string): Promise<void> {
const client = await this.prepareClient();
await client.evaluate((client, { roomId, userId, reason }) => client.ban(roomId, userId, reason), {
roomId,
userId,
reason,
});
}
/**
* Unban the given user from the given room.
* @param roomId the id of the room to unban from
* @param userId the id of the user to unban
*/
public async unban(roomId: string, userId: string): Promise<void> {
const client = await this.prepareClient();
await client.evaluate((client, { roomId, userId }) => client.unban(roomId, userId), { roomId, userId });
}
/**
* Wait for the client to have specific membership of a given room
*
* This is often useful after joining a room, when we need to wait for the sync loop to catch up.
*
* Times out with an error after 1 second.
*
* @param roomId - ID of the room to check
* @param membership - required membership.
*/
public async awaitRoomMembership(roomId: string, membership: string = "join") {
await this.evaluate(
(cli: MatrixClient, { roomId, membership }) => {
const isReady = () => {
// Fetch the room on each check, because we get a different instance before and after the join arrives.
const room = cli.getRoom(roomId);
const myMembership = room?.getMyMembership();
// @ts-ignore access to private field "logger"
cli.logger.info(`waiting for room ${roomId}: membership now ${myMembership}`);
return myMembership === membership;
};
if (isReady()) return;
const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 1000)).then(() => {
const room = cli.getRoom(roomId);
const myMembership = room?.getMyMembership();
throw new Error(
`Timeout waiting for room ${roomId} membership (now '${myMembership}', wanted '${membership}')`,
);
});
const readyPromise = new Promise<void>((resolve) => {
async function onEvent() {
if (isReady()) {
cli.removeListener(window.matrixcs.ClientEvent.Event, onEvent);
resolve();
}
}
cli.on(window.matrixcs.ClientEvent.Event, onEvent);
});
return Promise.race([timeoutPromise, readyPromise]);
},
{ roomId, membership },
);
}
/**
* @param {MatrixEvent} event
* @param {ReceiptType} receiptType
* @param {boolean} unthreaded
*/
public async sendReadReceipt(
event: JSHandle<MatrixEvent>,
receiptType?: ReceiptType,
unthreaded?: boolean,
): Promise<{}> {
const client = await this.prepareClient();
return client.evaluate(
(client, { event, receiptType, unthreaded }) => {
return client.sendReadReceipt(event, receiptType, unthreaded);
},
{ event, receiptType, unthreaded },
);
}
public async publicRooms(options?: IRoomDirectoryOptions): ReturnType<MatrixClient["publicRooms"]> {
const client = await this.prepareClient();
return client.evaluate((client, options) => {
return client.publicRooms(options);
}, options);
}
/**
* @param {string} name
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: {} an empty object.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public async setDisplayName(name: string): Promise<{}> {
const client = await this.prepareClient();
return client.evaluate(async (cli: MatrixClient, name) => cli.setDisplayName(name), name);
}
/**
* @param {string} url
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: {} an empty object.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public async setAvatarUrl(url: string): Promise<{}> {
const client = await this.prepareClient();
return client.evaluate(async (cli: MatrixClient, url) => cli.setAvatarUrl(url), url);
}
/**
* Upload a file to the media repository on the homeserver.
*
* @param {object} file The object to upload. On a browser, something that
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
* a Buffer, String or ReadStream.
*/
public async uploadContent(file: Buffer, opts?: UploadOpts): Promise<Awaited<Upload["promise"]>> {
const client = await this.prepareClient();
return client.evaluate(
async (cli: MatrixClient, { file, opts }) => cli.uploadContent(new Uint8Array(file), opts),
{
file: [...file],
opts,
},
);
}
/**
* Bootstraps cross-signing.
*/
public async bootstrapCrossSigning(credentials: Credentials): Promise<void> {
const client = await this.prepareClient();
return bootstrapCrossSigningForClient(client, credentials);
}
/**
* Sets account data for the user.
* @param type The type of account data to set
* @param content The content to set
*/
public async setAccountData(type: string, content: IContent): Promise<void> {
const client = await this.prepareClient();
return client.evaluate(
async (client, { type, content }) => {
await client.setAccountData(type, content);
},
{ type, content },
);
}
/**
* Sends a state event into the room.
* @param roomId ID of the room to send the event into
* @param eventType type of event to send
* @param content the event content to send
* @param stateKey the state key to use
*/
public async sendStateEvent(
roomId: string,
eventType: string,
content: IContent,
stateKey?: string,
): Promise<ISendEventResponse> {
const client = await this.prepareClient();
return client.evaluate(
async (client, { roomId, eventType, content, stateKey }) => {
return client.sendStateEvent(roomId, eventType as keyof StateEvents, content, stateKey);
},
{ roomId, eventType, content, stateKey },
);
}
/**
* Leaves the given room.
* @param roomId ID of the room to leave
*/
public async leave(roomId: string): Promise<void> {
const client = await this.prepareClient();
return client.evaluate(async (client, roomId) => {
await client.leave(roomId);
}, roomId);
}
/**
* Sets the directory visibility for a room.
* @param roomId ID of the room to set the directory visibility for
* @param visibility The new visibility for the room
*/
public async setRoomDirectoryVisibility(roomId: string, visibility: Visibility): Promise<void> {
const client = await this.prepareClient();
return client.evaluate(
async (client, { roomId, visibility }) => {
await client.setRoomDirectoryVisibility(roomId, visibility);
},
{ roomId, visibility },
);
}
}
/** Call `CryptoApi.bootstrapCrossSigning` on the given Matrix client, using the given credentials to authenticate
* the UIA request.
*/
export function bootstrapCrossSigningForClient(
client: JSHandle<MatrixClient>,
credentials: Credentials,
resetKeys: boolean = false,
) {
return client.evaluate(
async (client, { credentials, resetKeys }) => {
await client.getCrypto().bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (func) => {
await func({
type: "m.login.password",
identifier: {
type: "m.id.user",
user: credentials.userId,
},
password: credentials.password,
});
},
setupNewCrossSigning: resetKeys,
});
},
{ credentials, resetKeys },
);
}