From 79daa1a63c0992fdf740aa54b99cdde96f4a49ab Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 30 Nov 2023 10:18:18 +0000
Subject: [PATCH] Migrate remaining editing.spec.ts from Cypress to Playwright
(#11976)
* Migrate user-view.spec.ts from Cypress to Playwright
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Add bot support & update screenshot
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Add screenshot
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Use JSHandle
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Remove stale snapshots
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Migrate remaining editing.spec.ts from Cypress to Playwright
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* yay
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Fix tests
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Fix tests
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---------
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
cypress/e2e/editing/editing.spec.ts | 121 --------------
playwright/e2e/editing/editing.spec.ts | 93 ++++++++++-
playwright/e2e/left-panel/left-panel.spec.ts | 2 +-
playwright/e2e/location/location.spec.ts | 2 +-
playwright/e2e/login/consent.spec.ts | 4 +-
.../appearance-user-settings-tab.spec.ts | 4 +-
.../general-room-settings-tab.spec.ts | 2 +-
playwright/element-web-test.ts | 5 +-
playwright/global.d.ts | 20 +--
playwright/pages/ElementAppPage.ts | 45 +-----
playwright/pages/bot.ts | 63 ++------
playwright/pages/client.ts | 149 ++++++++++++++++++
12 files changed, 262 insertions(+), 248 deletions(-)
delete mode 100644 cypress/e2e/editing/editing.spec.ts
create mode 100644 playwright/pages/client.ts
diff --git a/cypress/e2e/editing/editing.spec.ts b/cypress/e2e/editing/editing.spec.ts
deleted file mode 100644
index 113da3421a..0000000000
--- a/cypress/e2e/editing/editing.spec.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-///
-
-import type { MsgType, IContent } from "matrix-js-sdk/src/matrix";
-import { HomeserverInstance } from "../../plugins/utils/homeserver";
-
-/** generate a message event which will take up some room on the page. */
-function mkPadding(n: number): IContent {
- return {
- msgtype: "m.text" as MsgType,
- body: `padding ${n}`,
- format: "org.matrix.custom.html",
- formatted_body: `
Test event ${n}
\n`.repeat(10),
- };
-}
-
-describe("Editing", () => {
- let homeserver: HomeserverInstance;
-
- beforeEach(() => {
- cy.startHomeserver("default").then((data) => {
- homeserver = data;
- cy.initTestUser(homeserver, "Edith").then(() => {
- cy.createRoom({ name: "Test room" });
- cy.injectAxe();
- });
- });
- });
-
- afterEach(() => {
- cy.stopHomeserver(homeserver);
- });
-
- it("should correctly display events which are edited, where we lack the edit event", () => {
- // This tests the behaviour when a message has been edited some time after it has been sent, and we
- // jump back in room history to view the event, but do not have the actual edit event.
- //
- // In that scenario, we rely on the server to replace the content (pre-MSC3925), or do it ourselves based on
- // the bundled edit event (post-MSC3925).
- //
- // To test it, we need to have a room with lots of events in, so we can jump around the timeline without
- // paginating in the event itself. Hence, we create a bot user which creates the room and populates it before
- // we join.
-
- let testRoomId: string;
- let originalEventId: string;
- let editEventId: string;
-
- // create a second user
- const bobChainable = cy.getBot(homeserver, { displayName: "Bob", userIdPrefix: "bob_" });
-
- cy.all([cy.window({ log: false }), bobChainable]).then(async ([win, bob]) => {
- // "bob" now creates the room, and sends a load of events in it. Note that all of this happens via calls on
- // the js-sdk rather than Cypress commands, so uses regular async/await.
-
- const room = await bob.createRoom({ name: "TestRoom", visibility: win.matrixcs.Visibility.Public });
- testRoomId = room.room_id;
- cy.log(`Bot user created room ${room.room_id}`);
-
- originalEventId = (await bob.sendMessage(room.room_id, { body: "original", msgtype: "m.text" })).event_id;
- cy.log(`Bot user sent original event ${originalEventId}`);
-
- // send a load of padding events. We make them large, so that they fill the whole screen
- // and the client doesn't end up paginating into the event we want.
- let i = 0;
- while (i < 10) {
- await bob.sendMessage(room.room_id, mkPadding(i++));
- }
-
- // ... then the edit ...
- editEventId = (
- await bob.sendMessage(room.room_id, {
- "m.new_content": { body: "Edited body", msgtype: "m.text" },
- "m.relates_to": {
- rel_type: "m.replace",
- event_id: originalEventId,
- },
- "body": "* edited",
- "msgtype": "m.text",
- })
- ).event_id;
- cy.log(`Bot user sent edit event ${editEventId}`);
-
- // ... then a load more padding ...
- while (i < 20) {
- await bob.sendMessage(room.room_id, mkPadding(i++));
- }
- });
-
- cy.getClient().then((cli) => {
- // now have the cypress user join the room, jump to the original event, and wait for the event to be
- // visible
- cy.joinRoom(testRoomId);
- cy.viewRoomByName("TestRoom");
- cy.visit(`#/room/${testRoomId}/${originalEventId}`);
- cy.get(`[data-event-id="${originalEventId}"]`).should((messageTile) => {
- // at this point, the edit event should still be unknown
- expect(cli.getRoom(testRoomId).getTimelineForEvent(editEventId)).to.be.null;
-
- // nevertheless, the event should be updated
- expect(messageTile.find(".mx_EventTile_body").text()).to.eq("Edited body");
- expect(messageTile.find(".mx_EventTile_edited")).to.exist;
- });
- });
- });
-});
diff --git a/playwright/e2e/editing/editing.spec.ts b/playwright/e2e/editing/editing.spec.ts
index d8add58d81..981c84ddca 100644
--- a/playwright/e2e/editing/editing.spec.ts
+++ b/playwright/e2e/editing/editing.spec.ts
@@ -16,17 +16,27 @@ limitations under the License.
import { Locator, Page } from "@playwright/test";
-import type { EventType, MsgType, ISendEventResponse } from "matrix-js-sdk/src/matrix";
-import { test, expect } from "../../element-web-test";
+import type { EventType, IContent, ISendEventResponse, MsgType, Visibility } from "matrix-js-sdk/src/matrix";
+import { expect, test } from "../../element-web-test";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { SettingLevel } from "../../../src/settings/SettingLevel";
-const sendEvent = async (app: ElementAppPage, roomId: string): Promise => {
- return app.sendEvent(roomId, null, "m.room.message" as EventType, {
+async function sendEvent(app: ElementAppPage, roomId: string): Promise {
+ return app.client.sendEvent(roomId, null, "m.room.message" as EventType, {
msgtype: "m.text" as MsgType,
body: "Message",
});
-};
+}
+
+/** generate a message event which will take up some room on the page. */
+function mkPadding(n: number): IContent {
+ return {
+ msgtype: "m.text" as MsgType,
+ body: `padding ${n}`,
+ format: "org.matrix.custom.html",
+ formatted_body: `Test event ${n}
\n`.repeat(10),
+ };
+}
test.describe("Editing", () => {
// Edit "Message"
@@ -58,9 +68,10 @@ test.describe("Editing", () => {
test.use({
displayName: "Edith",
room: async ({ user, app }, use) => {
- const roomId = await app.createRoom({ name: "Test room" });
+ const roomId = await app.client.createRoom({ name: "Test room" });
await use({ roomId });
},
+ botCreateOpts: { displayName: "Bob" },
});
test("should render and interact with the message edit history dialog", async ({ page, user, app, room }) => {
@@ -289,4 +300,74 @@ test.describe("Editing", () => {
// Assert that the edit composer has gone away
await expect(page.getByRole("textbox", { name: "Edit message" })).not.toBeVisible();
});
+
+ test("should correctly display events which are edited, where we lack the edit event", async ({
+ page,
+ user,
+ app,
+ axe,
+ checkA11y,
+ bot: bob,
+ }) => {
+ // This tests the behaviour when a message has been edited some time after it has been sent, and we
+ // jump back in room history to view the event, but do not have the actual edit event.
+ //
+ // In that scenario, we rely on the server to replace the content (pre-MSC3925), or do it ourselves based on
+ // the bundled edit event (post-MSC3925).
+ //
+ // To test it, we need to have a room with lots of events in, so we can jump around the timeline without
+ // paginating in the event itself. Hence, we create a bot user which creates the room and populates it before
+ // we join.
+
+ // "bob" now creates the room, and sends a load of events in it. Note that all of this happens via calls on
+ // the js-sdk rather than Cypress commands, so uses regular async/await.
+ const testRoomId = await bob.createRoom({ name: "TestRoom", visibility: "public" as Visibility });
+
+ const { event_id: originalEventId } = await bob.sendMessage(testRoomId, {
+ body: "original",
+ msgtype: "m.text",
+ });
+
+ // send a load of padding events. We make them large, so that they fill the whole screen
+ // and the client doesn't end up paginating into the event we want.
+ let i = 0;
+ while (i < 10) {
+ await bob.sendMessage(testRoomId, mkPadding(i++));
+ }
+
+ // ... then the edit ...
+ const editEventId = (
+ await bob.sendMessage(testRoomId, {
+ "m.new_content": { body: "Edited body", msgtype: "m.text" },
+ "m.relates_to": {
+ rel_type: "m.replace",
+ event_id: originalEventId,
+ },
+ "body": "* edited",
+ "msgtype": "m.text",
+ })
+ ).event_id;
+
+ // ... then a load more padding ...
+ while (i < 20) {
+ await bob.sendMessage(testRoomId, mkPadding(i++));
+ }
+
+ // now have the cypress user join the room, jump to the original event, and wait for the event to be visible
+ await app.client.joinRoom(testRoomId);
+ await app.viewRoomByName("TestRoom");
+ await page.goto(`#/room/${testRoomId}/${originalEventId}`);
+
+ const messageTile = page.locator(`[data-event-id="${originalEventId}"]`);
+ // at this point, the edit event should still be unknown
+ const timeline = await app.client.evaluate(
+ (cli, { testRoomId, editEventId }) => cli.getRoom(testRoomId).getTimelineForEvent(editEventId),
+ { testRoomId, editEventId },
+ );
+ expect(timeline).toBeNull();
+
+ // nevertheless, the event should be updated
+ await expect(messageTile.locator(".mx_EventTile_body")).toHaveText("Edited body");
+ await expect(messageTile.locator(".mx_EventTile_edited")).toBeVisible();
+ });
});
diff --git a/playwright/e2e/left-panel/left-panel.spec.ts b/playwright/e2e/left-panel/left-panel.spec.ts
index ae0efcad0c..98e4910854 100644
--- a/playwright/e2e/left-panel/left-panel.spec.ts
+++ b/playwright/e2e/left-panel/left-panel.spec.ts
@@ -24,7 +24,7 @@ test.describe("LeftPanel", () => {
test("should render the Rooms list", async ({ page, app, user }) => {
// create rooms and check room names are correct
for (const name of ["Apple", "Pineapple", "Orange"]) {
- await app.createRoom({ name });
+ await app.client.createRoom({ name });
await expect(page.getByRole("treeitem", { name })).toBeVisible();
}
});
diff --git a/playwright/e2e/location/location.spec.ts b/playwright/e2e/location/location.spec.ts
index 4b2d0f796d..e3f6120ef3 100644
--- a/playwright/e2e/location/location.spec.ts
+++ b/playwright/e2e/location/location.spec.ts
@@ -38,7 +38,7 @@ test.describe("Location sharing", () => {
});
test("sends and displays pin drop location message successfully", async ({ page, user, app }) => {
- const roomId = await app.createRoom({});
+ const roomId = await app.client.createRoom({});
await page.goto(`/#/room/${roomId}`);
const composerOptions = await app.openMessageComposerOptions();
diff --git a/playwright/e2e/login/consent.spec.ts b/playwright/e2e/login/consent.spec.ts
index eb966d7141..6e0c9df3db 100644
--- a/playwright/e2e/login/consent.spec.ts
+++ b/playwright/e2e/login/consent.spec.ts
@@ -31,7 +31,7 @@ test.describe("Consent", () => {
app,
}) => {
// Attempt to create a room using the js-sdk which should return an error with `M_CONSENT_NOT_GIVEN`
- await app.createRoom({}).catch(() => {});
+ await app.client.createRoom({}).catch(() => {});
const newPagePromise = new Promise((resolve) => context.once("page", resolve));
const dialog = page.locator(".mx_QuestionDialog");
@@ -49,7 +49,7 @@ test.describe("Consent", () => {
await expect(page.locator(".mx_MatrixChat")).toBeVisible();
// attempt to perform the same action again and expect it to not fail
- await app.createRoom({ name: "Test Room" });
+ await app.client.createRoom({ name: "Test Room" });
await expect(page.getByText("Test Room")).toBeVisible();
});
});
diff --git a/playwright/e2e/settings/appearance-user-settings-tab.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab.spec.ts
index dc834f00da..e924bffa01 100644
--- a/playwright/e2e/settings/appearance-user-settings-tab.spec.ts
+++ b/playwright/e2e/settings/appearance-user-settings-tab.spec.ts
@@ -38,7 +38,7 @@ test.describe("Appearance user settings tab", () => {
test("should support switching layouts", async ({ page, user, app }) => {
// Create and view a room first
- await app.createRoom({ name: "Test Room" });
+ await app.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room");
await app.settings.openUserSettings("Appearance");
@@ -119,7 +119,7 @@ test.describe("Appearance user settings tab", () => {
test("should support enabling compact group (modern) layout", async ({ page, app, user }) => {
// Create and view a room first
- await app.createRoom({ name: "Test Room" });
+ await app.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room");
await app.settings.openUserSettings("Appearance");
diff --git a/playwright/e2e/settings/general-room-settings-tab.spec.ts b/playwright/e2e/settings/general-room-settings-tab.spec.ts
index 6ba59bf22d..b73f6ce50b 100644
--- a/playwright/e2e/settings/general-room-settings-tab.spec.ts
+++ b/playwright/e2e/settings/general-room-settings-tab.spec.ts
@@ -24,7 +24,7 @@ test.describe("General room settings tab", () => {
});
test.beforeEach(async ({ user, app }) => {
- await app.createRoom({ name: roomName });
+ await app.client.createRoom({ name: roomName });
await app.viewRoomByName(roomName);
});
diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts
index 2110ad7d10..9433581aed 100644
--- a/playwright/element-web-test.ts
+++ b/playwright/element-web-test.ts
@@ -179,7 +179,8 @@ export const test = base.extend<
}),
app: async ({ page }, use) => {
- await use(new ElementAppPage(page));
+ const app = new ElementAppPage(page);
+ await use(app);
},
crypto: async ({ page, homeserver, request }, use) => {
await use(new Crypto(page, homeserver, request));
@@ -191,7 +192,7 @@ export const test = base.extend<
botCreateOpts: {},
bot: async ({ page, homeserver, botCreateOpts }, use) => {
const bot = new Bot(page, homeserver, botCreateOpts);
- await bot.start();
+ await bot.prepareClient(); // eagerly register the bot
await use(bot);
},
});
diff --git a/playwright/global.d.ts b/playwright/global.d.ts
index 87fdebcc5f..c537d0a142 100644
--- a/playwright/global.d.ts
+++ b/playwright/global.d.ts
@@ -14,31 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {
- ICreateClientOpts,
- type MatrixClient,
- MatrixScheduler,
- MemoryCryptoStore,
- MemoryStore,
-} from "matrix-js-sdk/src/matrix";
-
+import type * as Matrix from "matrix-js-sdk/src/matrix";
import { type SettingLevel } from "../src/settings/SettingLevel";
declare global {
interface Window {
mxMatrixClientPeg: {
- get(): MatrixClient;
+ get(): Matrix.MatrixClient;
};
mxSettingsStore: {
setValue(settingName: string, roomId: string | null, level: SettingLevel, value: any): Promise;
};
- // Partial type for the matrix-js-sdk module, exported by browser-matrix
- matrixcs: {
- MatrixClient: typeof MatrixClient;
- MatrixScheduler: typeof MatrixScheduler;
- MemoryStore: typeof MemoryStore;
- MemoryCryptoStore: typeof MemoryCryptoStore;
- createClient(opts: ICreateClientOpts | string);
- };
+ matrixcs: typeof Matrix;
}
}
diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts
index de39a7bc32..76037ff58e 100644
--- a/playwright/pages/ElementAppPage.ts
+++ b/playwright/pages/ElementAppPage.ts
@@ -16,13 +16,14 @@ limitations under the License.
import { type Locator, type Page } from "@playwright/test";
-import type { IContent, ICreateRoomOpts, ISendEventResponse } from "matrix-js-sdk/src/matrix";
import { Settings } from "./settings";
+import { Client } from "./client";
export class ElementAppPage {
public constructor(private readonly page: Page) {}
public settings = new Settings(this.page);
+ public client: Client = new Client(this.page);
/**
* Open the top left user menu, returning a Locator to the resulting context menu.
@@ -47,20 +48,6 @@ export class ElementAppPage {
return this.settings.closeDialog();
}
- /**
- * 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 {
- return this.page.evaluate, ICreateRoomOpts>(async (options) => {
- return window.mxMatrixClientPeg
- .get()
- .createRoom(options)
- .then((res) => res.room_id);
- }, options);
- }
-
/**
* Opens the given room by name. The room must be visible in the
* room list, but the room list may be folded horizontally, and the
@@ -107,32 +94,4 @@ export class ElementAppPage {
await composer.getByRole("button", { name: "More options", exact: true }).click();
return this.page.getByRole("menu");
}
-
- /**
- * @param {string} roomId
- * @param {string} threadId
- * @param {string} eventType
- * @param {Object} content
- */
- public async sendEvent(
- roomId: string,
- threadId: string | null,
- eventType: string,
- content: IContent,
- ): Promise {
- return this.page.evaluate<
- Promise,
- {
- roomId: string;
- threadId: string | null;
- eventType: string;
- content: IContent;
- }
- >(
- async ({ roomId, threadId, eventType, content }) => {
- return window.mxMatrixClientPeg.get().sendEvent(roomId, threadId, eventType, content);
- },
- { roomId, threadId, eventType, content },
- );
- }
}
diff --git a/playwright/pages/bot.ts b/playwright/pages/bot.ts
index 298c744e80..f005880095 100644
--- a/playwright/pages/bot.ts
+++ b/playwright/pages/bot.ts
@@ -17,9 +17,10 @@ limitations under the License.
import { JSHandle, Page } from "@playwright/test";
import { uniqueId } from "lodash";
-import type { MatrixClient, ISendEventResponse } from "matrix-js-sdk/src/matrix";
+import type { MatrixClient } from "matrix-js-sdk/src/matrix";
import type { AddSecretStorageKeyOpts } from "matrix-js-sdk/src/secret-storage";
import type { Credentials, HomeserverInstance } from "../plugins/homeserver";
+import { Client } from "./client";
export interface CreateBotOpts {
/**
@@ -59,27 +60,24 @@ const defaultCreateBotOptions = {
bootstrapCrossSigning: true,
} satisfies CreateBotOpts;
-export class Bot {
- private client: JSHandle;
+export class Bot extends Client {
public credentials?: Credentials;
- constructor(private page: Page, private homeserver: HomeserverInstance, private readonly opts: CreateBotOpts) {
+ constructor(page: Page, private homeserver: HomeserverInstance, private readonly opts: CreateBotOpts) {
+ super(page);
this.opts = Object.assign({}, defaultCreateBotOptions, opts);
}
- public async start(): Promise {
- this.credentials = await this.getCredentials();
- this.client = await this.setupBotClient();
- }
-
private async getCredentials(): Promise {
+ if (this.credentials) return this.credentials;
const username = uniqueId(this.opts.userIdPrefix);
const password = uniqueId("password_");
console.log(`getBot: Create bot user ${username} with opts ${JSON.stringify(this.opts)}`);
- return await this.homeserver.registerUser(username, password, this.opts.displayName);
+ this.credentials = await this.homeserver.registerUser(username, password, this.opts.displayName);
+ return this.credentials;
}
- private async setupBotClient(): Promise> {
+ protected async getClientHandle(): Promise> {
return this.page.evaluateHandle(
async ({ homeserver, credentials, opts }) => {
const keys = {};
@@ -123,7 +121,7 @@ export class Bot {
});
if (opts.autoAcceptInvites) {
- cli.on((window as any).matrixcs.RoomMemberEvent.Membership, (event, member) => {
+ cli.on(window.matrixcs.RoomMemberEvent.Membership, (event, member) => {
if (member.membership === "invite" && member.userId === cli.getUserId()) {
cli.joinRoom(member.roomId);
}
@@ -173,48 +171,9 @@ export class Bot {
},
{
homeserver: this.homeserver.config,
- credentials: this.credentials,
+ credentials: await this.getCredentials(),
opts: this.opts,
},
);
}
-
- /**
- * Make this bot join a room by name
- * @param roomName Name of the room to join
- */
- public async joinRoomByName(roomName: string): Promise {
- await this.client.evaluate(
- (client, { roomName }) => {
- const room = client.getRooms().find((r) => r.getDefaultRoomName(client.getUserId()) === roomName);
- if (room) {
- return client.joinRoom(room.roomId);
- }
- throw new Error(`Bot room join failed. Cannot find room '${roomName}'`);
- },
- {
- roomName,
- },
- );
- }
-
- /**
- * Send a message as a bot into a room
- * @param roomId ID of the room to join
- * @param message the message body to send
- */
- public async sendStringMessage(roomId: string, message: string): Promise {
- return this.client.evaluate(
- (client, { roomId, message }) => {
- return client.sendMessage(roomId, {
- msgtype: "m.text",
- body: message,
- });
- },
- {
- roomId,
- message,
- },
- );
- }
}
diff --git a/playwright/pages/client.ts b/playwright/pages/client.ts
new file mode 100644
index 0000000000..c1e78bf6a2
--- /dev/null
+++ b/playwright/pages/client.ts
@@ -0,0 +1,149 @@
+/*
+Copyright 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { JSHandle, Page } from "@playwright/test";
+import { PageFunctionOn } from "playwright-core/types/structs";
+
+import type { IContent, ICreateRoomOpts, ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
+
+export class Client {
+ protected client: JSHandle;
+
+ protected getClientHandle(): Promise> {
+ return this.page.evaluateHandle(() => window.mxMatrixClientPeg.get());
+ }
+
+ public async prepareClient(): Promise> {
+ if (!this.client) {
+ this.client = await this.getClientHandle();
+ }
+ return this.client;
+ }
+
+ public constructor(protected readonly page: Page) {
+ page.on("framenavigated", async () => {
+ this.client = null;
+ });
+ }
+
+ public evaluate(
+ pageFunction: PageFunctionOn,
+ arg: Arg,
+ ): Promise;
+ public evaluate(
+ pageFunction: PageFunctionOn,
+ arg?: any,
+ ): Promise;
+ public async evaluate(fn: (client: MatrixClient) => T, arg?: any): Promise {
+ await this.prepareClient();
+ return this.client.evaluate(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 {
+ const client = await this.prepareClient();
+ return client.evaluate(
+ async (client, { roomId, threadId, eventType, content }) => {
+ return client.sendEvent(roomId, threadId, eventType, content);
+ },
+ { roomId, threadId, eventType, content },
+ );
+ }
+
+ /**
+ * Send a message as a bot into a room
+ * @param roomId ID of the room to send the message into
+ * @param content the event content to send
+ */
+ public async sendMessage(roomId: string, content: IContent): Promise {
+ const client = await this.prepareClient();
+ return client.evaluate(
+ (client, { roomId, content }) => {
+ return client.sendMessage(roomId, content);
+ },
+ {
+ roomId,
+ content,
+ },
+ );
+ }
+
+ /**
+ * 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 {
+ 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((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);
+ }
+
+ /**
+ * 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 {
+ 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 {
+ const client = await this.prepareClient();
+ await client.evaluate(
+ (client, { roomName }) => {
+ const room = client.getRooms().find((r) => r.getDefaultRoomName(client.getUserId()) === roomName);
+ if (room) {
+ return client.joinRoom(room.roomId);
+ }
+ throw new Error(`Bot room join failed. Cannot find room '${roomName}'`);
+ },
+ {
+ roomName,
+ },
+ );
+ }
+}