Migrate settings/* from Cypress to Playwright (#11949)

* Migrate location.spec.ts from Cypress to Playwright

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

* Migrate location.spec.ts from Cypress to Playwright

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

* Migrate appearance-user-settings-tab.spec.ts from Cypress to Playwright

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

* Migrate device-management.spec.ts from Cypress to Playwright

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

* Migrate general-room-settings-tab.spec.ts from Cypress to Playwright

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

* Migrate general-user-settings-tab.spec.ts from Cypress to Playwright

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

* Migrate preferences-user-settings-tab.spec.ts from Cypress to Playwright

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

* Migrate security-user-settings-tab.spec.ts from Cypress to Playwright

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

* Add screenshots

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

* Add screenshot

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

* Deflake

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

* Update screenshots

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

* Update screenshots

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

* Move settings into subclass

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2023-11-29 13:50:13 +00:00 committed by GitHub
parent 8b7f49e74e
commit 1b3f473c10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 861 additions and 961 deletions

View file

@ -1,304 +0,0 @@
/*
Copyright 2023 Suguru Hirahara
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/// <reference types="cypress" />
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { SettingLevel } from "../../../src/settings/SettingLevel";
describe("Appearance user settings tab", () => {
let homeserver: HomeserverInstance;
beforeEach(() => {
cy.startHomeserver("default").then((data) => {
homeserver = data;
cy.initTestUser(homeserver, "Hanako");
});
});
afterEach(() => {
cy.stopHomeserver(homeserver);
});
it("should be rendered properly", () => {
cy.openUserSettings("Appearance");
cy.findByTestId("mx_AppearanceUserSettingsTab").within(() => {
cy.get("h2").should("have.text", "Customise your appearance").should("be.visible");
});
cy.findByTestId("mx_AppearanceUserSettingsTab").percySnapshotElement(
"User settings tab - Appearance (advanced options collapsed)",
{
// Emulate TabbedView's actual min and max widths
// 580: '.mx_UserSettingsDialog .mx_TabbedView' min-width
// 796: 1036 (mx_TabbedView_tabsOnLeft actual width) - 240 (mx_TabbedView_tabPanel margin-right)
widths: [580, 796],
},
);
// Click "Show advanced" link button
cy.findByRole("button", { name: "Show advanced" }).click();
// Assert that "Hide advanced" link button is rendered
cy.findByRole("button", { name: "Hide advanced" }).should("exist");
cy.findByTestId("mx_AppearanceUserSettingsTab").percySnapshotElement(
"User settings tab - Appearance (advanced options expanded)",
{
// Emulate TabbedView's actual min and max widths
// 580: '.mx_UserSettingsDialog .mx_TabbedView' min-width
// 796: 1036 (mx_TabbedView_tabsOnLeft actual width) - 240 (mx_TabbedView_tabPanel margin-right)
widths: [580, 796],
},
);
});
it("should support switching layouts", () => {
// Create and view a room first
cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
cy.openUserSettings("Appearance");
cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
// Assert that the layout selected by default is "Modern"
cy.get(".mx_LayoutSwitcher_RadioButton_selected .mx_StyledRadioButton_enabled").within(() => {
cy.findByLabelText("Modern").should("exist");
});
});
// Assert that the room layout is set to group (modern) layout
cy.get(".mx_RoomView_body[data-layout='group']").should("exist");
cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
// Select the first layout
cy.get(".mx_LayoutSwitcher_RadioButton").first().click();
// Assert that the layout selected is "IRC (Experimental)"
cy.get(".mx_LayoutSwitcher_RadioButton_selected .mx_StyledRadioButton_enabled").within(() => {
cy.findByLabelText("IRC (Experimental)").should("exist");
});
});
// Assert that the room layout is set to IRC layout
cy.get(".mx_RoomView_body[data-layout='irc']").should("exist");
cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
// Select the last layout
cy.get(".mx_LayoutSwitcher_RadioButton").last().click();
// Assert that the layout selected is "Message bubbles"
cy.get(".mx_LayoutSwitcher_RadioButton_selected .mx_StyledRadioButton_enabled").within(() => {
cy.findByLabelText("Message bubbles").should("exist");
});
});
// Assert that the room layout is set to bubble layout
cy.get(".mx_RoomView_body[data-layout='bubble']").should("exist");
});
it("should support changing font size by clicking the font slider", () => {
cy.openUserSettings("Appearance");
cy.findByTestId("mx_AppearanceUserSettingsTab").within(() => {
cy.get(".mx_FontScalingPanel_fontSlider").within(() => {
cy.findByLabelText("Font size").should("exist");
});
cy.get(".mx_FontScalingPanel_fontSlider").within(() => {
// Click the left position of the slider
cy.get("input").realClick({ position: "left" });
const MIN_FONT_SIZE = 11;
// Assert that the smallest font size is selected
cy.get(`input[value='${MIN_FONT_SIZE}']`).should("exist");
cy.get("output .mx_Slider_selection_label").findByText(MIN_FONT_SIZE);
});
cy.get(".mx_FontScalingPanel_fontSlider").percySnapshotElement("Font size slider - smallest (13)", {
widths: [486], // actual size (content-box, including inline padding)
});
cy.get(".mx_FontScalingPanel_fontSlider").within(() => {
// Click the right position of the slider
cy.get("input").realClick({ position: "right" });
const MAX_FONT_SIZE = 21;
// Assert that the largest font size is selected
cy.get(`input[value='${MAX_FONT_SIZE}']`).should("exist");
cy.get("output .mx_Slider_selection_label").findByText(MAX_FONT_SIZE);
});
cy.get(".mx_FontScalingPanel_fontSlider").percySnapshotElement("Font size slider - largest (21)", {
widths: [486],
});
});
});
it("should disable font size slider when custom font size is used", () => {
cy.openUserSettings("Appearance");
cy.findByTestId("mx_FontScalingPanel").within(() => {
cy.findByLabelText("Use custom size").click({ force: true }); // force click as checkbox size is zero
// Assert that the font slider is disabled
cy.get(".mx_FontScalingPanel_fontSlider input[disabled]").should("exist");
});
});
it("should support enabling compact group (modern) layout", () => {
// Create and view a room first
cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
cy.openUserSettings("Appearance");
// Click "Show advanced" link button
cy.findByRole("button", { name: "Show advanced" }).click();
// force click as checkbox size is zero
cy.findByLabelText("Use a more compact 'Modern' layout").click({ force: true });
// Assert that the room layout is set to compact group (modern) layout
cy.get("#matrixchat .mx_MatrixChat_wrapper.mx_MatrixChat_useCompactLayout").should("exist");
});
it("should disable compact group (modern) layout option on IRC layout and bubble layout", () => {
const checkDisabled = () => {
cy.findByLabelText("Use a more compact 'Modern' layout").should("be.disabled");
};
cy.openUserSettings("Appearance");
// Click "Show advanced" link button
cy.findByRole("button", { name: "Show advanced" }).click();
// Enable IRC layout
cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
// Select the first layout
cy.get(".mx_LayoutSwitcher_RadioButton").first().click();
// Assert that the layout selected is "IRC (Experimental)"
cy.get(".mx_LayoutSwitcher_RadioButton_selected .mx_StyledRadioButton_enabled").within(() => {
cy.findByLabelText("IRC (Experimental)").should("exist");
});
});
checkDisabled();
// Enable bubble layout
cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
// Select the first layout
cy.get(".mx_LayoutSwitcher_RadioButton").last().click();
// Assert that the layout selected is "IRC (Experimental)"
cy.get(".mx_LayoutSwitcher_RadioButton_selected .mx_StyledRadioButton_enabled").within(() => {
cy.findByLabelText("Message bubbles").should("exist");
});
});
checkDisabled();
});
it("should support enabling system font", () => {
cy.openUserSettings("Appearance");
// Click "Show advanced" link button
cy.findByRole("button", { name: "Show advanced" }).click();
// force click as checkbox size is zero
cy.findByLabelText("Use bundled emoji font").click({ force: true });
cy.findByLabelText("Use a system font").click({ force: true });
// Assert that the font-family value was removed
cy.get("body").should("have.css", "font-family", '""');
});
describe("Theme Choice Panel", () => {
beforeEach(() => {
// Disable the default theme for consistency in case ThemeWatcher automatically chooses it
cy.setSettingValue("use_system_theme", null, SettingLevel.DEVICE, false);
});
it("should be rendered with the light theme selected", () => {
cy.openUserSettings("Appearance")
.findByTestId("mx_ThemeChoicePanel")
.within(() => {
cy.findByTestId("checkbox-use-system-theme").within(() => {
cy.findByText("Match system theme").should("be.visible");
// Assert that 'Match system theme' is not checked
// Note that mx_Checkbox_checkmark exists and is hidden by CSS if it is not checked
cy.get(".mx_Checkbox_checkmark").should("not.be.visible");
});
cy.findByTestId("theme-choice-panel-selectors").within(() => {
cy.get(".mx_ThemeSelector_light").should("exist");
cy.get(".mx_ThemeSelector_dark").should("exist");
// Assert that the light theme is selected
cy.get(".mx_ThemeSelector_light.mx_StyledRadioButton_enabled").should("exist");
// Assert that the buttons for the light and dark theme are not enabled
cy.get(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled").should("not.exist");
cy.get(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled").should("not.exist");
});
// Assert that the checkbox for the high contrast theme is rendered
cy.findByLabelText("Use high contrast").should("exist");
});
});
it(
"should disable the labels for themes and the checkbox for the high contrast theme if the checkbox for " +
"the system theme is clicked",
() => {
cy.openUserSettings("Appearance")
.findByTestId("mx_ThemeChoicePanel")
.findByLabelText("Match system theme")
.click({ force: true }); // force click because the size of the checkbox is zero
cy.findByTestId("mx_ThemeChoicePanel").within(() => {
// Assert that the labels for the light theme and dark theme are disabled
cy.get(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled").should("exist");
cy.get(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled").should("exist");
// Assert that there does not exist a label for an enabled theme
cy.get("label.mx_StyledRadioButton_enabled").should("not.exist");
// Assert that the checkbox and label to enable the the high contrast theme should not exist
cy.findByLabelText("Use high contrast").should("not.exist");
});
},
);
it(
"should not render the checkbox and the label for the high contrast theme " +
"if the dark theme is selected",
() => {
cy.openUserSettings("Appearance");
// Assert that the checkbox and the label to enable the high contrast theme should exist
cy.findByLabelText("Use high contrast").should("exist");
// Enable the dark theme
cy.get(".mx_ThemeSelector_dark").click();
// Assert that the checkbox and the label should not exist
cy.findByLabelText("Use high contrast").should("not.exist");
},
);
});
});

View file

@ -1,140 +0,0 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/// <reference types="cypress" />
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import type { UserCredentials } from "../../support/login";
describe("Device manager", () => {
let homeserver: HomeserverInstance | undefined;
let user: UserCredentials | undefined;
beforeEach(() => {
cy.startHomeserver("default").then((data) => {
homeserver = data;
cy.initTestUser(homeserver, "Alice")
.then((credentials) => {
user = credentials;
})
.then(() => {
// create some extra sessions to manage
return cy.loginUser(homeserver, user.username, user.password);
})
.then(() => {
return cy.loginUser(homeserver, user.username, user.password);
});
});
});
afterEach(() => {
cy.stopHomeserver(homeserver!);
});
it("should display sessions", () => {
cy.openUserSettings("Sessions");
cy.findByText("Current session").should("exist");
cy.findByTestId("current-session-section").within(() => {
cy.findByText("Unverified session").should("exist");
// current session details opened
cy.findByRole("button", { name: "Show details" }).click();
cy.findByText("Session details").should("exist");
// close current session details
cy.findByRole("button", { name: "Hide details" }).click();
cy.findByText("Session details").should("not.exist");
});
cy.findByTestId("security-recommendations-section").within(() => {
cy.findByText("Security recommendations").should("exist");
cy.findByRole("button", { name: "View all (3)" }).click();
});
/**
* Other sessions section
*/
cy.findByText("Other sessions").should("exist");
// filter applied after clicking through from security recommendations
cy.findByLabelText("Filter devices").should("have.text", "Show: Unverified");
cy.get(".mx_FilteredDeviceList_list").within(() => {
cy.get(".mx_FilteredDeviceList_listItem").should("have.length", 3);
// select two sessions
cy.get(".mx_FilteredDeviceList_listItem")
.first()
.within(() => {
// force click as the input element itself is not visible (its size is zero)
cy.findByRole("checkbox").click({ force: true });
});
cy.get(".mx_FilteredDeviceList_listItem")
.last()
.within(() => {
// force click as the input element itself is not visible (its size is zero)
cy.findByRole("checkbox").click({ force: true });
});
});
// sign out from list selection action buttons
cy.findByRole("button", { name: "Sign out" }).click();
cy.get(".mx_Dialog .mx_QuestionDialog").within(() => {
cy.findByRole("button", { name: "Sign out" }).click();
});
// list updated after sign out
cy.get(".mx_FilteredDeviceList_list").find(".mx_FilteredDeviceList_listItem").should("have.length", 1);
// security recommendation count updated
cy.findByRole("button", { name: "View all (1)" });
const sessionName = `Alice's device`;
// open the first session
cy.get(".mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem")
.first()
.within(() => {
cy.findByRole("button", { name: "Show details" }).click();
cy.findByText("Session details").should("exist");
cy.findByRole("button", { name: "Rename" }).click();
cy.findByTestId("device-rename-input").type(sessionName);
cy.findByRole("button", { name: "Save" }).click();
// there should be a spinner while device updates
cy.get(".mx_Spinner").should("exist");
// wait for spinner to complete
cy.get(".mx_Spinner").should("not.exist");
// session name updated in details
cy.get(".mx_DeviceDetailHeading h4").within(() => {
cy.findByText(sessionName);
});
// and main list item
cy.get(".mx_DeviceTile h4").within(() => {
cy.findByText(sessionName);
});
// sign out using the device details sign out
cy.findByRole("button", { name: "Sign out of this session" }).click();
});
// confirm the signout
cy.get(".mx_Dialog .mx_QuestionDialog").within(() => {
cy.findByRole("button", { name: "Sign out" }).click();
});
// no other sessions or security recommendations sections when only one session
cy.findByText("Other sessions").should("not.exist");
cy.findByTestId("security-recommendations-section").should("not.exist");
});
});

View file

@ -1,97 +0,0 @@
/*
Copyright 2023 Suguru Hirahara
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/// <reference types="cypress" />
import { HomeserverInstance } from "../../plugins/utils/homeserver";
describe("General room settings tab", () => {
let homeserver: HomeserverInstance;
const roomName = "Test Room";
beforeEach(() => {
cy.startHomeserver("default").then((data) => {
homeserver = data;
cy.initTestUser(homeserver, "Hanako");
cy.createRoom({ name: roomName }).viewRoomByName(roomName);
});
});
afterEach(() => {
cy.stopHomeserver(homeserver);
});
it("should be rendered properly", () => {
cy.openRoomSettings("General");
// Assert that "Show less" details element is rendered
cy.findByText("Show less").should("exist");
cy.findByTestId("General").percySnapshotElement(
"Room settings tab - General (Local addresses details area expanded)",
{
// Emulate TabbedView's actual min and max widths
// 580: '.mx_UserSettingsDialog .mx_TabbedView' min-width
// 796: 1036 (mx_TabbedView_tabsOnLeft actual width) - 240 (mx_TabbedView_tabPanel margin-right)
widths: [580, 796],
},
);
// Click the "Show less" details element
cy.findByText("Show less").click();
// Assert that "Show more" details element is rendered instead of "Show more"
cy.findByText("Show less").should("not.exist");
cy.findByText("Show more").should("exist");
cy.findByTestId("General").percySnapshotElement(
"Room settings tab - General (Local addresses details area collapsed)",
{
// Emulate TabbedView's actual min and max widths
// 580: '.mx_UserSettingsDialog .mx_TabbedView' min-width
// 796: 1036 (mx_TabbedView_tabsOnLeft actual width) - 240 (mx_TabbedView_tabPanel margin-right)
widths: [580, 796],
},
);
});
it("long address should not cause dialog to overflow", () => {
cy.openRoomSettings("General");
// 1. Set the room-address to be a really long string
const longString =
"abcasdhjasjhdaj1jh1asdhasjdhajsdhjavhjksdnfjasdhfjh21jh3j12h3jashfcjbabbabasdbdasjh1j23hk1l2j3lamajshdjkltyiuwioeuqpirjdfmngsdnf8378234jskdfjkdfnbnsdfbasjbdjashdajshfgngnsdkfsdkkqwijeqiwjeiqhrkldfnaskldklasdn";
cy.get("#roomAliases").within(() => {
cy.get("input[label='Room address']").type(longString);
cy.contains("Add").click();
});
// 2. wait for the new setting to apply ...
cy.get("#canonicalAlias").should("have.value", `#${longString}:localhost`);
// 3. Check if the dialog overflows
cy.get(".mx_Dialog")
.invoke("outerWidth")
.then((dialogWidth) => {
cy.get("#canonicalAlias")
.invoke("outerWidth")
.then((fieldWidth) => {
// Assert that the width of the select element is less than that of .mx_Dialog div.
expect(fieldWidth).to.be.lessThan(dialogWidth);
});
});
});
});

View file

@ -1,238 +0,0 @@
/*
Copyright 2023 Suguru Hirahara
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/// <reference types="cypress" />
import { HomeserverInstance } from "../../plugins/utils/homeserver";
const USER_NAME = "Bob";
const USER_NAME_NEW = "Alice";
const IntegrationManager = "scalar.vector.im";
describe("General user settings tab", () => {
let homeserver: HomeserverInstance;
let userId: string;
beforeEach(() => {
cy.startHomeserver("default").then((data) => {
homeserver = data;
cy.initTestUser(homeserver, USER_NAME).then((user) => (userId = user.userId));
cy.tweakConfig({ default_country_code: "US" }); // For checking the international country calling code
});
cy.openUserSettings("General");
});
afterEach(() => {
cy.stopHomeserver(homeserver);
});
it("should be rendered properly", () => {
// Exclude userId from snapshots
const percyCSS = ".mx_ProfileSettings_profile_controls_userId { visibility: hidden !important; }";
cy.findByTestId("mx_GeneralUserSettingsTab").percySnapshotElement("User settings tab - General", {
percyCSS,
// Emulate TabbedView's actual min and max widths
// 580: '.mx_UserSettingsDialog .mx_TabbedView' min-width
// 796: 1036 (mx_TabbedView_tabsOnLeft actual width) - 240 (mx_TabbedView_tabPanel margin-right)
widths: [580, 796],
});
cy.findByTestId("mx_GeneralUserSettingsTab").within(() => {
// Assert that the top heading is rendered
cy.findByText("General").should("be.visible");
cy.get(".mx_ProfileSettings_profile")
.scrollIntoView()
.within(() => {
// Assert USER_NAME is rendered
cy.findByRole("textbox", { name: "Display Name" })
.get(`input[value='${USER_NAME}']`)
.should("be.visible");
// Assert that a userId is rendered
cy.get(".mx_ProfileSettings_profile_controls_userId").within(() => {
cy.findByText(userId).should("exist");
});
// Check avatar setting
cy.get(".mx_AvatarSetting_avatar")
.should("exist")
.realHover()
.get(".mx_AvatarSetting_avatar_hovering")
.within(() => {
// Hover effect
cy.get(".mx_AvatarSetting_hoverBg").should("exist");
cy.get(".mx_AvatarSetting_hover span").within(() => {
cy.findByText("Upload").should("exist");
});
});
});
// Wait until spinners disappear
cy.findByTestId("accountSection").within(() => {
cy.get(".mx_Spinner").should("not.exist");
});
cy.findByTestId("discoverySection").within(() => {
cy.get(".mx_Spinner").should("not.exist");
});
cy.findByTestId("accountSection").within(() => {
// Assert that input areas for changing a password exists
cy.get("form.mx_GeneralUserSettingsTab_section--account_changePassword")
.scrollIntoView()
.within(() => {
cy.findByLabelText("Current password").should("be.visible");
cy.findByLabelText("New Password").should("be.visible");
cy.findByLabelText("Confirm password").should("be.visible");
});
});
// Check email addresses area
cy.findByTestId("mx_AccountEmailAddresses")
.scrollIntoView()
.within(() => {
// Assert that an input area for a new email address is rendered
cy.findByRole("textbox", { name: "Email Address" }).should("be.visible");
// Assert the add button is visible
cy.findByRole("button", { name: "Add" }).should("be.visible");
});
// Check phone numbers area
cy.findByTestId("mx_AccountPhoneNumbers")
.scrollIntoView()
.within(() => {
// Assert that an input area for a new phone number is rendered
cy.findByRole("textbox", { name: "Phone Number" }).should("be.visible");
// Assert that the add button is rendered
cy.findByRole("button", { name: "Add" }).should("be.visible");
});
// Check language and region setting dropdown
cy.get(".mx_GeneralUserSettingsTab_section_languageInput")
.scrollIntoView()
.within(() => {
// Check the default value
cy.findByText("English").should("be.visible");
// Click the button to display the dropdown menu
cy.findByRole("button", { name: "Language Dropdown" }).click();
// Assert that the default option is rendered and highlighted
cy.findByRole("option", { name: /Albanian/ })
.should("be.visible")
.should("have.class", "mx_Dropdown_option_highlight");
cy.findByRole("option", { name: /Deutsch/ }).should("be.visible");
// Click again to close the dropdown
cy.findByRole("button", { name: "Language Dropdown" }).click();
// Assert that the default value is rendered again
cy.findByText("English").should("be.visible");
});
cy.get("form.mx_SetIdServer")
.scrollIntoView()
.within(() => {
// Assert that an input area for identity server exists
cy.findByRole("textbox", { name: "Enter a new identity server" }).should("be.visible");
});
cy.get(".mx_SetIntegrationManager")
.scrollIntoView()
.within(() => {
cy.contains(".mx_SetIntegrationManager_heading_manager", IntegrationManager).should("be.visible");
// Make sure integration manager's toggle switch is enabled
cy.get(".mx_ToggleSwitch_enabled").should("be.visible");
cy.get(".mx_SetIntegrationManager_heading_manager").should(
"have.text",
"Manage integrations(scalar.vector.im)",
);
});
// Assert the account deactivation button is displayed
cy.findByTestId("account-management-section")
.scrollIntoView()
.findByRole("button", { name: "Deactivate Account" })
.should("be.visible")
.should("have.class", "mx_AccessibleButton_kind_danger");
});
});
it("should support adding and removing a profile picture", () => {
cy.get(".mx_SettingsTab .mx_ProfileSettings").within(() => {
// Upload a picture
cy.get(".mx_ProfileSettings_avatarUpload").selectFile("cypress/fixtures/riot.png", { force: true });
// Find and click "Remove" link button
cy.get(".mx_ProfileSettings_profile").within(() => {
cy.findByRole("button", { name: "Remove" }).click();
});
// Assert that the link button disappeared
cy.get(".mx_AvatarSetting_avatar .mx_AccessibleButton_kind_link_sm").should("not.exist");
});
});
it("should set a country calling code based on default_country_code", () => {
// Check phone numbers area
cy.findByTestId("mx_AccountPhoneNumbers")
.scrollIntoView()
.within(() => {
// Assert that an input area for a new phone number is rendered
cy.findByRole("textbox", { name: "Phone Number" }).should("be.visible");
// Check a new phone number dropdown menu
cy.get(".mx_PhoneNumbers_country")
.scrollIntoView()
.within(() => {
// Assert that the country calling code of United States is visible
cy.findByText(/\+1/).should("be.visible");
// Click the button to display the dropdown menu
cy.findByRole("button", { name: "Country Dropdown" }).click();
// Assert that the option for calling code of United Kingdom is visible
cy.findByRole("option", { name: /United Kingdom/ }).should("be.visible");
// Click again to close the dropdown
cy.findByRole("button", { name: "Country Dropdown" }).click();
// Assert that the default value is rendered again
cy.findByText(/\+1/).should("be.visible");
});
cy.findByRole("button", { name: "Add" }).should("be.visible");
});
});
it("should support changing a display name", () => {
cy.get(".mx_SettingsTab .mx_ProfileSettings").within(() => {
// Change the diaplay name to USER_NAME_NEW
cy.findByRole("textbox", { name: "Display Name" }).type(`{selectAll}{del}${USER_NAME_NEW}{enter}`);
});
cy.closeDialog();
// Assert the avatar's initial characters are set
cy.get(".mx_UserMenu .mx_BaseAvatar").findByText("A").should("exist"); // Alice
cy.get(".mx_RoomView_wrapper .mx_BaseAvatar").findByText("A").should("exist"); // Alice
});
});

View file

@ -1,50 +0,0 @@
/*
Copyright 2023 Suguru Hirahara
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/// <reference types="cypress" />
import { HomeserverInstance } from "../../plugins/utils/homeserver";
describe("Preferences user settings tab", () => {
let homeserver: HomeserverInstance;
beforeEach(() => {
cy.startHomeserver("default").then((data) => {
homeserver = data;
cy.initTestUser(homeserver, "Bob");
});
});
afterEach(() => {
cy.stopHomeserver(homeserver);
});
it("should be rendered properly", () => {
cy.openUserSettings("Preferences");
cy.findByTestId("mx_PreferencesUserSettingsTab").within(() => {
// Assert that the top heading is rendered
cy.contains("Preferences").should("be.visible");
});
cy.findByTestId("mx_PreferencesUserSettingsTab").percySnapshotElement("User settings tab - Preferences", {
// Emulate TabbedView's actual min and max widths
// 580: '.mx_UserSettingsDialog .mx_TabbedView' min-width
// 796: 1036 (mx_TabbedView_tabsOnLeft actual width) - 240 (mx_TabbedView_tabPanel margin-right)
widths: [580, 796],
});
});
});

View file

@ -1,72 +0,0 @@
/*
Copyright 2023 Suguru Hirahara
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/// <reference types="cypress" />
import { HomeserverInstance } from "../../plugins/utils/homeserver";
describe("Security user settings tab", () => {
let homeserver: HomeserverInstance;
afterEach(() => {
cy.stopHomeserver(homeserver);
});
describe("with posthog enabled", () => {
beforeEach(() => {
// Enable posthog
cy.intercept("/config.json?cachebuster=*", (req) => {
req.continue((res) => {
res.send(200, {
...res.body,
posthog: {
project_api_key: "foo",
api_host: "bar",
},
privacy_policy_url: "example.tld", // Set privacy policy URL to enable privacyPolicyLink
});
});
});
cy.startHomeserver("default").then((data) => {
homeserver = data;
cy.initTestUser(homeserver, "Hanako");
});
// Hide "Notification" toast on Cypress Cloud
cy.contains(".mx_Toast_toast h2", "Notifications")
.should("exist")
.closest(".mx_Toast_toast")
.within(() => {
cy.findByRole("button", { name: "Dismiss" }).click();
});
cy.get(".mx_Toast_buttons").within(() => {
cy.findByRole("button", { name: "Yes" }).should("exist").click(); // Allow analytics
});
cy.openUserSettings("Security");
});
describe("AnalyticsLearnMoreDialog", () => {
it("should be rendered properly", () => {
cy.findByRole("button", { name: "Learn more" }).click();
cy.get(".mx_AnalyticsLearnMoreDialog_wrapper").percySnapshotElement("AnalyticsLearnMoreDialog");
});
});
});
});

View file

@ -2,5 +2,7 @@
set -e set -e
yarn link
yarn --cwd ../element-web install yarn --cwd ../element-web install
yarn --cwd ../element-web link matrix-react-sdk
npx playwright test --update-snapshots --reporter line --project='Legacy Crypto' $1 npx playwright test --update-snapshots --reporter line --project='Legacy Crypto' $1

View file

@ -216,7 +216,7 @@ test.describe("Editing", () => {
await app.closeDialog(); await app.closeDialog();
// Enable developer mode // Enable developer mode
await app.setSettingValue("developerMode", null, SettingLevel.ACCOUNT, true); await app.settings.setValue("developerMode", null, SettingLevel.ACCOUNT, true);
await clickEditedMessage(page, "Massage"); await clickEditedMessage(page, "Massage");

View file

@ -0,0 +1,251 @@
/*
Copyright 2023 Suguru Hirahara
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { test, expect } from "../../element-web-test";
import { SettingLevel } from "../../../src/settings/SettingLevel";
test.describe("Appearance user settings tab", () => {
test.use({
displayName: "Hanako",
});
test("should be rendered properly", async ({ page, user, app }) => {
const tab = await app.settings.openUserSettings("Appearance");
await expect(tab.getByRole("heading", { name: "Customise your appearance" })).toBeVisible();
// 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).toHaveScreenshot();
});
test("should support switching layouts", async ({ page, user, app }) => {
// Create and view a room first
await app.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room");
await app.settings.openUserSettings("Appearance");
const buttons = page.locator(".mx_LayoutSwitcher_RadioButton");
// Assert that the layout selected by default is "Modern"
await expect(
buttons.locator(".mx_StyledRadioButton_enabled", {
hasText: "Modern",
}),
).toBeVisible();
// Assert that the room layout is set to group (modern) layout
await expect(page.locator(".mx_RoomView_body[data-layout='group']")).toBeVisible();
// Select the first layout
await buttons.first().click();
// Assert that the layout selected is "IRC (Experimental)"
await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "IRC (Experimental)" })).toBeVisible();
// Assert that the room layout is set to IRC layout
await expect(page.locator(".mx_RoomView_body[data-layout='irc']")).toBeVisible();
// Select the last layout
await buttons.last().click();
// Assert that the layout selected is "Message bubbles"
await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "Message bubbles" })).toBeVisible();
// Assert that the room layout is set to bubble layout
await expect(page.locator(".mx_RoomView_body[data-layout='bubble']")).toBeVisible();
});
test("should support changing font size by clicking the font slider", async ({ page, app, user }) => {
await app.settings.openUserSettings("Appearance");
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
const fontSliderSection = tab.locator(".mx_FontScalingPanel_fontSlider");
await expect(fontSliderSection.getByLabel("Font size")).toBeVisible();
const slider = fontSliderSection.getByRole("slider");
// Click the left position of the slider
await slider.click({ position: { x: 0, y: 10 } });
const MIN_FONT_SIZE = 11;
// Assert that the smallest font size is selected
await expect(fontSliderSection.locator(`input[value='${MIN_FONT_SIZE}']`)).toBeVisible();
await expect(
fontSliderSection.locator("output .mx_Slider_selection_label", { hasText: String(MIN_FONT_SIZE) }),
).toBeVisible();
await expect(fontSliderSection).toHaveScreenshot(`font-slider-${MIN_FONT_SIZE}.png`);
// Click the right position of the slider
await slider.click({ position: { x: 572, y: 10 } });
const MAX_FONT_SIZE = 21;
// Assert that the largest font size is selected
await expect(fontSliderSection.locator(`input[value='${MAX_FONT_SIZE}']`)).toBeVisible();
await expect(
fontSliderSection.locator("output .mx_Slider_selection_label", { hasText: String(MAX_FONT_SIZE) }),
).toBeVisible();
await expect(fontSliderSection).toHaveScreenshot(`font-slider-${MAX_FONT_SIZE}.png`);
});
test("should disable font size slider when custom font size is used", async ({ page, app, user }) => {
await app.settings.openUserSettings("Appearance");
const panel = page.getByTestId("mx_FontScalingPanel");
await panel.locator("label", { hasText: "Use custom size" }).click();
// Assert that the font slider is disabled
await expect(panel.locator(".mx_FontScalingPanel_fontSlider input[disabled]")).toBeVisible();
});
test("should support enabling compact group (modern) layout", async ({ page, app, user }) => {
// Create and view a room first
await app.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room");
await app.settings.openUserSettings("Appearance");
// Click "Show advanced" link button
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
await tab.getByRole("button", { name: "Show advanced" }).click();
await tab.locator("label", { hasText: "Use a more compact 'Modern' layout" }).click();
// Assert that the room layout is set to compact group (modern) layout
await expect(page.locator("#matrixchat .mx_MatrixChat_wrapper.mx_MatrixChat_useCompactLayout")).toBeVisible();
});
test("should disable compact group (modern) layout option on IRC layout and bubble layout", async ({
page,
app,
user,
}) => {
await app.settings.openUserSettings("Appearance");
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
const checkDisabled = async () => {
await expect(tab.getByRole("checkbox", { name: "Use a more compact 'Modern' layout" })).toBeDisabled();
};
// Click "Show advanced" link button
await tab.getByRole("button", { name: "Show advanced" }).click();
const buttons = page.locator(".mx_LayoutSwitcher_RadioButton");
// Enable IRC layout
await buttons.first().click();
// Assert that the layout selected is "IRC (Experimental)"
await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "IRC (Experimental)" })).toBeVisible();
await checkDisabled();
// Enable bubble layout
await buttons.last().click();
// Assert that the layout selected is "IRC (Experimental)"
await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "Message bubbles" })).toBeVisible();
await checkDisabled();
});
test("should support enabling system font", async ({ page, app, user }) => {
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.locator(".mx_Checkbox", { hasText: "Use bundled emoji font" }).click();
await tab.locator(".mx_Checkbox", { hasText: "Use a system font" }).click();
// Assert that the font-family value was removed
await expect(page.locator("body")).toHaveCSS("font-family", '""');
});
test.describe("Theme Choice Panel", () => {
test.beforeEach(async ({ app, user }) => {
// Disable the default theme for consistency in case ThemeWatcher automatically chooses it
await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
});
test("should be rendered with the light theme selected", async ({ page, app }) => {
await app.settings.openUserSettings("Appearance");
const themePanel = page.getByTestId("mx_ThemeChoicePanel");
const useSystemTheme = themePanel.getByTestId("checkbox-use-system-theme");
await expect(useSystemTheme.getByText("Match system theme")).toBeVisible();
// Assert that 'Match system theme' is not checked
// Note that mx_Checkbox_checkmark exists and is hidden by CSS if it is not checked
await expect(useSystemTheme.locator(".mx_Checkbox_checkmark")).not.toBeVisible();
const selectors = themePanel.getByTestId("theme-choice-panel-selectors");
await expect(selectors.locator(".mx_ThemeSelector_light")).toBeVisible();
await expect(selectors.locator(".mx_ThemeSelector_dark")).toBeVisible();
// Assert that the light theme is selected
await expect(selectors.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_enabled")).toBeVisible();
// Assert that the buttons for the light and dark theme are not enabled
await expect(selectors.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled")).not.toBeVisible();
await expect(selectors.locator(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled")).not.toBeVisible();
// Assert that the checkbox for the high contrast theme is rendered
await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).toBeVisible();
});
test("should disable the labels for themes and the checkbox for the high contrast theme if the checkbox for the system theme is clicked", async ({
page,
app,
}) => {
await app.settings.openUserSettings("Appearance");
const themePanel = page.getByTestId("mx_ThemeChoicePanel");
await themePanel.locator(".mx_Checkbox", { hasText: "Match system theme" }).click();
// Assert that the labels for the light theme and dark theme are disabled
await expect(themePanel.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled")).toBeVisible();
await expect(themePanel.locator(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled")).toBeVisible();
// Assert that there does not exist a label for an enabled theme
await expect(themePanel.locator("label.mx_StyledRadioButton_enabled")).not.toBeVisible();
// Assert that the checkbox and label to enable the high contrast theme should not exist
await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).not.toBeVisible();
});
test("should not render the checkbox and the label for the high contrast theme if the dark theme is selected", async ({
page,
app,
}) => {
await app.settings.openUserSettings("Appearance");
const themePanel = page.getByTestId("mx_ThemeChoicePanel");
// Assert that the checkbox and the label to enable the high contrast theme should exist
await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).toBeVisible();
// Enable the dark theme
await themePanel.locator(".mx_ThemeSelector_dark").click();
// Assert that the checkbox and the label should not exist
await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).not.toBeVisible();
});
});
});

View file

@ -0,0 +1,105 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { test, expect } from "../../element-web-test";
test.describe("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 }) => {
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: "Sign out", 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 h4").getByText(sessionName)).toBeVisible();
// sign out using the device details sign out
await firstSession.getByRole("button", { name: "Sign out of 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();
});
});

View file

@ -0,0 +1,63 @@
/*
Copyright 2023 Suguru Hirahara
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { test, expect } from "../../element-web-test";
test.describe("General room settings tab", () => {
const roomName = "Test Room";
test.use({
displayName: "Hanako",
});
test.beforeEach(async ({ user, app }) => {
await app.createRoom({ name: roomName });
await app.viewRoomByName(roomName);
});
test("should be rendered properly", async ({ page, app }) => {
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).toHaveScreenshot();
// 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();
});
test("long address should not cause dialog to overflow", async ({ page, app }) => {
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 settings.locator("#roomAliases").getByText("Add", { exact: true }).click();
// 2. wait for the new setting to apply ...
await expect(settings.locator("#canonicalAlias")).toHaveValue(`#${longString}:localhost`);
// 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);
});
});

View file

@ -0,0 +1,189 @@
/*
Copyright 2023 Suguru Hirahara
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { test, expect } from "../../element-web-test";
const USER_NAME = "Bob";
const USER_NAME_NEW = "Alice";
const IntegrationManager = "scalar.vector.im";
test.describe("General user settings tab", () => {
let userId: string;
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("General");
await use(locator);
},
});
test("should be rendered properly", async ({ uut }) => {
await expect(uut).toHaveScreenshot("general.png", {
// Exclude userId from snapshots
mask: [uut.locator(".mx_ProfileSettings_profile_controls > p")],
});
// Assert that the top heading is rendered
await expect(uut.getByRole("heading", { name: "General" })).toBeVisible();
const profile = uut.locator(".mx_ProfileSettings_profile");
await profile.scrollIntoViewIfNeeded();
await expect(profile.getByRole("textbox", { name: "Display Name" })).toHaveValue(USER_NAME);
// Assert that a userId is rendered
await expect(profile.locator(".mx_ProfileSettings_profile_controls_userId", { hasText: userId })).toBeVisible();
// Check avatar setting
const avatar = profile.locator(".mx_AvatarSetting_avatar");
await avatar.hover();
// Hover effect
await expect(avatar.locator(".mx_AvatarSetting_hoverBg")).toBeVisible();
await expect(avatar.locator(".mx_AvatarSetting_hover span").getByText("Upload")).toBeVisible();
// 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");
// Assert that input areas for changing a password exists
const changePassword = accountSection.locator("form.mx_GeneralUserSettingsTab_section--account_changePassword");
await changePassword.scrollIntoViewIfNeeded();
await expect(changePassword.getByLabel("Current password")).toBeVisible();
await expect(changePassword.getByLabel("New Password")).toBeVisible();
await expect(changePassword.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();
// Check language and region setting dropdown
const languageInput = uut.locator(".mx_GeneralUserSettingsTab_section_languageInput");
await languageInput.scrollIntoViewIfNeeded();
// Check the default value
await expect(languageInput.getByText("English")).toBeVisible();
// Click the button to display the dropdown menu
await languageInput.getByRole("button", { name: "Language Dropdown" }).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.getByRole("button", { name: "Language Dropdown" }).click();
// Assert that the default value is rendered again
await expect(languageInput.getByText("English")).toBeVisible();
const setIdServer = uut.locator(".mx_SetIdServer");
await setIdServer.scrollIntoViewIfNeeded();
// Assert that an input area for identity server exists
await expect(setIdServer.getByRole("textbox", { name: "Enter a new identity server" })).toBeVisible();
const setIntegrationManager = uut.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
await expect(setIntegrationManager.locator(".mx_ToggleSwitch_enabled")).toBeVisible();
await expect(setIntegrationManager.locator(".mx_SetIntegrationManager_heading_manager")).toHaveText(
"Manage integrations(scalar.vector.im)",
);
// 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/,
);
});
test("should support adding and removing a profile picture", async ({ uut }) => {
const profileSettings = uut.locator(".mx_ProfileSettings");
// Upload a picture
await profileSettings
.locator(".mx_ProfileSettings_avatarUpload")
.setInputFiles("playwright/sample-files/riot.png");
// Find and click "Remove" link button
await profileSettings.locator(".mx_ProfileSettings_profile").getByRole("button", { name: "Remove" }).click();
// Assert that the link button disappeared
await expect(
profileSettings.locator(".mx_AvatarSetting_avatar .mx_AccessibleButton_kind_link_sm"),
).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 }) => {
// Change the diaplay name to USER_NAME_NEW
const displayNameInput = uut
.locator(".mx_SettingsTab .mx_ProfileSettings")
.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
await expect(page.locator(".mx_UserMenu .mx_BaseAvatar").getByText("A")).toBeVisible(); // Alice
await expect(page.locator(".mx_RoomView_wrapper .mx_BaseAvatar").getByText("A")).toBeVisible(); // Alice
});
});

View file

@ -0,0 +1,31 @@
/*
Copyright 2023 Suguru Hirahara
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { test, expect } from "../../element-web-test";
test.describe("Preferences user settings tab", () => {
test.use({
displayName: "Bob",
});
test("should be rendered properly", async ({ app, user }) => {
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).toHaveScreenshot();
});
});

View file

@ -0,0 +1,51 @@
/*
Copyright 2023 Suguru Hirahara
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { test, expect } from "../../element-web-test";
test.describe("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, user }) => {
// Dismiss "Notification" toast
await page
.locator(".mx_Toast_toast", { hasText: "Notifications" })
.getByRole("button", { name: "Dismiss" })
.click();
await page.locator(".mx_Toast_buttons").getByRole("button", { name: "Yes" }).click(); // Allow analytics
});
test.describe("AnalyticsLearnMoreDialog", () => {
test("should be rendered properly", async ({ app, page }) => {
const tab = await app.settings.openUserSettings("Security");
await tab.getByRole("button", { name: "Learn more" }).click();
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toHaveScreenshot();
});
});
});
});

View file

@ -36,7 +36,7 @@ test.describe("User Onboarding (new user)", () => {
test("page is shown and preference exists", async ({ page, app }) => { test("page is shown and preference exists", async ({ page, app }) => {
await expect(page.locator(".mx_UserOnboardingPage")).toHaveScreenshot(); await expect(page.locator(".mx_UserOnboardingPage")).toHaveScreenshot();
await app.openUserSettings("Preferences"); await app.settings.openUserSettings("Preferences");
await expect(page.getByText("Show shortcut to welcome checklist above the room list")).toBeVisible(); await expect(page.getByText("Show shortcut to welcome checklist above the room list")).toBeVisible();
}); });

View file

@ -30,7 +30,7 @@ test.describe("User Onboarding (old user)", () => {
test("page and preference are hidden", async ({ page, user, app }) => { test("page and preference are hidden", async ({ page, user, app }) => {
await expect(page.locator(".mx_UserOnboardingPage")).not.toBeVisible(); await expect(page.locator(".mx_UserOnboardingPage")).not.toBeVisible();
await expect(page.locator(".mx_UserOnboardingButton")).not.toBeVisible(); await expect(page.locator(".mx_UserOnboardingButton")).not.toBeVisible();
await app.openUserSettings("Preferences"); await app.settings.openUserSettings("Preferences");
await expect(page.getByText("Show shortcut to welcome checklist above the room list")).not.toBeVisible(); await expect(page.getByText("Show shortcut to welcome checklist above the room list")).not.toBeVisible();
}); });
}); });

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { test as base, expect } from "@playwright/test"; import { test as base, expect, Locator } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright"; import AxeBuilder from "@axe-core/playwright";
import _ from "lodash"; import _ from "lodash";
@ -66,6 +66,7 @@ export const test = base.extend<
crypto: Crypto; crypto: Crypto;
room?: { roomId: string }; room?: { roomId: string };
toasts: Toasts; toasts: Toasts;
uut?: Locator; // Unit Under Test, useful place to refer a prepared locator
} }
>({ >({
cryptoBackend: ["legacy", { option: true }], cryptoBackend: ["legacy", { option: true }],

View file

@ -17,70 +17,18 @@ limitations under the License.
import { type Locator, type Page } from "@playwright/test"; import { type Locator, type Page } from "@playwright/test";
import type { IContent, ICreateRoomOpts, ISendEventResponse } from "matrix-js-sdk/src/matrix"; import type { IContent, ICreateRoomOpts, ISendEventResponse } from "matrix-js-sdk/src/matrix";
import type { SettingLevel } from "../../src/settings/SettingLevel"; import { Settings } from "./settings";
export class ElementAppPage { export class ElementAppPage {
public constructor(private readonly page: Page) {} public constructor(private readonly page: Page) {}
/** public settings = new Settings(this.page);
* Sets the value for a setting. The room ID is optional if the
* setting is not being set for a particular room, otherwise it
* should be supplied. The value may be null to indicate that the
* level should no longer have an override.
* @param {string} settingName The name of the setting to change.
* @param {String} roomId The room ID to change the value in, may be
* null.
* @param {SettingLevel} level The level to change the value at.
* @param {*} value The new value of the setting, may be null.
* @return {Promise} Resolves when the setting has been changed.
*/
public async setSettingValue(settingName: string, roomId: string, level: SettingLevel, value: any): Promise<void> {
return this.page.evaluate<
Promise<void>,
{
settingName: string;
roomId: string | null;
level: SettingLevel;
value: any;
}
>(
({ settingName, roomId, level, value }) => {
return window.mxSettingsStore.setValue(settingName, roomId, level, value);
},
{ settingName, roomId, level, value },
);
}
/** /**
* Open the top left user menu, returning a Locator to the resulting context menu. * Open the top left user menu, returning a Locator to the resulting context menu.
*/ */
public async openUserMenu(): Promise<Locator> { public async openUserMenu(): Promise<Locator> {
await this.page.getByRole("button", { name: "User menu" }).click(); return this.settings.openUserMenu();
const locator = this.page.locator(".mx_ContextualMenu");
await locator.waitFor();
return locator;
}
/**
* Switch settings tab to the one by the given name
* @param tab the name of the tab to switch to.
*/
public async switchTab(tab: string): Promise<void> {
await this.page
.locator(".mx_TabbedView_tabLabels")
.locator(".mx_TabbedView_tabLabel", { hasText: tab })
.click();
}
/**
* Open user settings (via user menu), returns a locator to the dialog
* @param tab the name of the tab to switch to after opening, optional.
*/
public async openUserSettings(tab?: string): Promise<Locator> {
const locator = await this.openUserMenu();
await locator.getByRole("menuitem", { name: "All settings", exact: true }).click();
if (tab) await this.switchTab(tab);
return this.page.locator(".mx_UserSettingsDialog");
} }
/** /**
@ -96,7 +44,7 @@ export class ElementAppPage {
* Close dialog currently open dialog * Close dialog currently open dialog
*/ */
public async closeDialog(): Promise<void> { public async closeDialog(): Promise<void> {
return this.page.getByRole("button", { name: "Close dialog", exact: true }).click(); return this.settings.closeDialog();
} }
/** /**
@ -113,6 +61,34 @@ export class ElementAppPage {
}, options); }, options);
} }
/**
* Opens the given room by name. The room must be visible in the
* room list, but the room list may be folded horizontally, and the
* room may contain unread messages.
*
* @param name The exact room name to find and click on/open.
*/
public async viewRoomByName(name: string): Promise<void> {
// We look for the room inside the room list, which is a tree called Rooms.
//
// There are 3 cases:
// - the room list is folded:
// then the aria-label on the room tile is the name (with nothing extra)
// - the room list is unfolder and the room has messages:
// then the aria-label contains the unread count, but the title of the
// div inside the titleContainer equals the room name
// - the room list is unfolded and the room has no messages:
// then the aria-label is the name and so is the title of a div
//
// So by matching EITHER title=name OR aria-label=name we find this exact
// room in all three cases.
return this.page
.getByRole("tree", { name: "Rooms" })
.locator(`[title="${name}"],[aria-label="${name}"]`)
.first()
.click();
}
/** /**
* Get the composer element * Get the composer element
* @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer * @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer

View file

@ -0,0 +1,102 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Locator, Page } from "@playwright/test";
import type { SettingLevel } from "../../src/settings/SettingLevel";
export class Settings {
public constructor(private readonly page: Page) {}
/**
* Open the top left user menu, returning a Locator to the resulting context menu.
*/
public async openUserMenu(): Promise<Locator> {
await this.page.getByRole("button", { name: "User menu" }).click();
const locator = this.page.locator(".mx_ContextualMenu");
await locator.waitFor();
return locator;
}
/**
* Close dialog currently open dialog
*/
public async closeDialog(): Promise<void> {
return this.page.getByRole("button", { name: "Close dialog", exact: true }).click();
}
/**
* Sets the value for a setting. The room ID is optional if the
* setting is not being set for a particular room, otherwise it
* should be supplied. The value may be null to indicate that the
* level should no longer have an override.
* @param {string} settingName The name of the setting to change.
* @param {String} roomId The room ID to change the value in, may be
* null.
* @param {SettingLevel} level The level to change the value at.
* @param {*} value The new value of the setting, may be null.
* @return {Promise} Resolves when the setting has been changed.
*/
public async setValue(settingName: string, roomId: string, level: SettingLevel, value: any): Promise<void> {
return this.page.evaluate<
Promise<void>,
{
settingName: string;
roomId: string | null;
level: SettingLevel;
value: any;
}
>(
({ settingName, roomId, level, value }) => {
return window.mxSettingsStore.setValue(settingName, roomId, level, value);
},
{ settingName, roomId, level, value },
);
}
/**
* Switch settings tab to the one by the given name
* @param tab the name of the tab to switch to.
*/
public async switchTab(tab: string): Promise<void> {
await this.page
.locator(".mx_TabbedView_tabLabels")
.locator(".mx_TabbedView_tabLabel", { hasText: tab })
.click();
}
/**
* Open user settings (via user menu), returns a locator to the dialog
* @param tab the name of the tab to switch to after opening, optional.
*/
public async openUserSettings(tab?: string): Promise<Locator> {
const locator = await this.openUserMenu();
await locator.getByRole("menuitem", { name: "All settings", exact: true }).click();
if (tab) await this.switchTab(tab);
return this.page.locator(".mx_Dialog").filter({ has: this.page.locator(".mx_UserSettingsDialog") });
}
/**
* Open room settings (via room menu), returns a locator to the dialog
* @param tab the name of the tab to switch to after opening, optional.
*/
public async openRoomSettings(tab?: string): Promise<Locator> {
await this.page.getByRole("main").getByRole("button", { name: "Room options", exact: true }).click();
await this.page.locator(".mx_RoomTile_contextMenu").getByRole("menuitem", { name: "Settings" }).click();
if (tab) await this.switchTab(tab);
return this.page.locator(".mx_Dialog").filter({ has: this.page.locator(".mx_RoomSettingsDialog") });
}
}

View file

@ -31,6 +31,13 @@ export interface HomeserverInstance {
* @param displayName optional display name to set on the newly registered user * @param displayName optional display name to set on the newly registered user
*/ */
registerUser(username: string, password: string, displayName?: string): Promise<Credentials>; registerUser(username: string, password: string, displayName?: string): Promise<Credentials>;
/**
* Logs into synapse with the given username/password
* @param userId login username
* @param password login password
*/
loginUser(userId: string, password: string): Promise<Credentials>;
} }
export interface StartHomeserverOpts { export interface StartHomeserverOpts {

View file

@ -196,4 +196,27 @@ export class Synapse implements Homeserver, HomeserverInstance {
displayName, displayName,
}; };
} }
public async loginUser(userId: string, password: string): Promise<Credentials> {
const url = `${this.config.baseUrl}/_matrix/client/v3/login`;
const res = await this.request.post(url, {
data: {
type: "m.login.password",
identifier: {
type: "m.id.user",
user: userId,
},
password: password,
},
});
const json = await res.json();
return {
password,
accessToken: json.access_token,
userId: json.user_id,
deviceId: json.device_id,
homeServer: json.home_server,
};
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB