feat: show call participants in room list (Discord-style)
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Suguru Hirahara
|
||||
|
||||
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 { type Route } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { getSampleFilePath } from "../../sample-files";
|
||||
|
||||
const USER_NAME = "Bob";
|
||||
const USER_NAME_NEW = "Alice";
|
||||
const EXTERNAL_ACCOUNT_MANAGEMENT_URL = "https://just.for.test.io/";
|
||||
|
||||
test.describe("Account user settings tab", () => {
|
||||
test.use({
|
||||
displayName: USER_NAME,
|
||||
config: {
|
||||
default_country_code: "US", // For checking the international country calling code
|
||||
},
|
||||
uut: async ({ app, user }, use) => {
|
||||
const locator = await app.settings.openUserSettings("Account");
|
||||
await use(locator);
|
||||
},
|
||||
});
|
||||
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ uut, user, axe }) => {
|
||||
await expect(uut).toMatchScreenshot("account.png");
|
||||
|
||||
// Assert that the top heading is rendered
|
||||
await expect(uut.getByRole("heading", { name: "Account", exact: true })).toBeVisible();
|
||||
|
||||
const profile = uut.locator(".mx_UserProfileSettings_profile");
|
||||
await profile.scrollIntoViewIfNeeded();
|
||||
await expect(profile.getByRole("textbox", { name: "Display Name" })).toHaveValue(USER_NAME);
|
||||
|
||||
// Assert that a userId is rendered
|
||||
await expect(uut.getByLabel("Username")).toHaveText(user.userId);
|
||||
|
||||
// Wait until spinners disappear
|
||||
await expect(uut.getByTestId("accountSection").locator(".mx_Spinner")).not.toBeVisible();
|
||||
await expect(uut.getByTestId("discoverySection").locator(".mx_Spinner")).not.toBeVisible();
|
||||
|
||||
const accountSection = uut.getByTestId("accountSection");
|
||||
await accountSection.scrollIntoViewIfNeeded();
|
||||
// Assert that input areas for changing a password exists
|
||||
await expect(accountSection.getByLabel("Current password")).toBeVisible();
|
||||
await expect(accountSection.getByLabel("New Password")).toBeVisible();
|
||||
await expect(accountSection.getByLabel("Confirm password")).toBeVisible();
|
||||
|
||||
// Check email addresses area
|
||||
const emailAddresses = uut.getByTestId("mx_AccountEmailAddresses");
|
||||
await emailAddresses.scrollIntoViewIfNeeded();
|
||||
// Assert that an input area for a new email address is rendered
|
||||
await expect(emailAddresses.getByRole("textbox", { name: "Email Address" })).toBeVisible();
|
||||
// Assert the add button is visible
|
||||
await expect(emailAddresses.getByRole("button", { name: "Add" })).toBeVisible();
|
||||
|
||||
// Check phone numbers area
|
||||
const phoneNumbers = uut.getByTestId("mx_AccountPhoneNumbers");
|
||||
await phoneNumbers.scrollIntoViewIfNeeded();
|
||||
// Assert that an input area for a new phone number is rendered
|
||||
await expect(phoneNumbers.getByRole("textbox", { name: "Phone Number" })).toBeVisible();
|
||||
// Assert that the add button is rendered
|
||||
await expect(phoneNumbers.getByRole("button", { name: "Add" })).toBeVisible();
|
||||
|
||||
// Assert the account deactivation button is displayed
|
||||
const accountManagementSection = uut.getByTestId("account-management-section");
|
||||
await accountManagementSection.scrollIntoViewIfNeeded();
|
||||
await expect(accountManagementSection.getByRole("button", { name: "Deactivate Account" })).toHaveClass(
|
||||
/mx_AccessibleButton_kind_danger/,
|
||||
);
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page, uut }) => {
|
||||
await page.setViewportSize({ width: 700, height: 600 });
|
||||
await expect(uut).toMatchScreenshot("account-smallscreen.png");
|
||||
});
|
||||
|
||||
test.describe("with external account management", () => {
|
||||
test.use({
|
||||
page: async ({ page }, runFixture) => {
|
||||
const authMetadataHandler = async (route: Route): Promise<void> => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
issuer: EXTERNAL_ACCOUNT_MANAGEMENT_URL,
|
||||
authorization_endpoint: `${EXTERNAL_ACCOUNT_MANAGEMENT_URL}authorize`,
|
||||
token_endpoint: `${EXTERNAL_ACCOUNT_MANAGEMENT_URL}token`,
|
||||
revocation_endpoint: `${EXTERNAL_ACCOUNT_MANAGEMENT_URL}revoke`,
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code"],
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
account_management_uri: EXTERNAL_ACCOUNT_MANAGEMENT_URL,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
await page.route("**/_matrix/client/v1/auth_metadata", authMetadataHandler);
|
||||
await page.route("**/_matrix/client/unstable/org.matrix.msc2965/auth_metadata", authMetadataHandler);
|
||||
await runFixture(page);
|
||||
},
|
||||
});
|
||||
|
||||
test("should render the manage account button properly", { tag: "@screenshot" }, async ({ uut, axe }) => {
|
||||
const manageAccountButton = uut.getByTestId("external-account-management-link");
|
||||
|
||||
await expect(manageAccountButton).toBeVisible();
|
||||
await expect(manageAccountButton).toHaveAttribute("href", EXTERNAL_ACCOUNT_MANAGEMENT_URL);
|
||||
await expect(manageAccountButton).toHaveAttribute("target", "_blank");
|
||||
await expect(manageAccountButton).toHaveText(/Manage account/);
|
||||
|
||||
const profileButtons = uut.locator(".mx_UserProfileSettings_profile_buttons");
|
||||
await profileButtons.scrollIntoViewIfNeeded();
|
||||
await expect(profileButtons).toMatchScreenshot("account-manage-account-button.png");
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
|
||||
test("should show tooltips on narrow screen", async ({ page, uut }) => {
|
||||
await page.setViewportSize({ width: 700, height: 600 });
|
||||
await page.getByRole("tab", { name: "Account" }).hover();
|
||||
await expect(page.getByRole("tooltip")).toHaveText("Account");
|
||||
});
|
||||
|
||||
test("should support adding and removing a profile picture", async ({ uut, page }) => {
|
||||
const profileSettings = uut.locator(".mx_UserProfileSettings");
|
||||
// Upload a picture
|
||||
await profileSettings.getByAltText("Upload").setInputFiles(getSampleFilePath("riot.png"));
|
||||
|
||||
// Image should be visible
|
||||
await expect(profileSettings.locator(".mx_AvatarSetting_avatar img")).toBeVisible();
|
||||
|
||||
// Open the menu & click remove
|
||||
await profileSettings.getByRole("button", { name: "Profile Picture" }).click();
|
||||
await page.getByRole("menuitem", { name: "Remove" }).click();
|
||||
|
||||
// Assert that the image disappeared
|
||||
await expect(profileSettings.locator(".mx_AvatarSetting_avatar img")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should set a country calling code based on default_country_code", async ({ uut }) => {
|
||||
// Check phone numbers area
|
||||
const accountPhoneNumbers = uut.getByTestId("mx_AccountPhoneNumbers");
|
||||
await accountPhoneNumbers.scrollIntoViewIfNeeded();
|
||||
// Assert that an input area for a new phone number is rendered
|
||||
await expect(accountPhoneNumbers.getByRole("textbox", { name: "Phone Number" })).toBeVisible();
|
||||
|
||||
// Check a new phone number dropdown menu
|
||||
const dropdown = accountPhoneNumbers.locator(".mx_PhoneNumbers_country");
|
||||
await dropdown.scrollIntoViewIfNeeded();
|
||||
// Assert that the country calling code of the United States is visible
|
||||
await expect(dropdown.getByText(/\+1/)).toBeVisible();
|
||||
|
||||
// Click the button to display the dropdown menu
|
||||
await dropdown.getByRole("button", { name: "Country Dropdown" }).click();
|
||||
|
||||
// Assert that the option for calling code of the United Kingdom is visible
|
||||
await expect(dropdown.getByRole("option", { name: /United Kingdom/ })).toBeVisible();
|
||||
|
||||
// Click again to close the dropdown
|
||||
await dropdown.getByRole("button", { name: "Country Dropdown" }).click();
|
||||
|
||||
// Assert that the default value is rendered again
|
||||
await expect(dropdown.getByText(/\+1/)).toBeVisible();
|
||||
|
||||
await expect(accountPhoneNumbers.getByRole("button", { name: "Add" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should support changing a display name", async ({ uut, page, app, user }) => {
|
||||
// Change the diaplay name to USER_NAME_NEW
|
||||
const displayNameInput = uut
|
||||
.locator(".mx_SettingsTab .mx_UserProfileSettings")
|
||||
.getByRole("textbox", { name: "Display Name" });
|
||||
await displayNameInput.fill(USER_NAME_NEW);
|
||||
await displayNameInput.press("Enter");
|
||||
|
||||
await app.closeDialog();
|
||||
|
||||
// Assert the avatar's initial characters are set
|
||||
const menu = await app.openUserMenu();
|
||||
await expect(menu.getByRole("img", { name: user.userId }).getByText("A")).toBeVisible(); // Alice
|
||||
await expect(page.locator(".mx_RoomView_wrapper .mx_BaseAvatar").getByText("A")).toBeVisible(); // Alice
|
||||
});
|
||||
|
||||
// ported to a playwright test because the jest test was very flakey for no obvious reason
|
||||
test("should display an error if the code is incorrect when adding a phone number", async ({ uut, page }) => {
|
||||
const dummyUrl = "https://nowhere.dummy/_matrix/client/unstable/add_threepid/msisdn/submit_token";
|
||||
|
||||
await page.route(
|
||||
`**/_matrix/client/v3/account/3pid/msisdn/requestToken`,
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
success: true,
|
||||
sid: "1",
|
||||
msisdn: "447700900000",
|
||||
intl_fmt: "+44 7700 900000",
|
||||
submit_url: dummyUrl,
|
||||
},
|
||||
});
|
||||
},
|
||||
{ times: 1 },
|
||||
);
|
||||
|
||||
await page.route(
|
||||
dummyUrl,
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
status: 400,
|
||||
json: {
|
||||
errcode: "M_THREEPID_AUTH_FAILED",
|
||||
error: "That code is definitely wrong",
|
||||
},
|
||||
});
|
||||
},
|
||||
{ times: 1 },
|
||||
);
|
||||
|
||||
const phoneSection = page.getByTestId("mx_AccountPhoneNumbers");
|
||||
await phoneSection.getByRole("textbox", { name: "Phone Number" }).fill("07700900000");
|
||||
await phoneSection.getByRole("button", { name: "Add" }).click();
|
||||
|
||||
await phoneSection
|
||||
.getByRole("textbox", { name: "Verification code" })
|
||||
.fill("A small eurasian field mouse dancing the paso doble");
|
||||
|
||||
await phoneSection.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Unable to verify phone number." })).toBeVisible();
|
||||
});
|
||||
});
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2023 Suguru Hirahara
|
||||
|
||||
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 { expect, test } from ".";
|
||||
|
||||
test.describe("Appearance user settings tab", () => {
|
||||
test.use({
|
||||
displayName: "Hanako",
|
||||
});
|
||||
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, user, app, axe }) => {
|
||||
await app.closeVerifyToast();
|
||||
const tab = await app.settings.openUserSettings("Appearance");
|
||||
|
||||
// Click "Show advanced" link button
|
||||
await tab.getByRole("button", { name: "Show advanced" }).click();
|
||||
|
||||
// Assert that "Hide advanced" link button is rendered
|
||||
await expect(tab.getByRole("button", { name: "Hide advanced" })).toBeVisible();
|
||||
|
||||
await expect(tab).toMatchScreenshot("appearance-tab.png");
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test(
|
||||
"should support changing font size by using the font size dropdown",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
await app.closeVerifyToast();
|
||||
await app.settings.openUserSettings("Appearance");
|
||||
|
||||
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
|
||||
const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown");
|
||||
await expect(fontDropdown.getByLabel("Font size")).toBeVisible();
|
||||
|
||||
// Default browser font size is 16px and the select value is 0
|
||||
// -4 value is 12px
|
||||
await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" });
|
||||
|
||||
await expect(page).toMatchScreenshot("window-12px.png", { includeDialogBackground: true });
|
||||
},
|
||||
);
|
||||
|
||||
test("should support enabling system font", async ({ page, app, user }) => {
|
||||
await app.closeVerifyToast();
|
||||
await app.settings.openUserSettings("Appearance");
|
||||
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
|
||||
|
||||
// Click "Show advanced" link button
|
||||
await tab.getByRole("button", { name: "Show advanced" }).click();
|
||||
|
||||
await tab.getByRole("switch", { name: "Use bundled emoji font" }).click();
|
||||
await tab.getByRole("switch", { name: "Use a system font" }).click();
|
||||
|
||||
// Assert that the font-family value was removed
|
||||
await expect(page.locator("body")).toHaveCSS("font-family", '""');
|
||||
});
|
||||
|
||||
test(
|
||||
"should keep same font and emoji when switching theme",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, util }) => {
|
||||
await app.closeVerifyToast();
|
||||
|
||||
const roomId = await util.createAndDisplayRoom();
|
||||
|
||||
await app.client.sendMessage(roomId, { body: "Message with 🦡", msgtype: "m.text" });
|
||||
|
||||
await app.settings.openUserSettings("Appearance");
|
||||
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
|
||||
await tab.getByRole("button", { name: "Show advanced" }).click();
|
||||
await tab.getByRole("switch", { name: "Use bundled emoji font" }).click();
|
||||
await tab.getByRole("switch", { name: "Use a system font" }).click();
|
||||
|
||||
await app.closeDialog();
|
||||
await expect(page).toMatchScreenshot("window-before-switch.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
|
||||
// Switch to dark theme
|
||||
await app.settings.openUserSettings("Appearance");
|
||||
await util.getMatchSystemThemeSwitch().click();
|
||||
await util.getDarkTheme().click();
|
||||
|
||||
await app.closeDialog();
|
||||
// Font and emoji should remain the same after theme switch
|
||||
await expect(page).toMatchScreenshot("window-after-switch.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 { type Locator, type Page } from "@playwright/test";
|
||||
|
||||
import { type ElementAppPage } from "../../../pages/ElementAppPage";
|
||||
import { test as base, expect } from "../../../element-web-test";
|
||||
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../../src/settings/enums/Layout";
|
||||
|
||||
export { expect };
|
||||
|
||||
/**
|
||||
* Set up for the appearance tab test
|
||||
*/
|
||||
export const test = base.extend<{
|
||||
util: Helpers;
|
||||
}>({
|
||||
util: async ({ page, app }, use) => {
|
||||
await use(new Helpers(page, app));
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* A collection of helper functions for the appearance tab test
|
||||
* The goal is to make easier to get and interact with the button, input, or other elements of the appearance tab
|
||||
*/
|
||||
class Helpers {
|
||||
private CUSTOM_THEME_URL = "http://custom.theme";
|
||||
private CUSTOM_THEME = {
|
||||
name: "Custom theme",
|
||||
isDark: false,
|
||||
colors: {},
|
||||
compound: {
|
||||
"--cpd-color-bg-canvas-default": "tomato",
|
||||
},
|
||||
};
|
||||
|
||||
constructor(
|
||||
private page: Page,
|
||||
private app: ElementAppPage,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Open the appearance tab
|
||||
*/
|
||||
openAppearanceTab() {
|
||||
return this.app.settings.openUserSettings("Appearance");
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the appearance tab
|
||||
*/
|
||||
closeAppearanceTab() {
|
||||
return this.app.settings.closeDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare screenshot and hide the matrix chat
|
||||
* @param locator
|
||||
* @param screenshot
|
||||
*/
|
||||
assertScreenshot(locator: Locator, screenshot: `${string}.png`) {
|
||||
return expect(locator).toMatchScreenshot(screenshot, {
|
||||
css: `
|
||||
#matrixchat {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
// Theme Panel
|
||||
|
||||
/**
|
||||
* Disable in the settings the system theme
|
||||
*/
|
||||
disableSystemTheme() {
|
||||
return this.app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the theme section
|
||||
*/
|
||||
getThemePanel() {
|
||||
return this.page.getByTestId("themePanel");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the system theme toggle
|
||||
*/
|
||||
getMatchSystemThemeSwitch() {
|
||||
return this.getThemePanel().getByRole("switch", { name: "Match system theme" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the theme radio button
|
||||
* @param theme - the theme to select
|
||||
* @private
|
||||
*/
|
||||
private getThemeRadio(theme: string) {
|
||||
return this.getThemePanel().getByRole("radio", { name: theme });
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the light theme radio button
|
||||
*/
|
||||
getLightTheme() {
|
||||
return this.getThemeRadio("Light");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the dark theme radio button
|
||||
*/
|
||||
getDarkTheme() {
|
||||
return this.getThemeRadio("Dark");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the custom theme radio button
|
||||
*/
|
||||
getCustomTheme() {
|
||||
return this.getThemeRadio(this.CUSTOM_THEME.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the high contrast theme radio button
|
||||
*/
|
||||
getHighContrastTheme() {
|
||||
return this.getThemeRadio("High contrast");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom theme
|
||||
* Mock the request to the custom and return a fake local custom theme
|
||||
*/
|
||||
async addCustomTheme() {
|
||||
await this.page.route(this.CUSTOM_THEME_URL, (route) =>
|
||||
route.fulfill({ body: JSON.stringify(this.CUSTOM_THEME) }),
|
||||
);
|
||||
await this.page.getByRole("textbox", { name: "Add custom theme" }).fill(this.CUSTOM_THEME_URL);
|
||||
await this.page.getByRole("button", { name: "Add custom theme" }).click();
|
||||
await this.page.unroute(this.CUSTOM_THEME_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the custom theme
|
||||
*/
|
||||
removeCustomTheme() {
|
||||
return this.getThemePanel().getByRole("listitem", { name: this.CUSTOM_THEME.name }).getByRole("button").click();
|
||||
}
|
||||
|
||||
// Message layout Panel
|
||||
|
||||
/**
|
||||
* Create and display a room named Test Room
|
||||
*/
|
||||
async createAndDisplayRoom(): Promise<string> {
|
||||
const roomId = await this.app.client.createRoom({ name: "Test Room" });
|
||||
await this.app.viewRoomByName("Test Room");
|
||||
return roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the room layout
|
||||
* @param layout
|
||||
* @private
|
||||
*/
|
||||
private assertRoomLayout(layout: Layout) {
|
||||
return expect(this.page.locator(`.mx_RoomView_body[data-layout=${layout}]`)).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the room layout is modern
|
||||
*/
|
||||
assertModernLayout() {
|
||||
return this.assertRoomLayout(Layout.Group);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the room layout is bubble
|
||||
*/
|
||||
assertBubbleLayout() {
|
||||
return this.assertRoomLayout(Layout.Bubble);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the layout panel
|
||||
*/
|
||||
getMessageLayoutPanel() {
|
||||
return this.page.getByTestId("layoutPanel");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the layout radio button
|
||||
* @param layoutName
|
||||
* @private
|
||||
*/
|
||||
private getLayout(layoutName: string) {
|
||||
return this.getMessageLayoutPanel().getByRole("radio", { name: layoutName });
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the message bubbles layout radio button
|
||||
*/
|
||||
getBubbleLayout() {
|
||||
return this.getLayout("Message bubbles");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the modern layout radio button
|
||||
*/
|
||||
getModernLayout() {
|
||||
return this.getLayout("Modern");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the IRC layout radio button
|
||||
*/
|
||||
getIRCLayout() {
|
||||
return this.getLayout("IRC (experimental)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the compact layout switch
|
||||
*/
|
||||
getCompactLayoutSwitch() {
|
||||
return this.getMessageLayoutPanel().getByRole("switch", { name: "Show compact text and messages" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the compact layout is enabled
|
||||
*/
|
||||
assertCompactLayout() {
|
||||
return expect(
|
||||
this.page.locator("#matrixchat .mx_MatrixChat_wrapper.mx_MatrixChat_useCompactLayout"),
|
||||
).toBeVisible();
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Suguru Hirahara
|
||||
|
||||
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 { expect, test } from ".";
|
||||
|
||||
test.describe("Appearance user settings tab", () => {
|
||||
test.use({
|
||||
displayName: "Hanako",
|
||||
});
|
||||
|
||||
test.describe("Message Layout Panel", () => {
|
||||
test.beforeEach(async ({ app, user, util }) => {
|
||||
await util.createAndDisplayRoom();
|
||||
await util.assertModernLayout();
|
||||
await util.openAppearanceTab();
|
||||
});
|
||||
|
||||
test(
|
||||
"should change the message layout from modern to bubble",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, util }) => {
|
||||
await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png");
|
||||
|
||||
await util.getBubbleLayout().click();
|
||||
|
||||
// Assert that modern are irc layout are not selected
|
||||
await expect(util.getBubbleLayout()).toBeChecked();
|
||||
await expect(util.getModernLayout()).not.toBeChecked();
|
||||
await expect(util.getIRCLayout()).not.toBeChecked();
|
||||
|
||||
// Assert that the room layout is set to bubble layout
|
||||
await util.assertBubbleLayout();
|
||||
await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png");
|
||||
},
|
||||
);
|
||||
|
||||
test("should enable compact layout when the modern layout is selected", async ({ page, app, user, util }) => {
|
||||
await expect(util.getCompactLayoutSwitch()).not.toBeChecked();
|
||||
|
||||
await util.getCompactLayoutSwitch().click();
|
||||
await util.assertCompactLayout();
|
||||
});
|
||||
|
||||
test("should disable compact layout when the modern layout is not selected", async ({
|
||||
page,
|
||||
app,
|
||||
user,
|
||||
util,
|
||||
}) => {
|
||||
await expect(util.getCompactLayoutSwitch()).not.toBeDisabled();
|
||||
|
||||
// Select the bubble layout, which should disable the compact layout checkbox
|
||||
await util.getBubbleLayout().click();
|
||||
await expect(util.getCompactLayoutSwitch()).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Suguru Hirahara
|
||||
|
||||
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 { expect, test } from ".";
|
||||
|
||||
test.describe("Appearance user settings tab", () => {
|
||||
test.use({
|
||||
displayName: "Hanako",
|
||||
});
|
||||
|
||||
test.describe("Theme Choice Panel", () => {
|
||||
test.beforeEach(async ({ app, user, util }) => {
|
||||
// Disable the default theme for consistency in case ThemeWatcher automatically chooses it
|
||||
await util.disableSystemTheme();
|
||||
await app.closeVerifyToast();
|
||||
|
||||
await util.openAppearanceTab();
|
||||
});
|
||||
|
||||
test(
|
||||
"should be rendered with the light theme selected",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util, axe }) => {
|
||||
// Assert that 'Match system theme' is not checked
|
||||
await expect(util.getMatchSystemThemeSwitch()).not.toBeChecked();
|
||||
|
||||
// Assert that the light theme is selected
|
||||
await expect(util.getLightTheme()).toBeChecked();
|
||||
// Assert that the dark and high contrast themes are not selected
|
||||
await expect(util.getDarkTheme()).not.toBeChecked();
|
||||
await expect(util.getHighContrastTheme()).not.toBeChecked();
|
||||
|
||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png");
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"should disable the themes when the system theme is clicked",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
await util.getMatchSystemThemeSwitch().click();
|
||||
|
||||
// Assert that the themes are disabled
|
||||
await expect(util.getLightTheme()).toBeDisabled();
|
||||
await expect(util.getDarkTheme()).toBeDisabled();
|
||||
await expect(util.getHighContrastTheme()).toBeDisabled();
|
||||
|
||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png");
|
||||
},
|
||||
);
|
||||
|
||||
test("should change the theme to dark", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||
// Assert that the light theme is selected
|
||||
await expect(util.getLightTheme()).toBeChecked();
|
||||
|
||||
await util.getDarkTheme().click();
|
||||
|
||||
// Assert that the light and high contrast themes are not selected
|
||||
await expect(util.getLightTheme()).not.toBeChecked();
|
||||
await expect(util.getDarkTheme()).toBeChecked();
|
||||
await expect(util.getHighContrastTheme()).not.toBeChecked();
|
||||
|
||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-dark.png");
|
||||
});
|
||||
|
||||
test.describe("custom theme", () => {
|
||||
test.use({
|
||||
labsFlags: ["feature_custom_themes"],
|
||||
});
|
||||
|
||||
test("should render the custom theme section", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png");
|
||||
});
|
||||
|
||||
test(
|
||||
"should be able to add and remove a custom theme",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
await util.addCustomTheme();
|
||||
|
||||
await expect(util.getCustomTheme()).not.toBeChecked();
|
||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png");
|
||||
|
||||
await util.removeCustomTheme();
|
||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-removed.png");
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"should keep custom theme when reloading the page",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, util }) => {
|
||||
await util.addCustomTheme();
|
||||
await util.getCustomTheme().click();
|
||||
await util.closeAppearanceTab();
|
||||
|
||||
await expect(page).toMatchScreenshot("window-custom-theme.png");
|
||||
|
||||
await page.reload();
|
||||
await app.closeVerifyToast();
|
||||
|
||||
await util.openAppearanceTab();
|
||||
// Assert that the custom theme is still selected after reloading the page
|
||||
await expect(util.getCustomTheme()).toBeChecked();
|
||||
|
||||
await util.closeAppearanceTab();
|
||||
await expect(page).toMatchScreenshot("window-custom-theme.png");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 { test, expect } from "../../element-web-test";
|
||||
import { uiaLongSessionTimeoutHomeserver } from "../../plugins/homeserver/synapse/uiaLongSessionTimeoutHomeserver.ts";
|
||||
|
||||
// This is needed to not get stopped by UIA when deleting other devices
|
||||
test.use(uiaLongSessionTimeoutHomeserver);
|
||||
test.describe("Device manager", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ homeserver, user }) => {
|
||||
// create 3 extra sessions to manage
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await homeserver.loginUser(user.userId, user.password);
|
||||
}
|
||||
});
|
||||
|
||||
test("should display sessions", async ({ page, app, axe }) => {
|
||||
await app.settings.openUserSettings("Sessions");
|
||||
const tab = page.locator(".mx_SettingsTab");
|
||||
|
||||
await expect(tab.getByText("Current session", { exact: true })).toBeVisible();
|
||||
|
||||
const currentSessionSection = tab.getByTestId("current-session-section");
|
||||
await expect(currentSessionSection.getByText("Unverified session")).toBeVisible();
|
||||
|
||||
// current session details opened
|
||||
await currentSessionSection.getByRole("button", { name: "Show details" }).click();
|
||||
await expect(currentSessionSection.getByText("Session details")).toBeVisible();
|
||||
|
||||
// close current session details
|
||||
await currentSessionSection.getByRole("button", { name: "Hide details" }).click();
|
||||
await expect(currentSessionSection.getByText("Session details")).not.toBeVisible();
|
||||
|
||||
const securityRecommendationsSection = tab.getByTestId("security-recommendations-section");
|
||||
await expect(securityRecommendationsSection.getByText("Security recommendations")).toBeVisible();
|
||||
await securityRecommendationsSection.getByRole("button", { name: "View all (3)" }).click();
|
||||
|
||||
/**
|
||||
* Other sessions section
|
||||
*/
|
||||
await expect(tab.getByText("Other sessions")).toBeVisible();
|
||||
// filter applied after clicking through from security recommendations
|
||||
await expect(tab.getByLabel("Filter devices")).toHaveText("Show: Unverified");
|
||||
const filteredDeviceListItems = tab.locator(".mx_FilteredDeviceList_listItem");
|
||||
await expect(filteredDeviceListItems).toHaveCount(3);
|
||||
|
||||
// select two sessions
|
||||
// force click as the input element itself is not visible (its size is zero)
|
||||
await filteredDeviceListItems.first().click({ force: true });
|
||||
await filteredDeviceListItems.last().click({ force: true });
|
||||
|
||||
// sign out from list selection action buttons
|
||||
await tab.getByRole("button", { name: "Remove this device", exact: true }).click();
|
||||
await page.getByRole("dialog").getByTestId("dialog-primary-button").click();
|
||||
|
||||
// list updated after sign out
|
||||
await expect(filteredDeviceListItems).toHaveCount(1);
|
||||
// security recommendation count updated
|
||||
await expect(tab.getByRole("button", { name: "View all (1)" })).toBeVisible();
|
||||
|
||||
const sessionName = `Alice's device`;
|
||||
// open the first session
|
||||
const firstSession = filteredDeviceListItems.first();
|
||||
await firstSession.getByRole("button", { name: "Show details" }).click();
|
||||
|
||||
await expect(firstSession.getByText("Session details")).toBeVisible();
|
||||
|
||||
await firstSession.getByRole("button", { name: "Rename" }).click();
|
||||
await firstSession.getByTestId("device-rename-input").type(sessionName);
|
||||
await firstSession.getByRole("button", { name: "Save" }).click();
|
||||
// there should be a spinner while device updates
|
||||
await expect(firstSession.locator(".mx_Spinner")).toBeVisible();
|
||||
// wait for spinner to complete
|
||||
await expect(firstSession.locator(".mx_Spinner")).not.toBeVisible();
|
||||
|
||||
// session name updated in details
|
||||
await expect(firstSession.locator(".mx_DeviceDetailHeading h4").getByText(sessionName)).toBeVisible();
|
||||
// and main list item
|
||||
await expect(firstSession.locator(".mx_DeviceTile h3").getByText(sessionName)).toBeVisible();
|
||||
|
||||
// sign out using the device details sign out
|
||||
await firstSession.getByRole("button", { name: "Remove this session" }).click();
|
||||
|
||||
// confirm the signout
|
||||
await page.getByRole("dialog").getByTestId("dialog-primary-button").click();
|
||||
|
||||
// no other sessions or security recommendations sections when only one session
|
||||
await expect(tab.getByText("Other sessions")).not.toBeVisible();
|
||||
await expect(tab.getByTestId("security-recommendations-section")).not.toBeVisible();
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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 { test, expect } from "./index";
|
||||
import { checkDeviceIsCrossSigned } from "../../crypto/utils";
|
||||
import { bootstrapCrossSigningForClient } from "../../../pages/client";
|
||||
|
||||
test.describe("Advanced section in Encryption tab", () => {
|
||||
test.beforeEach(async ({ page, app, homeserver, credentials, util }) => {
|
||||
const clientHandle = await app.client.prepareClient();
|
||||
// Reset cross signing in order to have a verified session
|
||||
await bootstrapCrossSigningForClient(clientHandle, credentials, true);
|
||||
});
|
||||
|
||||
test("should show the encryption details", { tag: "@screenshot" }, async ({ page, app, util, axe }) => {
|
||||
await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
const deviceId = await page.evaluate(() => window.mxMatrixClientPeg.get().getDeviceId());
|
||||
await expect(section.getByText(deviceId)).toBeVisible();
|
||||
|
||||
await expect(section).toMatchScreenshot("encryption-details.png", {
|
||||
mask: [section.getByTestId("deviceId"), section.getByTestId("sessionKey")],
|
||||
});
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("should show the import room keys dialog", async ({ page, app, util }) => {
|
||||
await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
await section.getByRole("button", { name: "Import keys" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Import room keys" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show the export room keys dialog", async ({ page, app, util }) => {
|
||||
await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
await section.getByRole("button", { name: "Export keys" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Export room keys" })).toBeVisible();
|
||||
});
|
||||
|
||||
test(
|
||||
"should reset the cryptographic identity",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, credentials, util }) => {
|
||||
const tab = await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
await section.getByRole("button", { name: "Reset cryptographic identity" }).click();
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("reset-cryptographic-identity.png");
|
||||
await tab.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Fill password dialog and validate
|
||||
const dialog = page.locator(".mx_InteractiveAuthDialog");
|
||||
await dialog.getByRole("textbox", { name: "Password" }).fill(credentials.password);
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await expect(section.getByRole("button", { name: "Reset cryptographic identity" })).toBeVisible();
|
||||
|
||||
// After resetting the identity, the user should set up a new recovery key
|
||||
await expect(
|
||||
util.getEncryptionRecoverySection().getByRole("button", { name: "Get recovery key" }),
|
||||
).toBeVisible();
|
||||
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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 { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { test, expect } from ".";
|
||||
import {
|
||||
checkDeviceIsConnectedKeyBackup,
|
||||
checkDeviceIsCrossSigned,
|
||||
createBot,
|
||||
deleteCachedSecrets,
|
||||
verifySession,
|
||||
} from "../../crypto/utils";
|
||||
|
||||
test.describe("Encryption tab", () => {
|
||||
test.use({ displayName: "Alice" });
|
||||
|
||||
test.describe("when encryption is set up", () => {
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
let expectedBackupVersion: string;
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
|
||||
const botCredentials = { ...credentials };
|
||||
delete botCredentials.accessToken; // use a new login for the bot
|
||||
const res = await createBot(page, homeserver, botCredentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
expectedBackupVersion = res.expectedBackupVersion;
|
||||
});
|
||||
|
||||
test(
|
||||
"should show a 'Verify this device' button if the device is unverified",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
const dialog = await util.openEncryptionTab();
|
||||
const content = util.getEncryptionTabContent();
|
||||
|
||||
// The user's device is in an unverified state, therefore the only option available to them here is to verify it
|
||||
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
|
||||
await expect(verifyButton).toBeVisible();
|
||||
await expect(content).toMatchScreenshot("verify-device-encryption-tab.png");
|
||||
await verifyButton.click();
|
||||
|
||||
await util.verifyDevice(recoveryKey);
|
||||
|
||||
// Prevent flakiness by scrolling to top of the tab
|
||||
await page.getByRole("heading", { name: "Key storage" }).scrollIntoViewIfNeeded();
|
||||
|
||||
await expect(content).toMatchScreenshot("default-tab.png", {
|
||||
mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")],
|
||||
});
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
},
|
||||
);
|
||||
|
||||
// Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB.
|
||||
//
|
||||
// This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
|
||||
// We simulate this case by deleting the cached secrets in the indexedDB.
|
||||
test(
|
||||
"should prompt to enter the recovery key when the secrets are not cached locally",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
// We need to delete the cached secrets
|
||||
await deleteCachedSecrets(page);
|
||||
|
||||
await util.openEncryptionTab();
|
||||
// We ask the user to enter the recovery key
|
||||
const dialog = util.getEncryptionTabContent();
|
||||
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
|
||||
await expect(enterKeyButton).toBeVisible();
|
||||
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
|
||||
await enterKeyButton.click();
|
||||
|
||||
// Fill the recovery key
|
||||
await util.enterRecoveryKey(recoveryKey);
|
||||
await dialog.getByRole("heading", { name: "Key storage" }).scrollIntoViewIfNeeded();
|
||||
await expect(dialog).toMatchScreenshot("default-tab.png", {
|
||||
mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")],
|
||||
});
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
},
|
||||
);
|
||||
|
||||
test("should display the reset identity panel when the user clicks on 'Forgot recovery key?'", async ({
|
||||
page,
|
||||
app,
|
||||
util,
|
||||
}) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
// We need to delete the cached secrets
|
||||
await deleteCachedSecrets(page);
|
||||
|
||||
// The "Key storage is out sync" section is displayed and the user click on the "Forgot recovery key?" button
|
||||
await util.openEncryptionTab();
|
||||
const dialog = util.getEncryptionTabContent();
|
||||
await dialog.getByRole("button", { name: "Forgot recovery key?" }).click();
|
||||
|
||||
// The user is prompted to reset their identity
|
||||
await expect(
|
||||
dialog.getByText("Forgot your recovery key? You’ll need to reset your digital identity."),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
await util.openEncryptionTab();
|
||||
|
||||
await page.getByRole("switch", { name: "Allow key storage" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png");
|
||||
|
||||
const deleteRequestPromises = [
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.master")),
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.self_signing")),
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.user_signing")),
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.megolm_backup.v1")),
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.secret_storage.default_key")),
|
||||
page.waitForRequest((req) => req.url().includes("/account_data/m.secret_storage.key.")),
|
||||
];
|
||||
|
||||
await page.getByRole("button", { name: "Delete key storage" }).click();
|
||||
|
||||
await expect(page.getByRole("switch", { name: "Allow key storage" })).not.toBeChecked();
|
||||
|
||||
for (const prom of deleteRequestPromises) {
|
||||
const request = await prom;
|
||||
expect(request.method()).toBe("PUT");
|
||||
expect(request.postData()).toBe(JSON.stringify({}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("when encryption is not set up", () => {
|
||||
test("'Verify this device' allows us to become verified", async ({
|
||||
page,
|
||||
user,
|
||||
credentials,
|
||||
app,
|
||||
}, workerInfo) => {
|
||||
const settings = await app.settings.openUserSettings("Encryption");
|
||||
|
||||
// Initially, our device is not verified
|
||||
await expect(settings.getByRole("heading", { name: "Device not verified" })).toBeVisible();
|
||||
|
||||
// We will reset our identity
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
||||
|
||||
// First try cancelling and restarting
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
||||
|
||||
// Then click outside the dialog and restart
|
||||
await page.locator("li").filter({ hasText: "Encryption" }).click({ force: true });
|
||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
||||
|
||||
// Finally we actually continue
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Now we are verified, so we see the Key storage toggle
|
||||
await expect(settings.getByRole("heading", { name: "Key storage" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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 { type Page } from "@playwright/test";
|
||||
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { type ElementAppPage } from "../../../pages/ElementAppPage";
|
||||
import { test as base, expect } from "../../../element-web-test";
|
||||
export { expect };
|
||||
|
||||
/**
|
||||
* Set up for the encryption tab test
|
||||
*/
|
||||
export const test = base.extend<{
|
||||
util: Helpers;
|
||||
}>({
|
||||
displayName: "Alice",
|
||||
|
||||
util: async ({ page, app, bot }, use) => {
|
||||
await use(new Helpers(page, app));
|
||||
},
|
||||
});
|
||||
|
||||
class Helpers {
|
||||
constructor(
|
||||
private page: Page,
|
||||
private app: ElementAppPage,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Open the encryption tab
|
||||
*/
|
||||
openEncryptionTab() {
|
||||
return this.app.settings.openUserSettings("Encryption");
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through the device verification flow using the recovery key.
|
||||
*/
|
||||
async verifyDevice(recoveryKey: GeneratedSecretStorageKey) {
|
||||
// Select the security phrase
|
||||
await this.page.getByRole("button", { name: "Use recovery key" }).click();
|
||||
await this.enterRecoveryKey(recoveryKey);
|
||||
await this.page.getByRole("button", { name: "Done" }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the recovery key in the dialog
|
||||
* @param recoveryKey
|
||||
*/
|
||||
async enterRecoveryKey(recoveryKey: GeneratedSecretStorageKey) {
|
||||
// Fill the recovery key
|
||||
const dialog = this.page.locator(".mx_Dialog");
|
||||
await dialog.getByRole("textbox").fill(recoveryKey.encodedPrivateKey);
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the encryption tab content
|
||||
*/
|
||||
getEncryptionTabContent() {
|
||||
return this.page.getByTestId("encryptionTab");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recovery section
|
||||
*/
|
||||
getEncryptionRecoverySection() {
|
||||
return this.page.getByTestId("recoveryPanel");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the encryption details section
|
||||
*/
|
||||
getEncryptionDetailsSection() {
|
||||
return this.page.getByTestId("encryptionDetails");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default key id of the secret storage to `null`
|
||||
*/
|
||||
async removeSecretStorageDefaultKeyId() {
|
||||
const client = await this.app.client.prepareClient();
|
||||
await client.evaluate(async (client) => {
|
||||
await client.secretStorage.setDefaultKeyId(null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recovery key from the clipboard and fill in the input field
|
||||
* Then click on the finish button
|
||||
* @param title - The title of the dialog
|
||||
* @param confirmButtonLabel - The label of the confirm button
|
||||
* @param screenshot
|
||||
*/
|
||||
async confirmRecoveryKey(title: string, confirmButtonLabel: string, screenshot: `${string}.png`) {
|
||||
const dialog = this.getEncryptionTabContent();
|
||||
await expect(dialog.getByText(title, { exact: true })).toBeVisible();
|
||||
await expect(dialog).toMatchScreenshot(screenshot);
|
||||
|
||||
const clipboardContent = await this.app.getClipboard();
|
||||
await dialog.getByRole("textbox").fill(clipboardContent);
|
||||
const button = dialog.getByRole("button", { name: confirmButtonLabel });
|
||||
await button.click();
|
||||
// Button should disable immediately after clicking.
|
||||
await expect(button).toBeDisabled();
|
||||
await expect(this.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations 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 { type StartedHomeserverContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
import { type Page, type Browser, type TestInfo } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "./index";
|
||||
import { ElementAppPage } from "../../../pages/ElementAppPage";
|
||||
import { createRoom, sendMessageInCurrentRoom, verifyApp } from "../../crypto/utils";
|
||||
import { type CredentialsOptionalAccessToken } from "../../../pages/bot";
|
||||
|
||||
test.describe("Other people's devices section in Encryption tab", () => {
|
||||
test.use({
|
||||
displayName: "alice",
|
||||
});
|
||||
|
||||
test("unverified devices should be able to decrypt while global blacklist is not toggled", async ({
|
||||
page: alicePage,
|
||||
app: aliceElementApp,
|
||||
homeserver,
|
||||
browser,
|
||||
user: aliceCredentials,
|
||||
}, testInfo) => {
|
||||
await prepForEncryption(aliceElementApp, aliceCredentials);
|
||||
|
||||
// Create a second browser instance.
|
||||
const { bobCredentials, bobPage } = await newBrowser(homeserver, testInfo, browser);
|
||||
|
||||
// Create the room and invite bob
|
||||
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
|
||||
|
||||
// Alice sends a message, which Bob should be able to decrypt
|
||||
await sendMessageInCurrentRoom(alicePage, "Decryptable");
|
||||
await expect(bobPage.getByText("Decryptable")).toBeVisible();
|
||||
});
|
||||
|
||||
test("unverified devices should not be able to decrypt while global blacklist is toggled", async ({
|
||||
page: alicePage,
|
||||
app: aliceElementApp,
|
||||
homeserver,
|
||||
browser,
|
||||
user: aliceCredentials,
|
||||
util,
|
||||
}, testInfo) => {
|
||||
await prepForEncryption(aliceElementApp, aliceCredentials);
|
||||
|
||||
// Enable blacklist toggle.
|
||||
const dialog = await util.openEncryptionTab();
|
||||
const blacklistToggle = dialog.getByRole("switch", {
|
||||
name: "In encrypted rooms, only send messages to verified users",
|
||||
});
|
||||
await blacklistToggle.scrollIntoViewIfNeeded();
|
||||
await expect(blacklistToggle).toBeVisible();
|
||||
await blacklistToggle.click();
|
||||
await aliceElementApp.settings.closeDialog();
|
||||
|
||||
// Create a second browser instance.
|
||||
const { bobCredentials, bobPage } = await newBrowser(homeserver, testInfo, browser);
|
||||
|
||||
// Create the room and invite bob
|
||||
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
|
||||
|
||||
// Alice sends a message, which Bob should not be able to decrypt
|
||||
await sendMessageInCurrentRoom(alicePage, "Undecryptable");
|
||||
await expect(
|
||||
bobPage.getByText(
|
||||
"The sender has blocked you from receiving this message because your device is unverified",
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("verified devices should be able to decrypt while global blacklist is toggled", async ({
|
||||
page: alicePage,
|
||||
app: aliceElementApp,
|
||||
homeserver,
|
||||
browser,
|
||||
user: aliceCredentials,
|
||||
util,
|
||||
}, testInfo) => {
|
||||
await prepForEncryption(aliceElementApp, aliceCredentials);
|
||||
|
||||
// Enable blacklist toggle.
|
||||
const dialog = await util.openEncryptionTab();
|
||||
const blacklistToggle = dialog.getByRole("switch", {
|
||||
name: "In encrypted rooms, only send messages to verified users",
|
||||
});
|
||||
await blacklistToggle.scrollIntoViewIfNeeded();
|
||||
await expect(blacklistToggle).toBeVisible();
|
||||
await blacklistToggle.click();
|
||||
await aliceElementApp.settings.closeDialog();
|
||||
|
||||
// Create a second browser instance.
|
||||
const { bobCredentials, bobPage, bobElementApp } = await newBrowser(homeserver, testInfo, browser);
|
||||
|
||||
// Create the room and invite bob
|
||||
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
|
||||
await bobElementApp.closeNotificationToast();
|
||||
|
||||
// Perform verification.
|
||||
await verifyApp("alice", aliceElementApp, "bob", bobElementApp);
|
||||
|
||||
// Alice sends a message, which Bob should be able to decrypt
|
||||
await sendMessageInCurrentRoom(alicePage, "Decryptable");
|
||||
await expect(bobPage.getByText("Decryptable")).toBeVisible();
|
||||
});
|
||||
|
||||
test("setting per-room unverified blacklist toggle does not affect other rooms", async ({
|
||||
page: alicePage,
|
||||
app: aliceElementApp,
|
||||
homeserver,
|
||||
browser,
|
||||
user: aliceCredentials,
|
||||
}, testInfo) => {
|
||||
await prepForEncryption(aliceElementApp, aliceCredentials);
|
||||
|
||||
// Create a second browser instance.
|
||||
const { bobCredentials, bobPage } = await newBrowser(homeserver, testInfo, browser);
|
||||
|
||||
// Alice creates the room and invites Bob.
|
||||
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
|
||||
|
||||
// Alice configures her client to blacklist unverified users in this room.
|
||||
const dialog = await aliceElementApp.settings.openRoomSettings("Security & Privacy");
|
||||
await dialog.getByRole("switch", { name: "Only send messages to verified users." }).click();
|
||||
await aliceElementApp.settings.closeDialog();
|
||||
|
||||
// Alice sends a message which Bob should not be able to decrypt.
|
||||
await sendMessageInCurrentRoom(alicePage, "Undecryptable");
|
||||
await expect(
|
||||
bobPage.getByText(
|
||||
"The sender has blocked you from receiving this message because your device is unverified",
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
// Alice creates a second room and invites Bob.
|
||||
await createRoom(alicePage, "TestRoom2", true);
|
||||
await aliceElementApp.toggleRoomInfoPanel(); // should not be necessary, called in body of below
|
||||
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
|
||||
|
||||
// Bob accepts the invite.
|
||||
await bobPage.getByRole("option", { name: "TestRoom2" }).click();
|
||||
await bobPage.getByRole("button", { name: "Accept" }).click();
|
||||
|
||||
// Alice sends a message in the new room, which Bob should be able to decrypt.
|
||||
await sendMessageInCurrentRoom(alicePage, "Decryptable");
|
||||
await expect(bobPage.getByText("Decryptable")).toBeVisible();
|
||||
});
|
||||
|
||||
test("setting per-room unverified blacklist toggle overrides global toggle", async ({
|
||||
page: alicePage,
|
||||
app: aliceElementApp,
|
||||
homeserver,
|
||||
browser,
|
||||
user: aliceCredentials,
|
||||
util,
|
||||
}, testInfo) => {
|
||||
await prepForEncryption(aliceElementApp, aliceCredentials);
|
||||
|
||||
// Enable blacklist toggle.
|
||||
let dialog = await util.openEncryptionTab();
|
||||
const blacklistToggle = dialog.getByRole("switch", {
|
||||
name: "In encrypted rooms, only send messages to verified users",
|
||||
});
|
||||
await blacklistToggle.scrollIntoViewIfNeeded();
|
||||
await expect(blacklistToggle).toBeVisible();
|
||||
await blacklistToggle.click();
|
||||
await aliceElementApp.settings.closeDialog();
|
||||
|
||||
// Create a second browser instance.
|
||||
const { bobCredentials, bobPage } = await newBrowser(homeserver, testInfo, browser);
|
||||
|
||||
// Alice creates the room and invites Bob.
|
||||
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
|
||||
|
||||
// Alice configures her client to allow sending to unverified users in this room.
|
||||
dialog = await aliceElementApp.settings.openRoomSettings("Security & Privacy");
|
||||
await dialog.getByRole("switch", { name: "Only send messages to verified users." }).click();
|
||||
await aliceElementApp.settings.closeDialog();
|
||||
|
||||
// Alice sends a message which Bob should be able to decrypt.
|
||||
await sendMessageInCurrentRoom(alicePage, "Decryptable");
|
||||
await expect(bobPage.getByText("Decryptable")).toBeVisible();
|
||||
|
||||
// Alice creates a second room and invites Bob.
|
||||
await createRoom(alicePage, "TestRoom2", true);
|
||||
await aliceElementApp.toggleRoomInfoPanel(); // should not be necessary, called in body of below
|
||||
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
|
||||
|
||||
// Bob accepts the invite.
|
||||
await bobPage.getByRole("option", { name: "TestRoom2" }).click();
|
||||
await bobPage.getByRole("button", { name: "Accept" }).click();
|
||||
|
||||
// Alice sends a message in the new room, which Bob should not be able to decrypt.
|
||||
await sendMessageInCurrentRoom(alicePage, "Undecryptable");
|
||||
await expect(
|
||||
bobPage.getByText(
|
||||
"The sender has blocked you from receiving this message because your device is unverified",
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
async function inviteBobToNewRoom(
|
||||
alicePage: Page,
|
||||
aliceElementApp: ElementAppPage,
|
||||
bobCredentials: CredentialsOptionalAccessToken,
|
||||
bobPage: Page,
|
||||
) {
|
||||
await createRoom(alicePage, "TestRoom", true);
|
||||
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true });
|
||||
await bobPage.getByRole("option", { name: "TestRoom" }).click();
|
||||
await bobPage.getByRole("button", { name: "Accept" }).click();
|
||||
}
|
||||
|
||||
async function newBrowser(
|
||||
homeserver: StartedHomeserverContainer,
|
||||
testInfo: TestInfo,
|
||||
browser: Browser,
|
||||
): Promise<{ bobCredentials: CredentialsOptionalAccessToken; bobPage: Page; bobElementApp: ElementAppPage }> {
|
||||
const bobCredentials = await homeserver.registerUser(`user_${testInfo.testId}_bob`, "password", "bob");
|
||||
const bobPage = await createNewInstance(browser, bobCredentials, {});
|
||||
const bobElementApp = new ElementAppPage(bobPage);
|
||||
await prepForEncryption(bobElementApp, bobCredentials);
|
||||
return { bobCredentials, bobPage, bobElementApp };
|
||||
}
|
||||
|
||||
async function prepForEncryption(app: ElementAppPage, credentials: CredentialsOptionalAccessToken): Promise<void> {
|
||||
await app.client.bootstrapCrossSigning(credentials);
|
||||
await app.closeKeyStorageToast();
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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 { test, expect } from ".";
|
||||
import { checkDeviceIsConnectedKeyBackup, createBot, verifySession } from "../../crypto/utils";
|
||||
import type { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
test.describe("Recovery section in Encryption tab", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
|
||||
const botCredentials = { ...credentials };
|
||||
delete botCredentials.accessToken; // use a new login for the bot
|
||||
const res = await createBot(page, homeserver, botCredentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
});
|
||||
|
||||
test(
|
||||
"should change the recovery key",
|
||||
{ tag: ["@screenshot", "@no-webkit"] },
|
||||
async ({ page, app, homeserver, credentials, util, context }) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
const dialog = await util.openEncryptionTab();
|
||||
|
||||
// The user can only change the recovery key
|
||||
const changeButton = dialog.getByRole("button", { name: "Change recovery key" });
|
||||
await expect(changeButton).toBeVisible();
|
||||
await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
|
||||
await changeButton.click();
|
||||
|
||||
// Display the new recovery key and click on the copy button
|
||||
await expect(dialog.getByText("Change recovery key?")).toBeVisible();
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("change-key-1-encryption-tab.png", {
|
||||
mask: [dialog.getByTestId("recoveryKey")],
|
||||
});
|
||||
await dialog.getByRole("button", { name: "Copy" }).click();
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Confirm the recovery key
|
||||
await util.confirmRecoveryKey(
|
||||
"Enter your new recovery key",
|
||||
"Confirm new recovery key",
|
||||
"change-key-2-encryption-tab.png",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test("should setup the recovery key", { tag: ["@screenshot", "@no-webkit"] }, async ({ page, app, util }) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
await util.removeSecretStorageDefaultKeyId();
|
||||
|
||||
// The key backup is deleted and the user needs to set it up
|
||||
const dialog = await util.openEncryptionTab();
|
||||
const setupButton = dialog.getByRole("button", { name: "Get recovery key" });
|
||||
await expect(setupButton).toBeVisible();
|
||||
await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("set-up-recovery.png");
|
||||
await setupButton.click();
|
||||
|
||||
// Display an informative panel about the recovery key
|
||||
await expect(dialog.getByRole("heading", { name: "Get recovery key" })).toBeVisible();
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-key-1-encryption-tab.png");
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Display the new recovery key and click on the copy button
|
||||
await expect(dialog.getByText("Save your recovery key somewhere safe")).toBeVisible();
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-key-2-encryption-tab.png", {
|
||||
mask: [dialog.getByTestId("recoveryKey")],
|
||||
});
|
||||
await dialog.getByRole("button", { name: "Copy" }).click();
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Confirm the recovery key
|
||||
await util.confirmRecoveryKey(
|
||||
"Enter your recovery key to confirm",
|
||||
"Finish set up",
|
||||
"set-up-key-3-encryption-tab.png",
|
||||
);
|
||||
|
||||
// The recovery key is now set up and the user can change it
|
||||
await expect(dialog.getByRole("button", { name: "Change recovery key" })).toBeVisible();
|
||||
|
||||
// Check that the current device is connected to key backup and the backup version is the expected one
|
||||
await checkDeviceIsConnectedKeyBackup(app, "1", true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Suguru Hirahara
|
||||
|
||||
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 { test, expect } from "../../element-web-test";
|
||||
|
||||
test.describe("General room settings tab", () => {
|
||||
const roomName = "Test Room";
|
||||
|
||||
test.use({
|
||||
displayName: "Hanako",
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ user, app }) => {
|
||||
await app.client.createRoom({ name: roomName });
|
||||
await app.viewRoomByName(roomName);
|
||||
});
|
||||
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, app, axe }) => {
|
||||
const settings = await app.settings.openRoomSettings("General");
|
||||
|
||||
// Assert that "Show less" details element is rendered
|
||||
await expect(settings.getByText("Show less")).toBeVisible();
|
||||
|
||||
await expect(settings).toMatchScreenshot("General-room-settings-tab-should-be-rendered-properly-1.png");
|
||||
|
||||
// Click the "Show less" details element
|
||||
await settings.getByText("Show less").click();
|
||||
|
||||
// Assert that "Show more" details element is rendered instead of "Show more"
|
||||
await expect(settings.getByText("Show less")).not.toBeVisible();
|
||||
await expect(settings.getByText("Show more")).toBeVisible();
|
||||
|
||||
axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("long address should not cause dialog to overflow", { tag: "@no-webkit" }, async ({ page, app, user }) => {
|
||||
const settings = await app.settings.openRoomSettings("General");
|
||||
// 1. Set the room-address to be a really long string
|
||||
const longString = "abcasdhjasjhdaj1jh1asdhasjdhajsdhjavhjksd".repeat(4);
|
||||
await settings.locator("#roomAliases input[label='Room address']").fill(longString);
|
||||
await expect(page.getByText("This address is available to use")).toBeVisible();
|
||||
await settings.locator("#roomAliases").getByText("Add", { exact: true }).click();
|
||||
|
||||
// 2. wait for the new setting to apply ...
|
||||
await expect(settings.locator("#canonicalAlias")).toHaveValue(`#${longString}:${user.homeServer}`);
|
||||
|
||||
// 3. Check if the dialog overflows
|
||||
const dialogBoundingBox = await page.locator(".mx_Dialog").boundingBox();
|
||||
const inputBoundingBox = await settings.locator("#canonicalAlias").boundingBox();
|
||||
// Assert that the width of the select element is less than that of .mx_Dialog div.
|
||||
expect(inputBoundingBox.width).toBeLessThan(dialogBoundingBox.width);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
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 { test, expect } from "../../../element-web-test";
|
||||
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||
|
||||
test.describe("Notifications 2 tab", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
test("should display notification settings", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
|
||||
await app.settings.setValue("feature_notification_settings2", null, SettingLevel.DEVICE, true);
|
||||
await page.setViewportSize({ width: 1024, height: 2000 });
|
||||
const settings = await app.settings.openUserSettings("Notifications");
|
||||
|
||||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await expect(settings).toMatchScreenshot("standard-notifications-2-settings.png", {
|
||||
// Mask the mxid.
|
||||
mask: [settings.locator("#mx_NotificationSettings2_MentionCheckbox span")],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
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 { test, expect } from "../../../element-web-test";
|
||||
|
||||
test.describe("Notifications tab", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
test("should display notification settings", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
|
||||
await page.setViewportSize({ width: 1024, height: 1400 });
|
||||
const settings = await app.settings.openUserSettings("Notifications");
|
||||
await settings.getByLabel("Enable notifications for this account").check();
|
||||
await settings.getByLabel("Enable notifications for this device").check();
|
||||
|
||||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await expect(settings).toMatchScreenshot("standard-notification-settings.png");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2023 Suguru Hirahara
|
||||
|
||||
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 { test, expect } from "../../element-web-test";
|
||||
|
||||
test.use({
|
||||
locale: "en-GB",
|
||||
timezoneId: "Europe/London",
|
||||
});
|
||||
|
||||
test.describe("Preferences user settings tab", () => {
|
||||
test.use({
|
||||
displayName: "Bob",
|
||||
uut: async ({ app, user }, use) => {
|
||||
const locator = await app.settings.openUserSettings("Preferences");
|
||||
await use(locator);
|
||||
},
|
||||
// display message preview settings
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user, axe }) => {
|
||||
await page.setViewportSize({ width: 1024, height: 4000 });
|
||||
const tab = await app.settings.openUserSettings("Preferences");
|
||||
// Assert that the top heading is rendered
|
||||
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
|
||||
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png", {
|
||||
// masked with fixed-width due to daylight saving time making the text content vary
|
||||
mask: [tab.locator("#mx_dropdownUserTimezone_value")],
|
||||
css: `
|
||||
#mx_dropdownUserTimezone_value {
|
||||
width: 200px;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("should be able to change the app language", { tag: ["@no-firefox", "@no-webkit"] }, async ({ uut, user }) => {
|
||||
// Check language and region setting dropdown
|
||||
const languageInput = uut.getByRole("button", { name: "Language Dropdown" });
|
||||
await languageInput.scrollIntoViewIfNeeded();
|
||||
// Check the default value
|
||||
await expect(languageInput.getByText("English")).toBeVisible();
|
||||
// Click the button to display the dropdown menu
|
||||
await languageInput.click();
|
||||
// Assert that the default option is rendered and highlighted
|
||||
languageInput.getByRole("option", { name: /Albanian/ });
|
||||
await expect(languageInput.getByRole("option", { name: /Albanian/ })).toHaveClass(
|
||||
/mx_Dropdown_option_highlight/,
|
||||
);
|
||||
await expect(languageInput.getByRole("option", { name: /Deutsch/ })).toBeVisible();
|
||||
// Click again to close the dropdown
|
||||
await languageInput.click();
|
||||
// Assert that the default value is rendered again
|
||||
await expect(languageInput.getByText("English")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should be able to change the timezone", async ({ uut, user }) => {
|
||||
// Check language and region setting dropdown
|
||||
const timezoneInput = uut.locator(".mx_dropdownUserTimezone");
|
||||
const timezoneValue = uut.locator("#mx_dropdownUserTimezone_value");
|
||||
await timezoneInput.scrollIntoViewIfNeeded();
|
||||
// Check the default value
|
||||
await expect(timezoneValue.getByText("Browser default")).toBeVisible();
|
||||
// Click the button to display the dropdown menu
|
||||
await timezoneInput.getByRole("button", { name: "Set timezone" }).click();
|
||||
// Select a different value
|
||||
await timezoneInput.getByRole("option", { name: /Africa\/Abidjan/ }).click();
|
||||
// Check the new value
|
||||
await expect(timezoneValue.getByText("Africa/Abidjan")).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
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 { test, expect } from "../../element-web-test";
|
||||
|
||||
test.describe("Quick settings menu", () => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user, axe }) => {
|
||||
await page.getByRole("button", { name: "Quick settings" }).click();
|
||||
// Assert that the top heading is renderedc
|
||||
const settings = page.getByTestId("quick-settings-menu");
|
||||
await expect(settings).toBeVisible();
|
||||
await expect(settings).toMatchScreenshot("quick-settings.png");
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 { type Locator } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../../element-web-test";
|
||||
|
||||
test.describe("Roles & Permissions room settings tab", () => {
|
||||
const roomName = "Test room";
|
||||
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
let settings: Locator;
|
||||
|
||||
test.beforeEach(async ({ user, app }) => {
|
||||
await app.client.createRoom({ name: roomName });
|
||||
await app.viewRoomByName(roomName);
|
||||
settings = await app.settings.openRoomSettings("Roles & Permissions");
|
||||
});
|
||||
|
||||
test("should be able to change the role of a user", async ({ page, app, user, axe }) => {
|
||||
const privilegedUserSection = settings.locator(".mx_SettingsFieldset").first();
|
||||
const applyButton = privilegedUserSection.getByRole("button", { name: "Apply" });
|
||||
|
||||
// Alice is admin (100) and the Apply button should be disabled
|
||||
await expect(applyButton).toBeDisabled();
|
||||
let combobox = privilegedUserSection.getByRole("combobox", { name: user.userId });
|
||||
await expect(combobox).toHaveValue("100");
|
||||
|
||||
// Change the role of Alice to Moderator (50)
|
||||
await combobox.selectOption("Moderator");
|
||||
|
||||
// Should display a modal to warn that we are demoting the only admin user
|
||||
const modal = await page.locator(".mx_Dialog", {
|
||||
hasText: "Warning",
|
||||
});
|
||||
await expect(modal).toBeVisible();
|
||||
// Click on the continue button in the modal
|
||||
await modal.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
const respPromise = page.waitForRequest("**/state/**");
|
||||
await applyButton.click();
|
||||
await respPromise;
|
||||
await expect(combobox).toHaveValue("50");
|
||||
|
||||
// Reload and check Alice is still Moderator (50)
|
||||
await page.reload();
|
||||
settings = await app.settings.openRoomSettings("Roles & Permissions");
|
||||
combobox = privilegedUserSection.getByRole("combobox", { name: user.userId });
|
||||
await expect(combobox).toHaveValue("50");
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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 { type Locator } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../../element-web-test";
|
||||
|
||||
test.describe("Roles & Permissions room settings tab", () => {
|
||||
const roomName = "Test room";
|
||||
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
let settings: Locator;
|
||||
|
||||
test.beforeEach(async ({ user, app }) => {
|
||||
await app.client.createRoom({
|
||||
name: roomName,
|
||||
power_level_content_override: {
|
||||
events: {
|
||||
// Set the join rules as lower than the history vis to test an edge case.
|
||||
["m.room.join_rules"]: 80,
|
||||
["m.room.history_visibility"]: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.viewRoomByName(roomName);
|
||||
settings = await app.settings.openRoomSettings("Security & Privacy");
|
||||
});
|
||||
|
||||
test(
|
||||
"should be able to toggle on encryption in a room",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, axe }) => {
|
||||
await page.setViewportSize({ width: 1024, height: 1400 });
|
||||
const encryptedToggle = settings.getByLabel("Encrypted");
|
||||
await encryptedToggle.click();
|
||||
|
||||
// Accept the dialog.
|
||||
await page.getByRole("button", { name: "Ok " }).click();
|
||||
|
||||
await expect(encryptedToggle).toBeChecked();
|
||||
await expect(encryptedToggle).toBeDisabled();
|
||||
|
||||
await settings.getByLabel("Only send messages to verified users.").check();
|
||||
|
||||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await expect(settings).toMatchScreenshot("room-security-settings.png");
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"should automatically adjust history visibility when a room is changed from public to private",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, axe }) => {
|
||||
await page.setViewportSize({ width: 1024, height: 1400 });
|
||||
|
||||
const settingsGroupAccess = page.getByRole("group", { name: "Access" });
|
||||
const settingsGroupHistory = page.getByRole("group", { name: "Who can read history?" });
|
||||
|
||||
await settingsGroupAccess.getByText("Anyone", { exact: true }).click();
|
||||
await settingsGroupHistory.getByText("Anyone").click();
|
||||
|
||||
// Test that we have the warning appear.
|
||||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await expect(settings).toMatchScreenshot("room-security-settings-world-readable.png");
|
||||
|
||||
await settingsGroupAccess.getByText("Invite only").click();
|
||||
// Element should have automatically set the room to "sharing" history visibility
|
||||
await expect(settingsGroupHistory.getByText("Members (full history)")).toBeChecked();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"should disallow changing from public to private if the user cannot alter history",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, bot }) => {
|
||||
await page.setViewportSize({ width: 1024, height: 1400 });
|
||||
|
||||
const settingsGroupAccess = page.getByRole("group", { name: "Access" });
|
||||
const settingsGroupHistory = page.getByRole("group", { name: "Who can read history?" });
|
||||
|
||||
await settingsGroupAccess.getByText("Anyone", { exact: true }).click();
|
||||
await settingsGroupHistory.getByText("Anyone").click();
|
||||
|
||||
// De-op ourselves
|
||||
await app.settings.switchTab("Roles & Permissions");
|
||||
|
||||
// Wait for the permissions list to be visible
|
||||
await expect(settings.getByRole("heading", { name: "Permissions" })).toBeVisible();
|
||||
|
||||
const ourComboBox = settings.getByRole("combobox", { name: user.userId });
|
||||
await ourComboBox.selectOption("Custom level");
|
||||
const ourPl = settings.getByRole("spinbutton", { name: user.userId });
|
||||
await ourPl.fill("80");
|
||||
await ourPl.blur(); // Shows a warning on
|
||||
|
||||
// Accept the de-op
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await settings.getByRole("button", { name: "Apply", disabled: false }).click();
|
||||
|
||||
await app.settings.switchTab("Security & Privacy");
|
||||
|
||||
await settingsGroupAccess.getByText("Invite only").click();
|
||||
// Element should have automatically set the room to "sharing" history visibility
|
||||
const errorDialog = page.getByRole("heading", { name: "Cannot make room private" });
|
||||
await expect(errorDialog).toBeVisible();
|
||||
await errorDialog.getByLabel("OK");
|
||||
await expect(settingsGroupHistory.getByText("Anyone")).toBeChecked();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 { type Locator } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../../element-web-test";
|
||||
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||
|
||||
test.describe("Voice & Video room settings tab", () => {
|
||||
const roomName = "Test room";
|
||||
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
let settings: Locator;
|
||||
|
||||
test.beforeEach(async ({ user, app, page }) => {
|
||||
// Execute client actions before setting, as the setting will force a reload.
|
||||
await app.client.createRoom({ name: roomName });
|
||||
await app.settings.setValue("feature_group_calls", null, SettingLevel.DEVICE, true);
|
||||
await app.viewRoomByName(roomName);
|
||||
settings = await app.settings.openRoomSettings("Voice & Video");
|
||||
});
|
||||
|
||||
test(
|
||||
"should be able to toggle on Element Call in the room",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, axe }) => {
|
||||
await page.setViewportSize({ width: 1024, height: 1400 });
|
||||
const callToggle = settings.getByLabel("Enable Element Call as an additional calling option in this room");
|
||||
await callToggle.check();
|
||||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await expect(settings).toMatchScreenshot("room-video-settings.png");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2023 Suguru Hirahara
|
||||
|
||||
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 { test, expect } from "../../element-web-test";
|
||||
|
||||
const IntegrationManager = "scalar.vector.im";
|
||||
|
||||
test.describe("Security user settings tab", () => {
|
||||
test.describe("with posthog enabled", () => {
|
||||
test.use({
|
||||
displayName: "Hanako",
|
||||
// Enable posthog
|
||||
config: {
|
||||
posthog: {
|
||||
project_api_key: "foo",
|
||||
api_host: "bar",
|
||||
},
|
||||
privacy_policy_url: "example.tld", // Set privacy policy URL to enable privacyPolicyLink
|
||||
},
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// Dismiss toasts
|
||||
await app.closeVerifyToast();
|
||||
await app.closeNotificationToast();
|
||||
await page.locator(".mx_Toast_buttons").getByRole("button", { name: "Yes" }).click(); // Allow analytics
|
||||
});
|
||||
|
||||
test.describe("AnalyticsLearnMoreDialog", () => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user, axe }) => {
|
||||
const tab = await app.settings.openUserSettings("Security");
|
||||
await tab.getByRole("button", { name: "Learn more" }).click();
|
||||
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot(
|
||||
"Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1.png",
|
||||
);
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
|
||||
test("should render the security tab", { tag: "@screenshot" }, async ({ app, page, user }) => {
|
||||
await page.setViewportSize({ width: 1024, height: 1400 });
|
||||
const tab = await app.settings.openUserSettings("Security");
|
||||
await expect(tab).toMatchScreenshot("security-settings-tab.png", {
|
||||
mask: [
|
||||
// Contains IM name.
|
||||
tab.locator("#mx_SetIntegrationManager_BodyText"),
|
||||
tab.locator("#mx_SetIntegrationManager_ManagerName"),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("should be able to set an ID server", async ({ app, context, user, page }) => {
|
||||
const tab = await app.settings.openUserSettings("Security");
|
||||
|
||||
await context.route("https://identity.example.org/_matrix/identity/v2", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
json: {},
|
||||
});
|
||||
});
|
||||
await context.route("https://identity.example.org/_matrix/identity/v2/account/register", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
json: {
|
||||
token: "AToken",
|
||||
},
|
||||
});
|
||||
});
|
||||
await context.route("https://identity.example.org/_matrix/identity/v2/account", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
json: {
|
||||
user_id: user.userId,
|
||||
},
|
||||
});
|
||||
});
|
||||
await context.route("https://identity.example.org/_matrix/identity/v2/terms", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
json: {
|
||||
policies: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
const setIdServer = tab.locator(".mx_IdentityServerPicker");
|
||||
await setIdServer.scrollIntoViewIfNeeded();
|
||||
|
||||
const textElement = setIdServer.getByRole("textbox", { name: "Enter a new identity server" });
|
||||
await textElement.click();
|
||||
await textElement.fill("https://identity.example.org");
|
||||
await setIdServer.getByRole("button", { name: "Change" }).click();
|
||||
|
||||
await expect(setIdServer.getByText("Checking server")).toBeVisible();
|
||||
// Accept terms
|
||||
await page.getByTestId("dialog-primary-button").click();
|
||||
// Check identity has changed.
|
||||
await expect(setIdServer.getByText("Your identity server has been changed")).toBeVisible();
|
||||
// Ensure section title is updated.
|
||||
await expect(tab.getByText(`Identity server (identity.example.org)`, { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show integrations as enabled", async ({ app, page, user }) => {
|
||||
const tab = await app.settings.openUserSettings("Security");
|
||||
|
||||
const setIntegrationManager = tab.locator(".mx_SetIntegrationManager");
|
||||
await setIntegrationManager.scrollIntoViewIfNeeded();
|
||||
await expect(
|
||||
setIntegrationManager.locator(".mx_SetIntegrationManager_heading_manager", {
|
||||
hasText: IntegrationManager,
|
||||
}),
|
||||
).toBeVisible();
|
||||
// Make sure integration manager's toggle switch is enabled
|
||||
const toggleswitch = setIntegrationManager.getByLabel("Enable the integration manager");
|
||||
await expect(toggleswitch).toBeVisible();
|
||||
await expect(toggleswitch).toBeChecked();
|
||||
await expect(setIntegrationManager.locator(".mx_SetIntegrationManager_heading_manager")).toHaveText(
|
||||
"Manage integrations(scalar.vector.im)",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user