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>
pull/28788/head^2
Michael Telatynski 2023-11-30 10:18:18 +00:00 committed by GitHub
parent 07b7ee6111
commit 79daa1a63c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 262 additions and 248 deletions

View File

@ -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.
*/
/// <reference types="cypress" />
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: `<h3>Test event ${n}</h3>\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;
});
});
});
});

View File

@ -16,17 +16,27 @@ limitations under the License.
import { Locator, Page } from "@playwright/test"; import { Locator, Page } from "@playwright/test";
import type { EventType, MsgType, ISendEventResponse } from "matrix-js-sdk/src/matrix"; import type { EventType, IContent, ISendEventResponse, MsgType, Visibility } from "matrix-js-sdk/src/matrix";
import { test, expect } from "../../element-web-test"; import { expect, test } from "../../element-web-test";
import { ElementAppPage } from "../../pages/ElementAppPage"; import { ElementAppPage } from "../../pages/ElementAppPage";
import { SettingLevel } from "../../../src/settings/SettingLevel"; import { SettingLevel } from "../../../src/settings/SettingLevel";
const sendEvent = async (app: ElementAppPage, roomId: string): Promise<ISendEventResponse> => { async function sendEvent(app: ElementAppPage, roomId: string): Promise<ISendEventResponse> {
return app.sendEvent(roomId, null, "m.room.message" as EventType, { return app.client.sendEvent(roomId, null, "m.room.message" as EventType, {
msgtype: "m.text" as MsgType, msgtype: "m.text" as MsgType,
body: "Message", 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: `<h3>Test event ${n}</h3>\n`.repeat(10),
};
}
test.describe("Editing", () => { test.describe("Editing", () => {
// Edit "Message" // Edit "Message"
@ -58,9 +68,10 @@ test.describe("Editing", () => {
test.use({ test.use({
displayName: "Edith", displayName: "Edith",
room: async ({ user, app }, use) => { 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 }); await use({ roomId });
}, },
botCreateOpts: { displayName: "Bob" },
}); });
test("should render and interact with the message edit history dialog", async ({ page, user, app, room }) => { 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 // Assert that the edit composer has gone away
await expect(page.getByRole("textbox", { name: "Edit message" })).not.toBeVisible(); 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();
});
}); });

View File

@ -24,7 +24,7 @@ test.describe("LeftPanel", () => {
test("should render the Rooms list", async ({ page, app, user }) => { test("should render the Rooms list", async ({ page, app, user }) => {
// create rooms and check room names are correct // create rooms and check room names are correct
for (const name of ["Apple", "Pineapple", "Orange"]) { for (const name of ["Apple", "Pineapple", "Orange"]) {
await app.createRoom({ name }); await app.client.createRoom({ name });
await expect(page.getByRole("treeitem", { name })).toBeVisible(); await expect(page.getByRole("treeitem", { name })).toBeVisible();
} }
}); });

View File

@ -38,7 +38,7 @@ test.describe("Location sharing", () => {
}); });
test("sends and displays pin drop location message successfully", async ({ page, user, app }) => { 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}`); await page.goto(`/#/room/${roomId}`);
const composerOptions = await app.openMessageComposerOptions(); const composerOptions = await app.openMessageComposerOptions();

View File

@ -31,7 +31,7 @@ test.describe("Consent", () => {
app, app,
}) => { }) => {
// Attempt to create a room using the js-sdk which should return an error with `M_CONSENT_NOT_GIVEN` // 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<Page>((resolve) => context.once("page", resolve)); const newPagePromise = new Promise<Page>((resolve) => context.once("page", resolve));
const dialog = page.locator(".mx_QuestionDialog"); const dialog = page.locator(".mx_QuestionDialog");
@ -49,7 +49,7 @@ test.describe("Consent", () => {
await expect(page.locator(".mx_MatrixChat")).toBeVisible(); await expect(page.locator(".mx_MatrixChat")).toBeVisible();
// attempt to perform the same action again and expect it to not fail // 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(); await expect(page.getByText("Test Room")).toBeVisible();
}); });
}); });

View File

@ -38,7 +38,7 @@ test.describe("Appearance user settings tab", () => {
test("should support switching layouts", async ({ page, user, app }) => { test("should support switching layouts", async ({ page, user, app }) => {
// Create and view a room first // 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.viewRoomByName("Test Room");
await app.settings.openUserSettings("Appearance"); 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 }) => { test("should support enabling compact group (modern) layout", async ({ page, app, user }) => {
// Create and view a room first // 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.viewRoomByName("Test Room");
await app.settings.openUserSettings("Appearance"); await app.settings.openUserSettings("Appearance");

View File

@ -24,7 +24,7 @@ test.describe("General room settings tab", () => {
}); });
test.beforeEach(async ({ user, app }) => { test.beforeEach(async ({ user, app }) => {
await app.createRoom({ name: roomName }); await app.client.createRoom({ name: roomName });
await app.viewRoomByName(roomName); await app.viewRoomByName(roomName);
}); });

View File

@ -179,7 +179,8 @@ export const test = base.extend<
}), }),
app: async ({ page }, use) => { app: async ({ page }, use) => {
await use(new ElementAppPage(page)); const app = new ElementAppPage(page);
await use(app);
}, },
crypto: async ({ page, homeserver, request }, use) => { crypto: async ({ page, homeserver, request }, use) => {
await use(new Crypto(page, homeserver, request)); await use(new Crypto(page, homeserver, request));
@ -191,7 +192,7 @@ export const test = base.extend<
botCreateOpts: {}, botCreateOpts: {},
bot: async ({ page, homeserver, botCreateOpts }, use) => { bot: async ({ page, homeserver, botCreateOpts }, use) => {
const bot = new Bot(page, homeserver, botCreateOpts); const bot = new Bot(page, homeserver, botCreateOpts);
await bot.start(); await bot.prepareClient(); // eagerly register the bot
await use(bot); await use(bot);
}, },
}); });

View File

@ -14,31 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { import type * as Matrix from "matrix-js-sdk/src/matrix";
ICreateClientOpts,
type MatrixClient,
MatrixScheduler,
MemoryCryptoStore,
MemoryStore,
} from "matrix-js-sdk/src/matrix";
import { type SettingLevel } from "../src/settings/SettingLevel"; import { type SettingLevel } from "../src/settings/SettingLevel";
declare global { declare global {
interface Window { interface Window {
mxMatrixClientPeg: { mxMatrixClientPeg: {
get(): MatrixClient; get(): Matrix.MatrixClient;
}; };
mxSettingsStore: { mxSettingsStore: {
setValue(settingName: string, roomId: string | null, level: SettingLevel, value: any): Promise<void>; setValue(settingName: string, roomId: string | null, level: SettingLevel, value: any): Promise<void>;
}; };
// Partial type for the matrix-js-sdk module, exported by browser-matrix matrixcs: typeof Matrix;
matrixcs: {
MatrixClient: typeof MatrixClient;
MatrixScheduler: typeof MatrixScheduler;
MemoryStore: typeof MemoryStore;
MemoryCryptoStore: typeof MemoryCryptoStore;
createClient(opts: ICreateClientOpts | string);
};
} }
} }

View File

@ -16,13 +16,14 @@ limitations under the License.
import { type Locator, type Page } from "@playwright/test"; import { type Locator, type Page } from "@playwright/test";
import type { IContent, ICreateRoomOpts, ISendEventResponse } from "matrix-js-sdk/src/matrix";
import { Settings } from "./settings"; import { Settings } from "./settings";
import { Client } from "./client";
export class ElementAppPage { export class ElementAppPage {
public constructor(private readonly page: Page) {} public constructor(private readonly page: Page) {}
public settings = new Settings(this.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. * 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(); 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<string> {
return this.page.evaluate<Promise<string>, 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 * 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 * 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(); await composer.getByRole("button", { name: "More options", exact: true }).click();
return this.page.getByRole("menu"); 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<ISendEventResponse> {
return this.page.evaluate<
Promise<ISendEventResponse>,
{
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 },
);
}
} }

View File

@ -17,9 +17,10 @@ limitations under the License.
import { JSHandle, Page } from "@playwright/test"; import { JSHandle, Page } from "@playwright/test";
import { uniqueId } from "lodash"; 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 { AddSecretStorageKeyOpts } from "matrix-js-sdk/src/secret-storage";
import type { Credentials, HomeserverInstance } from "../plugins/homeserver"; import type { Credentials, HomeserverInstance } from "../plugins/homeserver";
import { Client } from "./client";
export interface CreateBotOpts { export interface CreateBotOpts {
/** /**
@ -59,27 +60,24 @@ const defaultCreateBotOptions = {
bootstrapCrossSigning: true, bootstrapCrossSigning: true,
} satisfies CreateBotOpts; } satisfies CreateBotOpts;
export class Bot { export class Bot extends Client {
private client: JSHandle<MatrixClient>;
public credentials?: Credentials; 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); this.opts = Object.assign({}, defaultCreateBotOptions, opts);
} }
public async start(): Promise<void> {
this.credentials = await this.getCredentials();
this.client = await this.setupBotClient();
}
private async getCredentials(): Promise<Credentials> { private async getCredentials(): Promise<Credentials> {
if (this.credentials) return this.credentials;
const username = uniqueId(this.opts.userIdPrefix); const username = uniqueId(this.opts.userIdPrefix);
const password = uniqueId("password_"); const password = uniqueId("password_");
console.log(`getBot: Create bot user ${username} with opts ${JSON.stringify(this.opts)}`); 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<JSHandle<MatrixClient>> { protected async getClientHandle(): Promise<JSHandle<MatrixClient>> {
return this.page.evaluateHandle( return this.page.evaluateHandle(
async ({ homeserver, credentials, opts }) => { async ({ homeserver, credentials, opts }) => {
const keys = {}; const keys = {};
@ -123,7 +121,7 @@ export class Bot {
}); });
if (opts.autoAcceptInvites) { 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()) { if (member.membership === "invite" && member.userId === cli.getUserId()) {
cli.joinRoom(member.roomId); cli.joinRoom(member.roomId);
} }
@ -173,48 +171,9 @@ export class Bot {
}, },
{ {
homeserver: this.homeserver.config, homeserver: this.homeserver.config,
credentials: this.credentials, credentials: await this.getCredentials(),
opts: this.opts, 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<void> {
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<ISendEventResponse> {
return this.client.evaluate(
(client, { roomId, message }) => {
return client.sendMessage(roomId, {
msgtype: "m.text",
body: message,
});
},
{
roomId,
message,
},
);
}
} }

149
playwright/pages/client.ts Normal file
View File

@ -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<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;
});
}
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);
}
/**
* @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 }) => {
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<ISendEventResponse> {
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<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);
}
/**
* 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<void> {
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,
},
);
}
}