Playwright test for history sharing on invite (#30948)

* Playwright: `getCurrentRoomIdFromUrl`

Helper function to fish a room ID out of the URL

* Playwright: use updated `Credentials` class from common lib

Pick up the extended `Credentials` interface that was added in
https://github.com/element-hq/element-modules/pull/80.

* Playwright: use `routeConfigJson` from common lib

https://github.com/element-hq/element-modules/pull/81 added a utility function
for building and routing `config.json`; we should use it.

* Playwright test for history sharing on invite

Fixes https://github.com/element-hq/element-meta/issues/2920

* Avoid use of CSS in playwright locators
This commit is contained in:
Richard van der Hoff
2025-10-08 09:49:11 +00:00
committed by GitHub
parent 0fcc4d15c8
commit 4a0e8d661f
9 changed files with 102 additions and 35 deletions
@@ -0,0 +1,59 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { createNewInstance } from "@element-hq/element-web-playwright-common";
import { expect, test } from "../../element-web-test";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { createRoom, sendMessageInCurrentRoom } from "./utils";
test.use({
displayName: "Alice",
labsFlags: ["feature_share_history_on_invite"],
});
/** Tests for MSC4268: encrypted history sharing */
test.describe("History sharing", function () {
test(
"We should share history when sending invites",
{ tag: "@screenshot" },
async (
{ labsFlags, browser, page: alicePage, user: aliceCredentials, app: aliceElementApp, homeserver },
testInfo,
) => {
// In this test, Alice creates an encrypted room and sends an event;
// we then invite Bob, and ensure Bob can see the content.
await aliceElementApp.client.bootstrapCrossSigning(aliceCredentials);
// Register a second user, and open it in a second instance of the app
const bobCredentials = await homeserver.registerUser(`user_${testInfo.testId}_bob`, "password", "Bob");
const bobPage = await createNewInstance(browser, bobCredentials, {}, labsFlags);
const bobElementApp = new ElementAppPage(bobPage);
await bobElementApp.client.bootstrapCrossSigning(bobCredentials);
// Create the room and send a message
await createRoom(alicePage, "TestRoom", true);
await sendMessageInCurrentRoom(alicePage, "A message from Alice");
// Send the invite to Bob
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
// Bob accepts the invite
await bobPage.getByRole("option", { name: "TestRoom" }).click();
await bobPage.getByRole("button", { name: "Accept" }).click();
// Bob should now be able to decrypt the event
await expect(bobPage.getByText("A message from Alice")).toBeVisible();
const mask = [bobPage.locator(".mx_MessageTimestamp")];
await expect(bobPage.locator(".mx_RoomView_body")).toMatchScreenshot("shared-history-invite-accepted.png", {
mask,
});
},
);
});
@@ -27,8 +27,7 @@ test.describe("Create Knock Room", () => {
await expect(page.locator(".mx_RoomHeader").getByText("Cybersecurity")).toBeVisible();
const urlHash = await page.evaluate(() => window.location.hash);
const roomId = urlHash.replace("#/room/", "");
const roomId = await app.getCurrentRoomIdFromUrl();
// Room should have a knock join rule
await waitForRoom(page, app.client, roomId, (room) => {
@@ -44,8 +43,7 @@ test.describe("Create Knock Room", () => {
await expect(page.locator(".mx_RoomHeader").getByText("Cybersecurity")).toBeVisible();
const urlHash = await page.evaluate(() => window.location.hash);
const roomId = urlHash.replace("#/room/", "");
const roomId = await app.getCurrentRoomIdFromUrl();
await app.settings.openRoomSettings("Security & Privacy");
@@ -70,8 +68,7 @@ test.describe("Create Knock Room", () => {
await expect(page.locator(".mx_RoomHeader").getByText("Cybersecurity")).toBeVisible();
const urlHash = await page.evaluate(() => window.location.hash);
const roomId = urlHash.replace("#/room/", "");
const roomId = await app.getCurrentRoomIdFromUrl();
// Room should have a knock join rule
await waitForRoom(page, app.client, roomId, (room) => {
+1
View File
@@ -51,6 +51,7 @@ export async function doTokenRegistration(
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
return page.evaluate(() => ({
homeserverBaseUrl: window.mxMatrixClientPeg.get().getHomeserverUrl(),
accessToken: window.mxMatrixClientPeg.get().getAccessToken(),
userId: window.mxMatrixClientPeg.get().getUserId(),
deviceId: window.mxMatrixClientPeg.get().getDeviceId(),
+3 -13
View File
@@ -6,9 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type Config, CONFIG_JSON } from "@element-hq/element-web-playwright-common";
import { type Config } from "@element-hq/element-web-playwright-common";
import { type Browser, type Page } from "@playwright/test";
import { type StartedHomeserverContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers/HomeserverContainer";
import { routeConfigJson } from "@element-hq/element-web-playwright-common";
import { test, expect } from "../../element-web-test.ts";
import { logInAccountMas, registerAccountMas } from ".";
@@ -242,17 +243,6 @@ async function verifyUsingOtherDevice(deviceToVerifyPage: Page, alreadyVerifiedD
*/
async function newContext(browser: Browser, config: Partial<Partial<Config>>, homeserver: StartedHomeserverContainer) {
const otherContext = await browser.newContext();
await otherContext.route(`http://localhost:8080/config.json*`, async (route) => {
const json = {
...CONFIG_JSON,
...config,
default_server_config: {
"m.homeserver": {
base_url: homeserver.baseUrl,
},
},
};
await route.fulfill({ json });
});
await routeConfigJson(otherContext, homeserver.baseUrl, config);
return otherContext;
}
+29
View File
@@ -69,6 +69,20 @@ export class ElementAppPage {
return await this.page.evaluate(() => navigator.clipboard.readText());
}
/**
* Get the room ID from the current URL.
*
* @returns The room ID.
* @throws if the current URL does not contain a room ID.
*/
public async getCurrentRoomIdFromUrl(): Promise<string> {
const urlHash = await this.page.evaluate(() => window.location.hash);
if (!urlHash.startsWith("#/room/")) {
throw new Error("URL hash suggests we are not in a room");
}
return urlHash.replace("#/room/", "");
}
/**
* Opens the given room by name. The room must be visible in the
* room list and the room may contain unread messages.
@@ -197,6 +211,21 @@ export class ElementAppPage {
return memberlist;
}
/**
* Open the room info panel, and use it to send an invite to the given user.
*
* @param userId - The user to invite to the room.
*/
public async inviteUserToCurrentRoom(userId: string): Promise<void> {
await this.toggleRoomInfoPanel(); // TODO skip this if the room info panel is already open
await this.page.getByLabel("Right panel").getByRole("menuitem", { name: "Invite" }).click();
const input = this.page.getByRole("dialog").getByTestId("invite-dialog-input");
await input.fill(userId);
await input.press("Enter");
await this.page.getByRole("dialog").getByRole("button", { name: "Invite" }).click();
}
/**
* Get a locator for the tooltip associated with an element
* @param e The element with the tooltip
+2 -11
View File
@@ -6,7 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type ClientServerApi } from "@element-hq/element-web-playwright-common/lib/utils/api.js";
import { type ClientServerApi, type Credentials } from "@element-hq/element-web-playwright-common/lib/utils/api.js";
export { type Credentials } from "@element-hq/element-web-playwright-common/lib/utils/api.js";
export interface HomeserverInstance {
readonly baseUrl: string;
@@ -37,14 +38,4 @@ export interface HomeserverInstance {
setThreepid(userId: string, medium: string, address: string): Promise<void>;
}
export interface Credentials {
accessToken: string;
userId: string;
deviceId: string;
homeServer: string;
password: string | null; // null for password-less users
displayName?: string;
username: string; // the localpart of the userId
}
export type HomeserverType = "synapse" | "dendrite" | "pinecone";
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB