Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix flaky playwright tests #28959

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/playwright.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ test.use({
```

The appropriate homeserver will be launched by the Playwright worker and reused for all tests which match the worker configuration.
Due to homeservers being reused between tests, please use unique names for any rooms put into the room directory as
they may be visible from other tests, the suggested approach is to use `testInfo.testId` within the name or lodash's uniqueId.
We remove public rooms from the room directory between tests but deleting users doesn't have a homeserver agnostic solution.
The logs from testcontainers will be attached to any reports output from Playwright.

## Writing Tests
Expand Down
53 changes: 33 additions & 20 deletions playwright/e2e/forgot-password/forgot-password.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,25 @@ 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 { expect, test } from "../../element-web-test";
import { expect, test as base } from "../../element-web-test";
import { selectHomeserver } from "../utils";
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { Credentials } from "../../plugins/homeserver";

const username = "user1234";
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
const password = "oETo7MPf0o";
const email = "user@nowhere.dummy";

const test = base.extend<{ credentials: Pick<Credentials, "username" | "password"> }>({
// eslint-disable-next-line no-empty-pattern
credentials: async ({}, use, testInfo) => {
await use({
username: `user_${testInfo.testId}`,
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
password: "oETo7MPf0o",
});
},
});

test.use(emailHomeserver);
test.use({
config: {
Expand Down Expand Up @@ -45,31 +54,35 @@ test.describe("Forgot Password", () => {
await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png");
});

test("renders email verification dialog properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
const user = await homeserver.registerUser(username, password);
test(
"renders email verification dialog properly",
{ tag: "@screenshot" },
async ({ page, homeserver, credentials }) => {
const user = await homeserver.registerUser(credentials.username, credentials.password);

await homeserver.setThreepid(user.userId, "email", email);
await homeserver.setThreepid(user.userId, "email", email);

await page.goto("/");
await page.goto("/");

await page.getByRole("link", { name: "Sign in" }).click();
await selectHomeserver(page, homeserver.baseUrl);
await page.getByRole("link", { name: "Sign in" }).click();
await selectHomeserver(page, homeserver.baseUrl);

await page.getByRole("button", { name: "Forgot password?" }).click();
await page.getByRole("button", { name: "Forgot password?" }).click();

await page.getByRole("textbox", { name: "Email address" }).fill(email);
await page.getByRole("textbox", { name: "Email address" }).fill(email);

await page.getByRole("button", { name: "Send email" }).click();
await page.getByRole("button", { name: "Send email" }).click();

await page.getByRole("button", { name: "Next" }).click();
await page.getByRole("button", { name: "Next" }).click();

await page.getByRole("textbox", { name: "New Password", exact: true }).fill(password);
await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(password);
await page.getByRole("textbox", { name: "New Password", exact: true }).fill(credentials.password);
await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(credentials.password);

await page.getByRole("button", { name: "Reset password" }).click();
await page.getByRole("button", { name: "Reset password" }).click();

await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport();
await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport();

await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png");
});
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png");
},
);
});
6 changes: 6 additions & 0 deletions playwright/e2e/login/login-consent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ async function login(page: Page, homeserver: HomeserverInstance, credentials: Cr
await page.getByRole("button", { name: "Sign in" }).click();
}

// This test suite uses the same userId for all tests in the suite
// due to DEVICE_SIGNING_KEYS_BODY being specific to that userId,
// so we restart the Synapse container to make it forget everything.
test.use(consentHomeserver);
test.use({
config: {
Expand All @@ -97,6 +100,9 @@ test.use({
...credentials,
displayName,
});

// Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts
await homeserver.restart();
},
});

Expand Down
2 changes: 1 addition & 1 deletion playwright/e2e/oidc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
await expect(async () => {
messages = await mailhog.messages();
expect(messages.items).toHaveLength(1);
}).toPass();

Check failure on line 34 in playwright/e2e/oidc/index.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 6/6

[Chrome] › crypto/backups-mas.spec.ts:31:9 › Encryption state after registration › user is prompted to set up recovery

1) [Chrome] › crypto/backups-mas.spec.ts:31:9 › Encryption state after registration › user is prompted to set up recovery Error: expect(received).toHaveLength(expected) Expected length: 1 Received length: 0 Received array: [] Call Log: - Test timeout of 30000ms exceeded at oidc/index.ts:34 32 | messages = await mailhog.messages(); 33 | expect(messages.items).toHaveLength(1); > 34 | }).toPass(); | ^ 35 | expect(messages.items[0].to).toEqual(`${username} <${email}>`); 36 | const [, code] = messages.items[0].text.match(/Your verification code to confirm this email address is: (\d{6})/); 37 | at registerAccountMas (/home/runner/work/element-web/element-web/playwright/e2e/oidc/index.ts:34:8) at /home/runner/work/element-web/element-web/playwright/e2e/crypto/backups-mas.spec.ts:34:9

Check failure on line 34 in playwright/e2e/oidc/index.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 6/6

[Chrome] › crypto/backups-mas.spec.ts:48:9 › Key backup reset from elsewhere › Key backup is disabled when reset from elsewhere

2) [Chrome] › crypto/backups-mas.spec.ts:48:9 › Key backup reset from elsewhere › Key backup is disabled when reset from elsewhere Error: expect(received).toHaveLength(expected) Expected length: 1 Received length: 0 Received array: [] Call Log: - Test timeout of 30000ms exceeded at oidc/index.ts:34 32 | messages = await mailhog.messages(); 33 | expect(messages.items).toHaveLength(1); > 34 | }).toPass(); | ^ 35 | expect(messages.items[0].to).toEqual(`${username} <${email}>`); 36 | const [, code] = messages.items[0].text.match(/Your verification code to confirm this email address is: (\d{6})/); 37 | at registerAccountMas (/home/runner/work/element-web/element-web/playwright/e2e/oidc/index.ts:34:8) at /home/runner/work/element-web/element-web/playwright/e2e/crypto/backups-mas.spec.ts:58:9
expect(messages.items[0].to).toEqual(`${username} <${email}>`);
const [code] = messages.items[0].text.match(/(\d{6})/);
const [, code] = messages.items[0].text.match(/Your verification code to confirm this email address is: (\d{6})/);

await page.getByRole("textbox", { name: "6-digit code" }).fill(code);
await page.getByRole("button", { name: "Continue" }).click();
Expand Down
17 changes: 14 additions & 3 deletions playwright/e2e/oidc/oidc-native.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,30 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.skip(isDendrite, "does not yet support MAS");
test.slow(); // trace recording takes a while here

test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhogClient, mas }) => {
test("can register the oauth2 client and an account", async ({
context,
page,
homeserver,
mailhogClient,
mas,
}, testInfo) => {
await page.clock.install();

const tokenUri = `${mas.baseUrl}/oauth2/token`;
const tokenApiPromise = page.waitForRequest(
(request) => request.url() === tokenUri && request.postDataJSON()["grant_type"] === "authorization_code",
);

await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!");

const userId = `alice_${testInfo.testId}`;
await registerAccountMas(page, mailhogClient, userId, "alice@email.com", "Pa$sW0rD!");

// Eventually, we should end up at the home screen.
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
await expect(page.getByRole("heading", { name: "Welcome alice", exact: true })).toBeVisible();
await expect(page.getByRole("heading", { name: `Welcome ${userId}`, exact: true })).toBeVisible();
await page.clock.runFor(20000); // run the timer so we see the token request

const tokenApiRequest = await tokenApiPromise;
expect(tokenApiRequest.postDataJSON()["grant_type"]).toBe("authorization_code");
Expand Down
4 changes: 2 additions & 2 deletions playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ const test = base.extend<{
test.describe("1:1 chat room", () => {
test.use({
displayName: "Jeff",
user2: async ({ homeserver }, use) => {
const credentials = await homeserver.registerUser("user1234", "p4s5W0rD", "Timmy");
user2: async ({ homeserver }, use, testInfo) => {
const credentials = await homeserver.registerUser(`user2_${testInfo.testId}`, "p4s5W0rD", "Timmy");
await use(credentials);
},
});
Expand Down
6 changes: 3 additions & 3 deletions playwright/e2e/share-dialog/share-dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ test.describe("Share dialog", () => {

const dialog = page.getByRole("dialog", { name: "Share room" });
await expect(dialog.getByText(`https://matrix.to/#/${room.roomId}`)).toBeVisible();
expect(dialog).toMatchScreenshot("share-dialog-room.png", {
await expect(dialog).toMatchScreenshot("share-dialog-room.png", {
// QRCode and url changes at every run
mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")],
});
Expand All @@ -40,7 +40,7 @@ test.describe("Share dialog", () => {

const dialog = page.getByRole("dialog", { name: "Share User" });
await expect(dialog.getByText(`https://matrix.to/#/${user.userId}`)).toBeVisible();
expect(dialog).toMatchScreenshot("share-dialog-user.png", {
await expect(dialog).toMatchScreenshot("share-dialog-user.png", {
// QRCode changes at every run
mask: [page.locator(".mx_QRCode")],
});
Expand All @@ -57,7 +57,7 @@ test.describe("Share dialog", () => {

const dialog = page.getByRole("dialog", { name: "Share Room Message" });
await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked();
expect(dialog).toMatchScreenshot("share-dialog-event.png", {
await expect(dialog).toMatchScreenshot("share-dialog-event.png", {
// QRCode and url changes at every run
mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")],
});
Expand Down
1 change: 0 additions & 1 deletion playwright/e2e/sliding-sync/sliding-sync.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*

Check failure on line 1 in playwright/e2e/sliding-sync/sliding-sync.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 4/6

[Chrome] › sliding-sync/sliding-sync.spec.ts:365:5 › Sliding Sync › should send unsubscribe_rooms for every room switch

2) [Chrome] › sliding-sync/sliding-sync.spec.ts:365:5 › Sliding Sync › should send unsubscribe_rooms for every room switch Test timeout of 30000ms exceeded.
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.

Expand Down Expand Up @@ -108,7 +108,6 @@
await page.getByRole("menuitemradio", { name: "A-Z" }).dispatchEvent("click");
await expect(page.locator(".mx_StyledRadioButton_checked").getByText("A-Z")).toBeVisible();

await page.pause();
await checkOrder(["Apple", "Orange", "Pineapple", "Test Room"], page);
});

Expand Down Expand Up @@ -397,7 +396,7 @@
// Switch to another room and wait for playwright to get the request
await Promise.all([
page.waitForRequest(matchRoomSubRequest(roomPId)),
page.waitForRequest(matchRoomUnsubRequest(roomAId)),

Check failure on line 399 in playwright/e2e/sliding-sync/sliding-sync.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 4/6

[Chrome] › sliding-sync/sliding-sync.spec.ts:365:5 › Sliding Sync › should send unsubscribe_rooms for every room switch

2) [Chrome] › sliding-sync/sliding-sync.spec.ts:365:5 › Sliding Sync › should send unsubscribe_rooms for every room switch Error: page.waitForRequest: Test timeout of 30000ms exceeded. 397 | await Promise.all([ 398 | page.waitForRequest(matchRoomSubRequest(roomPId)), > 399 | page.waitForRequest(matchRoomUnsubRequest(roomAId)), | ^ 400 | page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(), 401 | ]); 402 | at /home/runner/work/element-web/element-web/playwright/e2e/sliding-sync/sliding-sync.spec.ts:399:18
page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(),
]);

Expand Down
27 changes: 13 additions & 14 deletions playwright/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,33 @@ Please see LICENSE files in the repository root for full details.

import { test as base } from "@playwright/test";
import mailhog from "mailhog";
import { GenericContainer, Network, StartedNetwork, StartedTestContainer, Wait } from "testcontainers";
import { Network, StartedNetwork } from "testcontainers";
import { PostgreSqlContainer, StartedPostgreSqlContainer } from "@testcontainers/postgresql";

import { SynapseConfigOptions, SynapseContainer } from "./testcontainers/synapse.ts";
import { ContainerLogger } from "./testcontainers/utils.ts";
import { StartedMatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts";
import { HomeserverContainer, StartedHomeserverContainer } from "./testcontainers/HomeserverContainer.ts";
import { MailhogContainer, StartedMailhogContainer } from "./testcontainers/mailhog.ts";

interface TestFixtures {
mailhogClient: mailhog.API;
}

export interface Services {
logger: ContainerLogger;

network: StartedNetwork;
postgres: StartedPostgreSqlContainer;

mailhog: StartedTestContainer;
mailhogClient: mailhog.API;
mailhog: StartedMailhogContainer;

synapseConfigOptions: SynapseConfigOptions;
_homeserver: HomeserverContainer<any>;
homeserver: StartedHomeserverContainer;
mas?: StartedMatrixAuthenticationServiceContainer;
}

export const test = base.extend<{}, Services>({
export const test = base.extend<TestFixtures, Services>({
logger: [
// eslint-disable-next-line no-empty-pattern
async ({}, use) => {
Expand Down Expand Up @@ -79,24 +82,20 @@ export const test = base.extend<{}, Services>({

mailhog: [
async ({ logger, network }, use) => {
const container = await new GenericContainer("mailhog/mailhog:latest")
const container = await new MailhogContainer()
.withNetwork(network)
.withNetworkAliases("mailhog")
.withExposedPorts(8025)
.withLogConsumer(logger.getConsumer("mailhog"))
.withWaitStrategy(Wait.forListeningPorts())
.start();
await use(container);
await container.stop();
},
{ scope: "worker" },
],
mailhogClient: [
async ({ mailhog: container }, use) => {
await use(mailhog({ host: container.getHost(), port: container.getMappedPort(8025) }));
},
{ scope: "worker" },
],
mailhogClient: async ({ mailhog: container }, use) => {
await use(container.client);
await container.client.deleteAll();
},

synapseConfigOptions: [{}, { option: true, scope: "worker" }],
_homeserver: [
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions playwright/testcontainers/mailhog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
Copyright 2024 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 { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers";
import mailhog from "mailhog";

export class MailhogContainer extends GenericContainer {
constructor() {
super("mailhog/mailhog:latest");

this.withExposedPorts(8025).withWaitStrategy(Wait.forListeningPorts());
}

public override async start(): Promise<StartedMailhogContainer> {
return new StartedMailhogContainer(await super.start());
}
}

export class StartedMailhogContainer extends AbstractStartedContainer {
public readonly client: mailhog.API;

constructor(container: StartedTestContainer) {
super(container);
this.client = mailhog({ host: container.getHost(), port: container.getMappedPort(8025) });
}
}
Loading