mirror of https://github.com/vector-im/riot-web
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
parent
07b7ee6111
commit
79daa1a63c
|
@ -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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue