Migrate user-view.spec.ts from Cypress to Playwright (#11944)
* 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> * Update element-web-test.ts --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
2160f29473
commit
e897d7360d
8 changed files with 282 additions and 58 deletions
|
@ -1,55 +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 { HomeserverInstance } from "../../plugins/utils/homeserver";
|
|
||||||
import { MatrixClient } from "../../global";
|
|
||||||
|
|
||||||
describe("UserView", () => {
|
|
||||||
let homeserver: HomeserverInstance;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.startHomeserver("default").then((data) => {
|
|
||||||
homeserver = data;
|
|
||||||
|
|
||||||
cy.initTestUser(homeserver, "Violet");
|
|
||||||
cy.getBot(homeserver, { displayName: "Usman" }).as("bot");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
cy.stopHomeserver(homeserver);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render the user view as expected", () => {
|
|
||||||
cy.get<MatrixClient>("@bot").then((bot) => {
|
|
||||||
cy.visit(`/#/user/${bot.getUserId()}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get(".mx_RightPanel .mx_UserInfo_profile h2").within(() => {
|
|
||||||
cy.findByText("Usman").should("exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.findByText("1 session").should("be.visible");
|
|
||||||
|
|
||||||
cy.get(".mx_RightPanel").percySnapshotElement("User View", {
|
|
||||||
// Hide the MXID field as it'll vary on each test
|
|
||||||
percyCSS: ".mx_UserInfo_profile_mxid { visibility: hidden !important; }",
|
|
||||||
widths: [260, 500],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
4
playwright/.gitignore
vendored
4
playwright/.gitignore
vendored
|
@ -2,5 +2,5 @@
|
||||||
/html-report/
|
/html-report/
|
||||||
/synapselogs/
|
/synapselogs/
|
||||||
# Only commit snapshots from Linux
|
# Only commit snapshots from Linux
|
||||||
/snapshots/*/*.png
|
/snapshots/**/*.png
|
||||||
!/snapshots/*/*-linux.png
|
!/snapshots/**/*-linux.png
|
||||||
|
|
35
playwright/e2e/user-view/user-view.spec.ts
Normal file
35
playwright/e2e/user-view/user-view.spec.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
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 { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
|
test.describe("UserView", () => {
|
||||||
|
test.use({
|
||||||
|
displayName: "Violet",
|
||||||
|
botCreateOpts: { displayName: "Usman" },
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render the user view as expected", async ({ page, homeserver, user, bot }) => {
|
||||||
|
await page.goto(`/#/user/${bot.credentials.userId}`);
|
||||||
|
|
||||||
|
const rightPanel = page.getByRole("complementary");
|
||||||
|
await expect(rightPanel.getByRole("heading", { name: bot.credentials.displayName, exact: true })).toBeVisible();
|
||||||
|
await expect(rightPanel.getByText("1 session")).toBeVisible();
|
||||||
|
await expect(rightPanel).toHaveScreenshot("user-info.png", {
|
||||||
|
mask: [page.locator(".mx_BaseAvatar, .mx_UserInfo_profile_mxid")],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -28,6 +28,7 @@ import { ElementAppPage } from "./pages/ElementAppPage";
|
||||||
import { OAuthServer } from "./plugins/oauth_server";
|
import { OAuthServer } from "./plugins/oauth_server";
|
||||||
import { Crypto } from "./pages/crypto";
|
import { Crypto } from "./pages/crypto";
|
||||||
import { Toasts } from "./pages/toasts";
|
import { Toasts } from "./pages/toasts";
|
||||||
|
import { Bot, CreateBotOpts } from "./pages/bot";
|
||||||
|
|
||||||
const CONFIG_JSON: Partial<IConfigOptions> = {
|
const CONFIG_JSON: Partial<IConfigOptions> = {
|
||||||
// This is deliberately quite a minimal config.json, so that we can test that the default settings
|
// This is deliberately quite a minimal config.json, so that we can test that the default settings
|
||||||
|
@ -67,6 +68,8 @@ export const test = base.extend<
|
||||||
room?: { roomId: string };
|
room?: { roomId: string };
|
||||||
toasts: Toasts;
|
toasts: Toasts;
|
||||||
uut?: Locator; // Unit Under Test, useful place to refer a prepared locator
|
uut?: Locator; // Unit Under Test, useful place to refer a prepared locator
|
||||||
|
botCreateOpts: CreateBotOpts;
|
||||||
|
bot: Bot;
|
||||||
}
|
}
|
||||||
>({
|
>({
|
||||||
cryptoBackend: ["legacy", { option: true }],
|
cryptoBackend: ["legacy", { option: true }],
|
||||||
|
@ -173,6 +176,13 @@ export const test = base.extend<
|
||||||
toasts: async ({ page }, use) => {
|
toasts: async ({ page }, use) => {
|
||||||
await use(new Toasts(page));
|
await use(new Toasts(page));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
botCreateOpts: {},
|
||||||
|
bot: async ({ page, homeserver, botCreateOpts }, use) => {
|
||||||
|
const bot = new Bot(page, homeserver, botCreateOpts);
|
||||||
|
await bot.start();
|
||||||
|
await use(bot);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test.use({});
|
test.use({});
|
||||||
|
|
16
playwright/global.d.ts
vendored
16
playwright/global.d.ts
vendored
|
@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
import {
|
||||||
|
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";
|
||||||
|
|
||||||
|
@ -26,5 +32,13 @@ declare global {
|
||||||
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: {
|
||||||
|
MatrixClient: typeof MatrixClient;
|
||||||
|
MatrixScheduler: typeof MatrixScheduler;
|
||||||
|
MemoryStore: typeof MemoryStore;
|
||||||
|
MemoryCryptoStore: typeof MemoryCryptoStore;
|
||||||
|
createClient(opts: ICreateClientOpts | string);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
220
playwright/pages/bot.ts
Normal file
220
playwright/pages/bot.ts
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
/*
|
||||||
|
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 { uniqueId } from "lodash";
|
||||||
|
|
||||||
|
import type { MatrixClient, ISendEventResponse } from "matrix-js-sdk/src/matrix";
|
||||||
|
import type { AddSecretStorageKeyOpts } from "matrix-js-sdk/src/secret-storage";
|
||||||
|
import type { Credentials, HomeserverInstance } from "../plugins/homeserver";
|
||||||
|
|
||||||
|
export interface CreateBotOpts {
|
||||||
|
/**
|
||||||
|
* A prefix to use for the userid. If unspecified, "bot_" will be used.
|
||||||
|
*/
|
||||||
|
userIdPrefix?: string;
|
||||||
|
/**
|
||||||
|
* Whether the bot should automatically accept all invites.
|
||||||
|
*/
|
||||||
|
autoAcceptInvites?: boolean;
|
||||||
|
/**
|
||||||
|
* The display name to give to that bot user
|
||||||
|
*/
|
||||||
|
displayName?: string;
|
||||||
|
/**
|
||||||
|
* Whether to start the syncing client.
|
||||||
|
*/
|
||||||
|
startClient?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to generate cross-signing keys
|
||||||
|
*/
|
||||||
|
bootstrapCrossSigning?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to use the rust crypto impl. Defaults to false (for now!)
|
||||||
|
*/
|
||||||
|
rustCrypto?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to bootstrap the secret storage
|
||||||
|
*/
|
||||||
|
bootstrapSecretStorage?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultCreateBotOptions = {
|
||||||
|
userIdPrefix: "bot_",
|
||||||
|
autoAcceptInvites: true,
|
||||||
|
startClient: true,
|
||||||
|
bootstrapCrossSigning: true,
|
||||||
|
} satisfies CreateBotOpts;
|
||||||
|
|
||||||
|
export class Bot {
|
||||||
|
private client: JSHandle<MatrixClient>;
|
||||||
|
public credentials?: Credentials;
|
||||||
|
|
||||||
|
constructor(private page: Page, private homeserver: HomeserverInstance, private readonly opts: CreateBotOpts) {
|
||||||
|
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> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setupBotClient(): Promise<JSHandle<MatrixClient>> {
|
||||||
|
return this.page.evaluateHandle(
|
||||||
|
async ({ homeserver, credentials, opts }) => {
|
||||||
|
const keys = {};
|
||||||
|
|
||||||
|
const getCrossSigningKey = (type: string) => {
|
||||||
|
return keys[type];
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveCrossSigningKeys = (k: Record<string, Uint8Array>) => {
|
||||||
|
Object.assign(keys, k);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store the cached secret storage key and return it when `getSecretStorageKey` is called
|
||||||
|
let cachedKey: { keyId: string; key: Uint8Array };
|
||||||
|
const cacheSecretStorageKey = (keyId: string, keyInfo: AddSecretStorageKeyOpts, key: Uint8Array) => {
|
||||||
|
cachedKey = {
|
||||||
|
keyId,
|
||||||
|
key,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecretStorageKey = () =>
|
||||||
|
Promise.resolve<[string, Uint8Array]>([cachedKey.keyId, cachedKey.key]);
|
||||||
|
|
||||||
|
const cryptoCallbacks = {
|
||||||
|
getCrossSigningKey,
|
||||||
|
saveCrossSigningKeys,
|
||||||
|
cacheSecretStorageKey,
|
||||||
|
getSecretStorageKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cli = new window.matrixcs.MatrixClient({
|
||||||
|
baseUrl: homeserver.baseUrl,
|
||||||
|
userId: credentials.userId,
|
||||||
|
deviceId: credentials.deviceId,
|
||||||
|
accessToken: credentials.accessToken,
|
||||||
|
store: new window.matrixcs.MemoryStore(),
|
||||||
|
scheduler: new window.matrixcs.MatrixScheduler(),
|
||||||
|
cryptoStore: new window.matrixcs.MemoryCryptoStore(),
|
||||||
|
cryptoCallbacks,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.autoAcceptInvites) {
|
||||||
|
cli.on((window as any).matrixcs.RoomMemberEvent.Membership, (event, member) => {
|
||||||
|
if (member.membership === "invite" && member.userId === cli.getUserId()) {
|
||||||
|
cli.joinRoom(member.roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts.startClient) {
|
||||||
|
return cli;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.rustCrypto) {
|
||||||
|
await cli.initRustCrypto({ useIndexedDB: false });
|
||||||
|
} else {
|
||||||
|
await cli.initCrypto();
|
||||||
|
}
|
||||||
|
cli.setGlobalErrorOnUnknownDevices(false);
|
||||||
|
await cli.startClient();
|
||||||
|
|
||||||
|
if (opts.bootstrapCrossSigning) {
|
||||||
|
await cli.getCrypto()!.bootstrapCrossSigning({
|
||||||
|
authUploadDeviceSigningKeys: async (func) => {
|
||||||
|
await func({
|
||||||
|
type: "m.login.password",
|
||||||
|
identifier: {
|
||||||
|
type: "m.id.user",
|
||||||
|
user: credentials.userId,
|
||||||
|
},
|
||||||
|
password: credentials.password,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.bootstrapSecretStorage) {
|
||||||
|
const passphrase = "new passphrase";
|
||||||
|
const recoveryKey = await cli.getCrypto().createRecoveryKeyFromPassphrase(passphrase);
|
||||||
|
Object.assign(cli, { __playwright_recovery_key: recoveryKey });
|
||||||
|
|
||||||
|
await cli.getCrypto()!.bootstrapSecretStorage({
|
||||||
|
setupNewSecretStorage: true,
|
||||||
|
setupNewKeyBackup: true,
|
||||||
|
createSecretStorageKey: () => Promise.resolve(recoveryKey),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return cli;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
homeserver: this.homeserver.config,
|
||||||
|
credentials: this.credentials,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Loading…
Add table
Add a link
Reference in a new issue