Run Playwright tests on Firefox & "Safari" nightly (#28757)

* Run Playwright tests on Firefox

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update playwright.config.ts

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update end-to-end-tests.yaml

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Finalise

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Documentation

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* typo

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
pull/28776/head
Michael Telatynski 2024-12-19 23:42:09 +00:00 committed by GitHub
parent be181d2c79
commit 5448de5dd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 266 additions and 161 deletions

View File

@ -3,6 +3,9 @@
# as an artifact and run end-to-end tests. # as an artifact and run end-to-end tests.
name: End to End Tests name: End to End Tests
on: on:
# CRON to run all Projects at 6am UTC
schedule:
- cron: "0 6 * * *"
pull_request: {} pull_request: {}
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
@ -32,6 +35,8 @@ concurrency:
env: env:
# fetchdep.sh needs to know our PR number # fetchdep.sh needs to know our PR number
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
# Use 6 runners in the default case, but 4 when running on a schedule where we run all 5 projects (20 runners total)
NUM_RUNNERS: ${{ github.event_name == 'schedule' && 4 || 6 }}
permissions: {} # No permissions required permissions: {} # No permissions required
@ -40,6 +45,9 @@ jobs:
name: "Build Element-Web" name: "Build Element-Web"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
if: inputs.skip != true if: inputs.skip != true
outputs:
num-runners: ${{ env.NUM_RUNNERS }}
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -79,8 +87,17 @@ jobs:
path: webapp path: webapp
retention-days: 1 retention-days: 1
- name: Calculate runner variables
id: runner-vars
uses: actions/github-script@v7
with:
script: |
const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
const matrix = Array.from({ length: numRunners }, (_, i) => i + 1);
core.setOutput("matrix", JSON.stringify(matrix));
playwright: playwright:
name: "Run Tests ${{ matrix.runner }}/${{ strategy.job-total }}" name: "Run Tests [${{ matrix.project }}] ${{ matrix.runner }}/${{ needs.build.outputs.num-runners }}"
needs: build needs: build
if: inputs.skip != true if: inputs.skip != true
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@ -92,7 +109,19 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
# Run multiple instances in parallel to speed up the tests # Run multiple instances in parallel to speed up the tests
runner: [1, 2, 3, 4, 5, 6] runner: ${{ fromJSON(needs.build.outputs.runners-matrix) }}
project:
- Chrome
- Firefox
- WebKit
isCron:
- ${{ github.event_name == 'schedule' }}
# Skip the Firefox & Safari runs unless this was a cron trigger
exclude:
- isCron: false
project: Firefox
- isCron: false
project: WebKit
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -124,24 +153,30 @@ jobs:
with: with:
path: | path: |
~/.cache/ms-playwright ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }}-chromium key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }}
- name: Install Playwright browser - name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true' if: steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn playwright install --with-deps --no-shell chromium run: yarn playwright install --with-deps --no-shell
- name: Install system dependencies for WebKit
# Some WebKit dependencies seem to lay outside the cache and will need to be installed separately
if: matrix.project == 'WebKit' && steps.playwright-cache.outputs.cache-hit == 'true'
run: yarn playwright install-deps webkit
# We skip tests tagged with @mergequeue when running on PRs, but run them in MQ and everywhere else # We skip tests tagged with @mergequeue when running on PRs, but run them in MQ and everywhere else
- name: Run Playwright tests - name: Run Playwright tests
run: | run: |
yarn playwright test \ yarn playwright test \
--shard "${{ matrix.runner }}/${{ strategy.job-total }}" \ --shard "${{ matrix.runner }}/${{ needs.build.outputs.num-runners }}" \
--project="${{ matrix.project }}" \
${{ github.event_name == 'pull_request' && '--grep-invert @mergequeue' || '' }} ${{ github.event_name == 'pull_request' && '--grep-invert @mergequeue' || '' }}
- name: Upload blob report to GitHub Actions Artifacts - name: Upload blob report to GitHub Actions Artifacts
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: all-blob-reports-${{ matrix.runner }} name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }}
path: blob-report path: blob-report
retention-days: 1 retention-days: 1

View File

@ -53,15 +53,11 @@ yarn run test:playwright:open --headed --debug
See more command line options at <https://playwright.dev/docs/test-cli>. See more command line options at <https://playwright.dev/docs/test-cli>.
### Running with Rust cryptography ## Projects
`matrix-js-sdk` is currently in the By default, Playwright will run all "Projects", this means tests will run against Chrome, Firefox and "Safari" (Webkit).
[process](https://github.com/vector-im/element-web/issues/21972) of being We only run tests against Chrome in pull request CI, but all projects in the merge queue.
updated to replace its end-to-end encryption implementation to use the [Matrix Some tests are excluded from running on certain browsers due to incompatibilities in the test harness.
Rust SDK](https://github.com/matrix-org/matrix-rust-sdk). This is not currently
enabled by default, but it is possible to have Playwright configure Element to use
the Rust crypto implementation by passing `--project="Rust Crypto"` or using
the top left options in open mode.
## How the Tests Work ## How the Tests Work
@ -224,3 +220,14 @@ We use test tags to categorise tests for running subsets more efficiently.
- `@mergequeue`: Tests that are slow or flaky and cover areas of the app we update seldom, should not be run on every PR commit but will be run in the Merge Queue. - `@mergequeue`: Tests that are slow or flaky and cover areas of the app we update seldom, should not be run on every PR commit but will be run in the Merge Queue.
- `@screenshot`: Tests that use `toMatchScreenshot` to speed up a run of `test:playwright:screenshots`. A test with this tag must not also have the `@mergequeue` tag as this would cause false positives in the stale screenshot detection. - `@screenshot`: Tests that use `toMatchScreenshot` to speed up a run of `test:playwright:screenshots`. A test with this tag must not also have the `@mergequeue` tag as this would cause false positives in the stale screenshot detection.
- `@no-$project`: Tests which are unsupported in $Project. These tests will be skipped when running in $Project.
Anything testing Matrix media will need to have `@no-firefox` and `@no-webkit` as those rely on the service worker which
has to be disabled in Playwright on Firefox & Webkit to retain routing functionality.
Anything testing VoIP/microphone will need to have `@no-webkit` as fake microphone functionality is not available
there at this time.
## Colima
If you are running under Colima, you may need to set the environment variable `TMPDIR` to `/tmp/colima` or a path
within `$HOME` to allow bind mounting temporary directories into the Docker containers.

View File

@ -64,7 +64,7 @@
"test:playwright:open": "yarn test:playwright --ui", "test:playwright:open": "yarn test:playwright --ui",
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run", "test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run",
"test:playwright:screenshots:build": "docker build playwright -t element-web-playwright", "test:playwright:screenshots:build": "docker build playwright -t element-web-playwright",
"test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot", "test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot --project=Chrome",
"coverage": "yarn test --coverage", "coverage": "yarn test --coverage",
"analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts", "analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts",
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp", "analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",

View File

@ -11,16 +11,49 @@ import { defineConfig, devices } from "@playwright/test";
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080"; const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
export default defineConfig({ export default defineConfig({
projects: [{ name: "Chrome", use: { ...devices["Desktop Chrome"], channel: "chromium" } }], projects: [
{
name: "Chrome",
use: {
...devices["Desktop Chrome"],
channel: "chromium",
permissions: ["clipboard-write", "clipboard-read", "microphone"],
launchOptions: {
args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--mute-audio"],
},
},
},
{
name: "Firefox",
use: {
...devices["Desktop Firefox"],
launchOptions: {
firefoxUserPrefs: {
"permissions.default.microphone": 1,
},
},
// This is needed to work around an issue between Playwright routes, Firefox, and Service workers
// https://github.com/microsoft/playwright/issues/33561#issuecomment-2471642120
serviceWorkers: "block",
},
ignoreSnapshots: true,
},
{
name: "WebKit",
use: {
...devices["Desktop Safari"],
// Seemingly WebKit has the same issue as Firefox in Playwright routes not working
// https://playwright.dev/docs/network#missing-network-events-and-service-workers
serviceWorkers: "block",
},
ignoreSnapshots: true,
},
],
use: { use: {
viewport: { width: 1280, height: 720 }, viewport: { width: 1280, height: 720 },
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
video: "retain-on-failure", video: "retain-on-failure",
baseURL, baseURL,
permissions: ["clipboard-write", "clipboard-read", "microphone"],
launchOptions: {
args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--mute-audio"],
},
trace: "on-first-retry", trace: "on-first-retry",
}, },
webServer: { webServer: {

View File

@ -13,7 +13,7 @@ import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout"; import { Layout } from "../../../src/settings/enums/Layout";
import { ElementAppPage } from "../../pages/ElementAppPage"; import { ElementAppPage } from "../../pages/ElementAppPage";
test.describe("Audio player", () => { test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.use({ test.use({
displayName: "Hanako", displayName: "Hanako",
}); });

View File

@ -51,90 +51,94 @@ test.describe("Backups", () => {
displayName: "Hanako", displayName: "Hanako",
}); });
test("Create, delete and recreate a keys backup", async ({ page, user, app }, workerInfo) => { test(
// Create a backup "Create, delete and recreate a keys backup",
const securityTab = await app.settings.openUserSettings("Security & Privacy"); { tag: "@no-webkit" },
async ({ page, user, app }, workerInfo) => {
// Create a backup
const securityTab = await app.settings.openUserSettings("Security & Privacy");
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible(); await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
await securityTab.getByRole("button", { name: "Set up", exact: true }).click(); await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
const currentDialogLocator = page.locator(".mx_Dialog"); const currentDialogLocator = page.locator(".mx_Dialog");
// It's the first time and secure storage is not set up, so it will create one // It's the first time and secure storage is not set up, so it will create one
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click(); await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click(); await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
// copy the recovery key to use it later // copy the recovery key to use it later
const securityKey = await app.getClipboard(); const securityKey = await app.getClipboard();
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click(); await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click(); await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
// Open the settings again // Open the settings again
await app.settings.openUserSettings("Security & Privacy"); await app.settings.openUserSettings("Security & Privacy");
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible(); await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
// expand the advanced section to see the active version in the reports // expand the advanced section to see the active version in the reports
await page await page
.locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced") .locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
.locator("..") .locator("..")
.click(); .click();
await expectBackupVersionToBe(page, "1"); await expectBackupVersionToBe(page, "1");
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click(); await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
// Delete it // Delete it
await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup" await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
// Create another // Create another
await securityTab.getByRole("button", { name: "Set up", exact: true }).click(); await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
await currentDialogLocator.getByLabel("Security Key").fill(securityKey); await currentDialogLocator.getByLabel("Security Key").fill(securityKey);
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click(); await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
// Should be successful // Should be successful
await expect(currentDialogLocator.getByRole("heading", { name: "Success!" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Success!" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "OK", exact: true }).click(); await currentDialogLocator.getByRole("button", { name: "OK", exact: true }).click();
// Open the settings again // Open the settings again
await app.settings.openUserSettings("Security & Privacy"); await app.settings.openUserSettings("Security & Privacy");
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible(); await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
// expand the advanced section to see the active version in the reports // expand the advanced section to see the active version in the reports
await page await page
.locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced") .locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
.locator("..") .locator("..")
.click(); .click();
await expectBackupVersionToBe(page, "2"); await expectBackupVersionToBe(page, "2");
// == // ==
// Ensure that if you don't have the secret storage passphrase the backup won't be created // Ensure that if you don't have the secret storage passphrase the backup won't be created
// == // ==
// First delete version 2 // First delete version 2
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click(); await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
// Click "Delete Backup" // Click "Delete Backup"
await currentDialogLocator.getByTestId("dialog-primary-button").click(); await currentDialogLocator.getByTestId("dialog-primary-button").click();
// Try to create another // Try to create another
await securityTab.getByRole("button", { name: "Set up", exact: true }).click(); await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
// But cancel the security key dialog, to simulate not having the secret storage passphrase // But cancel the security key dialog, to simulate not having the secret storage passphrase
await currentDialogLocator.getByTestId("dialog-cancel-button").click(); await currentDialogLocator.getByTestId("dialog-cancel-button").click();
await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible();
// check that it failed // check that it failed
await expect(currentDialogLocator.getByText("Unable to create key backup")).toBeVisible(); await expect(currentDialogLocator.getByText("Unable to create key backup")).toBeVisible();
// cancel // cancel
await currentDialogLocator.getByTestId("dialog-cancel-button").click(); await currentDialogLocator.getByTestId("dialog-cancel-button").click();
// go back to the settings to check that no backup was created (the setup button should still be there) // go back to the settings to check that no backup was created (the setup button should still be there)
await app.settings.openUserSettings("Security & Privacy"); await app.settings.openUserSettings("Security & Privacy");
await expect(securityTab.getByRole("button", { name: "Set up", exact: true })).toBeVisible(); await expect(securityTab.getByRole("button", { name: "Set up", exact: true })).toBeVisible();
}); },
);
}); });

View File

@ -21,7 +21,7 @@ import {
} from "./utils"; } from "./utils";
import { Bot } from "../../pages/bot"; import { Bot } from "../../pages/bot";
test.describe("Device verification", () => { test.describe("Device verification", { tag: "@no-webkit" }, () => {
let aliceBotClient: Bot; let aliceBotClient: Bot;
/** The backup version that was set up by the bot client. */ /** The backup version that was set up by the bot client. */

View File

@ -25,7 +25,7 @@ const test = base.extend<Fixtures>({
}, },
}); });
test.describe("migration", function () { test.describe("migration", { tag: "@no-webkit" }, function () {
test.use({ displayName: "Alice" }); test.use({ displayName: "Alice" });
test("Should support migration from legacy crypto", async ({ context, user, page }, workerInfo) => { test("Should support migration from legacy crypto", async ({ context, user, page }, workerInfo) => {

View File

@ -10,7 +10,8 @@ import { Locator, Page } from "@playwright/test";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
test.describe("Location sharing", () => { // Firefox headless lacks WebGL support https://bugzilla.mozilla.org/show_bug.cgi?id=1375585
test.describe("Location sharing", { tag: "@no-firefox" }, () => {
const selectLocationShareTypeOption = (page: Page, shareType: string): Locator => { const selectLocationShareTypeOption = (page: Page, shareType: string): Locator => {
return page.getByTestId(`share-location-option-${shareType}`); return page.getByTestId(`share-location-option-${shareType}`);
}; };

View File

@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import { test, expect, registerAccountMas } from "."; import { test, expect, registerAccountMas } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("OIDC Aware", () => { test.describe("OIDC Aware", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.skip(isDendrite, "does not yet support MAS"); test.skip(isDendrite, "does not yet support MAS");
test.slow(); // trace recording takes a while here test.slow(); // trace recording takes a while here

View File

@ -10,7 +10,7 @@ import { test, expect, registerAccountMas } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";
import { ElementAppPage } from "../../pages/ElementAppPage.ts"; import { ElementAppPage } from "../../pages/ElementAppPage.ts";
test.describe("OIDC Native", () => { test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.skip(isDendrite, "does not yet support MAS"); test.skip(isDendrite, "does not yet support MAS");
test.slow(); // trace recording takes a while here test.slow(); // trace recording takes a while here

View File

@ -9,7 +9,9 @@ Please see LICENSE files in the repository root for full details.
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
test.describe("Registration", () => { test.describe("Registration", () => {
test.use({ startHomeserverOpts: "consent" }); test.use({
startHomeserverOpts: "consent",
});
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.goto("/#/register"); await page.goto("/#/register");

View File

@ -39,7 +39,7 @@ test.describe("FilePanel", () => {
await expect(page.locator(".mx_FilePanel")).toBeVisible(); await expect(page.locator(".mx_FilePanel")).toBeVisible();
}); });
test.describe("render", () => { test.describe("render", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test("should render empty state", { tag: "@screenshot" }, async ({ page }) => { test("should render empty state", { tag: "@screenshot" }, async ({ page }) => {
// Wait until the information about the empty state is rendered // Wait until the information about the empty state is rendered
await expect(page.locator(".mx_EmptyState")).toBeVisible(); await expect(page.locator(".mx_EmptyState")).toBeVisible();

View File

@ -15,37 +15,43 @@ test.describe("Room Directory", () => {
botCreateOpts: { displayName: "Paul" }, botCreateOpts: { displayName: "Paul" },
}); });
test("should allow admin to add alias & publish room to directory", async ({ page, app, user, bot }) => { test(
const roomId = await app.client.createRoom({ "should allow admin to add alias & publish room to directory",
name: "Gaming", { tag: "@no-webkit" },
preset: "public_chat" as Preset, async ({ page, app, user, bot }) => {
}); const roomId = await app.client.createRoom({
name: "Gaming",
preset: "public_chat" as Preset,
});
await app.viewRoomByName("Gaming"); await app.viewRoomByName("Gaming");
await app.settings.openRoomSettings(); await app.settings.openRoomSettings();
// First add a local address `gaming` // First add a local address `gaming`
const localAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Local Addresses" }); const localAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Local Addresses" });
await localAddresses.getByRole("textbox").fill("gaming"); await localAddresses.getByRole("textbox").fill("gaming");
await localAddresses.getByRole("button", { name: "Add" }).click(); await localAddresses.getByRole("button", { name: "Add" }).click();
await expect(localAddresses.getByText("#gaming:localhost")).toHaveClass("mx_EditableItem_item"); await expect(localAddresses.getByText("#gaming:localhost")).toHaveClass("mx_EditableItem_item");
// Publish into the public rooms directory // Publish into the public rooms directory
const publishedAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Published Addresses" }); const publishedAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Published Addresses" });
await expect(publishedAddresses.locator("#canonicalAlias")).toHaveValue("#gaming:localhost"); await expect(publishedAddresses.locator("#canonicalAlias")).toHaveValue("#gaming:localhost");
const checkbox = publishedAddresses const checkbox = publishedAddresses
.locator(".mx_SettingsFlag", { hasText: "Publish this room to the public in localhost's room directory?" }) .locator(".mx_SettingsFlag", {
.getByRole("switch"); hasText: "Publish this room to the public in localhost's room directory?",
await checkbox.check(); })
await expect(checkbox).toBeChecked(); .getByRole("switch");
await checkbox.check();
await expect(checkbox).toBeChecked();
await app.closeDialog(); await app.closeDialog();
const resp = await bot.publicRooms({}); const resp = await bot.publicRooms({});
expect(resp.total_room_count_estimate).toEqual(1); expect(resp.total_room_count_estimate).toEqual(1);
expect(resp.chunk).toHaveLength(1); expect(resp.chunk).toHaveLength(1);
expect(resp.chunk[0].room_id).toEqual(roomId); expect(resp.chunk[0].room_id).toEqual(roomId);
}); },
);
test( test(
"should allow finding published rooms in directory", "should allow finding published rooms in directory",

View File

@ -36,7 +36,7 @@ test.describe("General room settings tab", () => {
await expect(settings.getByText("Show more")).toBeVisible(); await expect(settings.getByText("Show more")).toBeVisible();
}); });
test("long address should not cause dialog to overflow", async ({ page, app }) => { test("long address should not cause dialog to overflow", { tag: "@no-webkit" }, async ({ page, app }) => {
const settings = await app.settings.openRoomSettings("General"); const settings = await app.settings.openRoomSettings("General");
// 1. Set the room-address to be a really long string // 1. Set the room-address to be a really long string
const longString = "abcasdhjasjhdaj1jh1asdhasjdhajsdhjavhjksd".repeat(4); const longString = "abcasdhjasjhdaj1jh1asdhasjdhajsdhjavhjksd".repeat(4);

View File

@ -31,7 +31,7 @@ test.describe("Preferences user settings tab", () => {
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png"); await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png");
}); });
test("should be able to change the app language", async ({ uut, user }) => { test("should be able to change the app language", { tag: ["@no-firefox", "@no-webkit"] }, async ({ uut, user }) => {
// Check language and region setting dropdown // Check language and region setting dropdown
const languageInput = uut.getByRole("button", { name: "Language Dropdown" }); const languageInput = uut.getByRole("button", { name: "Language Dropdown" });
await languageInput.scrollIntoViewIfNeeded(); await languageInput.scrollIntoViewIfNeeded();

View File

@ -55,38 +55,44 @@ test.describe("Spaces", () => {
botCreateOpts: { displayName: "BotBob" }, botCreateOpts: { displayName: "BotBob" },
}); });
test("should allow user to create public space", { tag: "@screenshot" }, async ({ page, app, user }) => { test(
const contextMenu = await openSpaceCreateMenu(page); "should allow user to create public space",
await expect(contextMenu).toMatchScreenshot("space-create-menu.png"); { tag: ["@screenshot", "@no-webkit"] },
async ({ page, app, user }) => {
const contextMenu = await openSpaceCreateMenu(page);
await expect(contextMenu).toMatchScreenshot("space-create-menu.png");
await contextMenu.getByRole("button", { name: /Public/ }).click(); await contextMenu.getByRole("button", { name: /Public/ }).click();
await contextMenu await contextMenu
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]') .locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
.setInputFiles("playwright/sample-files/riot.png"); .setInputFiles("playwright/sample-files/riot.png");
await contextMenu.getByRole("textbox", { name: "Name" }).fill("Let's have a Riot"); await contextMenu.getByRole("textbox", { name: "Name" }).fill("Let's have a Riot");
await expect(contextMenu.getByRole("textbox", { name: "Address" })).toHaveValue("lets-have-a-riot"); await expect(contextMenu.getByRole("textbox", { name: "Address" })).toHaveValue("lets-have-a-riot");
await contextMenu.getByRole("textbox", { name: "Description" }).fill("This is a space to reminisce Riot.im!"); await contextMenu
await contextMenu.getByRole("button", { name: "Create" }).click(); .getByRole("textbox", { name: "Description" })
.fill("This is a space to reminisce Riot.im!");
await contextMenu.getByRole("button", { name: "Create" }).click();
// Create the default General & Random rooms, as well as a custom "Jokes" room // Create the default General & Random rooms, as well as a custom "Jokes" room
await expect(page.getByPlaceholder("General")).toBeVisible(); await expect(page.getByPlaceholder("General")).toBeVisible();
await expect(page.getByPlaceholder("Random")).toBeVisible(); await expect(page.getByPlaceholder("Random")).toBeVisible();
await page.getByPlaceholder("Support").fill("Jokes"); await page.getByPlaceholder("Support").fill("Jokes");
await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click();
// Copy matrix.to link // Copy matrix.to link
await page.getByRole("button", { name: "Share invite link" }).click(); await page.getByRole("button", { name: "Share invite link" }).click();
expect(await app.getClipboardText()).toEqual("https://matrix.to/#/#lets-have-a-riot:localhost"); expect(await app.getClipboardText()).toEqual("https://matrix.to/#/#lets-have-a-riot:localhost");
// Go to space home // Go to space home
await page.getByRole("button", { name: "Go to my first room" }).click(); await page.getByRole("button", { name: "Go to my first room" }).click();
// Assert rooms exist in the room list // Assert rooms exist in the room list
await expect(page.getByRole("treeitem", { name: "General" })).toBeVisible(); await expect(page.getByRole("treeitem", { name: "General" })).toBeVisible();
await expect(page.getByRole("treeitem", { name: "Random" })).toBeVisible(); await expect(page.getByRole("treeitem", { name: "Random" })).toBeVisible();
await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible(); await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible();
}); },
);
test("should allow user to create private space", { tag: "@screenshot" }, async ({ page, app, user }) => { test("should allow user to create private space", { tag: "@screenshot" }, async ({ page, app, user }) => {
const menu = await openSpaceCreateMenu(page); const menu = await openSpaceCreateMenu(page);
@ -157,7 +163,7 @@ test.describe("Spaces", () => {
).toBeVisible(); ).toBeVisible();
}); });
test("should allow user to invite another to a space", async ({ page, app, user, bot }) => { test("should allow user to invite another to a space", { tag: "@no-webkit" }, async ({ page, app, user, bot }) => {
await app.client.createSpace({ await app.client.createSpace({
visibility: "public" as any, visibility: "public" as any,
room_alias_name: "space", room_alias_name: "space",

View File

@ -9,7 +9,7 @@
import { expect, test } from "."; import { expect, test } from ".";
import { CommandOrControl } from "../../utils"; import { CommandOrControl } from "../../utils";
test.describe("Threads Activity Centre", () => { test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
test.use({ test.use({
displayName: "Alice", displayName: "Alice",
botCreateOpts: { displayName: "Other User" }, botCreateOpts: { displayName: "Other User" },

View File

@ -324,7 +324,7 @@ test.describe("Threads", () => {
}); });
}); });
test("can send voice messages", async ({ page, app, user }) => { test("can send voice messages", { tag: ["@no-firefox", "@no-webkit"] }, async ({ page, app, user }) => {
// Increase right-panel size, so that voice messages fit // Increase right-panel size, so that voice messages fit
await page.evaluate(() => { await page.evaluate(() => {
window.localStorage.setItem("mx_rhs_size", "600"); window.localStorage.setItem("mx_rhs_size", "600");
@ -353,7 +353,7 @@ test.describe("Threads", () => {
test( test(
"should send location and reply to the location on ThreadView", "should send location and reply to the location on ThreadView",
{ tag: "@screenshot" }, { tag: ["@screenshot", "@no-firefox"] },
async ({ page, app, bot }) => { async ({ page, app, bot }) => {
const roomId = await app.client.createRoom({}); const roomId = await app.client.createRoom({});
await app.client.inviteUser(roomId, bot.credentials.userId); await app.client.inviteUser(roomId, bot.credentials.userId);

View File

@ -90,7 +90,7 @@ test.describe("Timeline", () => {
let oldAvatarUrl: string; let oldAvatarUrl: string;
let newAvatarUrl: string; let newAvatarUrl: string;
test.describe("useOnlyCurrentProfiles", () => { test.describe("useOnlyCurrentProfiles", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.beforeEach(async ({ app, user }) => { test.beforeEach(async ({ app, user }) => {
({ content_uri: oldAvatarUrl } = await app.client.uploadContent(OLD_AVATAR, { type: "image/png" })); ({ content_uri: oldAvatarUrl } = await app.client.uploadContent(OLD_AVATAR, { type: "image/png" }));
await app.client.setAvatarUrl(oldAvatarUrl); await app.client.setAvatarUrl(oldAvatarUrl);
@ -876,7 +876,7 @@ test.describe("Timeline", () => {
}); });
}); });
test.describe("message sending", () => { test.describe("message sending", { tag: ["@no-firefox", "@no-webkit"] }, () => {
const MESSAGE = "Hello world"; const MESSAGE = "Hello world";
const reply = "Reply"; const reply = "Reply";
const viewRoomSendMessageAndSetupReply = async (page: Page, app: ElementAppPage, roomId: string) => { const viewRoomSendMessageAndSetupReply = async (page: Page, app: ElementAppPage, roomId: string) => {
@ -914,7 +914,6 @@ test.describe("Timeline", () => {
}); });
test("can reply with a voice message", async ({ page, app, room, context }) => { test("can reply with a voice message", async ({ page, app, room, context }) => {
await context.grantPermissions(["microphone"]);
await viewRoomSendMessageAndSetupReply(page, app, room.roomId); await viewRoomSendMessageAndSetupReply(page, app, room.roomId);
const composerOptions = await app.openMessageComposerOptions(); const composerOptions = await app.openMessageComposerOptions();

View File

@ -128,7 +128,7 @@ async function setWidgetAccountData(
}); });
} }
test.describe("Stickers", () => { test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.use({ test.use({
displayName: "Sally", displayName: "Sally",
room: async ({ app }, use) => { room: async ({ app }, use) => {

View File

@ -127,6 +127,14 @@ export interface Fixtures {
} }
export const test = base.extend<Fixtures>({ export const test = base.extend<Fixtures>({
context: async ({ context }, use, testInfo) => {
// We skip tests instead of using grep-invert to still surface the counts in the html report
test.skip(
testInfo.tags.includes(`@no-${testInfo.project.name.toLowerCase()}`),
`Test does not work on ${testInfo.project.name}`,
);
await use(context);
},
config: CONFIG_JSON, config: CONFIG_JSON,
page: async ({ context, page, config, labsFlags }, use) => { page: async ({ context, page, config, labsFlags }, use) => {
await context.route(`http://localhost:8080/config.json*`, async (route) => { await context.route(`http://localhost:8080/config.json*`, async (route) => {

View File

@ -140,8 +140,12 @@ export class Docker {
* Detects whether the docker command is actually podman. * Detects whether the docker command is actually podman.
* To do this, it looks for "podman" in the output of "docker --help". * To do this, it looks for "podman" in the output of "docker --help".
*/ */
static _isPodman?: boolean;
static async isPodman(): Promise<boolean> { static async isPodman(): Promise<boolean> {
const { stdout } = await exec("docker", ["--help"], true); if (Docker._isPodman === undefined) {
return stdout.toLowerCase().includes("podman"); const { stdout } = await exec("docker", ["--help"], true);
Docker._isPodman = stdout.toLowerCase().includes("podman");
}
return Docker._isPodman;
} }
} }