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, + }, + ); + } +}