Use semantic headings in user settings - integrations and account deletion (#10837)

* allow testids in settings sections

* use semantic headings in LabsUserSettingsTab

* put back margin var

* use SettingsTab wrapper

* use semantic headings for deactivate acc section

* use semantic heading in manage integratios

* i18n

* explicit cast to boolean

* Update src/components/views/settings/shared/SettingsSubsection.tsx

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* test manage integration settings

* test deactivate account section display

* remove debug

* fix cypress test

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
Kerry 2023-05-17 19:52:44 +12:00 committed by GitHub
parent c3687489dd
commit 8cd84b0e7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 232 additions and 54 deletions

View file

@ -43,7 +43,7 @@ describe("General user settings tab", () => {
// Exclude userId from snapshots
const percyCSS = ".mx_ProfileSettings_profile_controls_userId { visibility: hidden !important; }";
cy.get(".mx_SettingsTab.mx_GeneralUserSettingsTab").percySnapshotElement("User settings tab - General", {
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
@ -51,7 +51,7 @@ describe("General user settings tab", () => {
widths: [580, 796],
});
cy.get(".mx_SettingsTab.mx_GeneralUserSettingsTab").within(() => {
cy.findByTestId("mx_GeneralUserSettingsTab").within(() => {
// Assert that the top heading is rendered
cy.findByTestId("general").should("have.text", "General").should("be.visible");
@ -156,16 +156,10 @@ describe("General user settings tab", () => {
// Make sure integration manager's toggle switch is enabled
cy.get(".mx_ToggleSwitch_enabled").should("be.visible");
// Assert space between "Manage integrations" and the integration server address is set to 4px;
cy.get(".mx_SetIntegrationManager_heading_manager").should("have.css", "column-gap", "4px");
cy.get(".mx_SetIntegrationManager_heading_manager").within(() => {
cy.get(".mx_SettingsTab_heading").should("have.text", "Manage integrations");
// Assert the headings' inline end margin values are set to zero in favor of the column-gap declaration
cy.get(".mx_SettingsTab_heading").should("have.css", "margin-inline-end", "0px");
cy.get(".mx_SettingsTab_subheading").should("have.css", "margin-inline-end", "0px");
});
cy.get(".mx_SetIntegrationManager_heading_manager").should(
"have.text",
"Manage integrations(scalar.vector.im)",
);
});
// Assert the account deactivation button is displayed
@ -178,7 +172,7 @@ describe("General user settings tab", () => {
});
it("should support adding and removing a profile picture", () => {
cy.get(".mx_SettingsTab.mx_GeneralUserSettingsTab .mx_ProfileSettings").within(() => {
cy.get(".mx_SettingsTab .mx_ProfileSettings").within(() => {
// Upload a picture
cy.get(".mx_ProfileSettings_avatarUpload").selectFile("cypress/fixtures/riot.png", { force: true });
@ -225,7 +219,7 @@ describe("General user settings tab", () => {
});
it("should support changing a display name", () => {
cy.get(".mx_SettingsTab.mx_GeneralUserSettingsTab .mx_ProfileSettings").within(() => {
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}`);
});

View file

@ -17,20 +17,12 @@ limitations under the License.
.mx_SetIntegrationManager {
.mx_SettingsFlag {
align-items: center;
margin-top: var(--SettingsTab_heading_nth_child-margin-top);
.mx_SetIntegrationManager_heading_manager {
display: flex;
align-items: center;
flex-wrap: wrap;
column-gap: $spacing-4;
.mx_SettingsTab_heading,
.mx_SettingsTab_subheading {
margin-top: 0;
margin-bottom: 0;
margin-inline-end: 0; /* Cancel the default right (inline-end) margin */
}
}
.mx_ToggleSwitch {

View file

@ -23,6 +23,8 @@ import { IntegrationManagerInstance } from "../../../integrations/IntegrationMan
import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
import ToggleSwitch from "../elements/ToggleSwitch";
import Heading from "../typography/Heading";
import { SettingsSubsectionText } from "./shared/SettingsSubsection";
interface IProps {}
@ -70,11 +72,15 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
}
return (
<label className="mx_SetIntegrationManager" htmlFor="toggle_integration">
<label
className="mx_SetIntegrationManager"
data-testid="mx_SetIntegrationManager"
htmlFor="toggle_integration"
>
<div className="mx_SettingsFlag">
<div className="mx_SetIntegrationManager_heading_manager">
<span className="mx_SettingsTab_heading">{_t("Manage integrations")}</span>
<span className="mx_SettingsTab_subheading">{managerName}</span>
<Heading size="h2">{_t("Manage integrations")}</Heading>
<Heading size="h3">{managerName}</Heading>
</div>
<ToggleSwitch
id="toggle_integration"
@ -83,13 +89,13 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
onChange={this.onProvisioningToggled}
/>
</div>
<div className="mx_SettingsTab_subsectionText">{bodyText}</div>
<div className="mx_SettingsTab_subsectionText">
<SettingsSubsectionText>{bodyText}</SettingsSubsectionText>
<SettingsSubsectionText>
{_t(
"Integration managers receive configuration data, and can modify widgets, " +
"send room invites, and set power levels on your behalf.",
)}
</div>
</SettingsSubsectionText>
</label>
);
}

View file

@ -54,6 +54,9 @@ import SetIdServer from "../../SetIdServer";
import SetIntegrationManager from "../../SetIntegrationManager";
import ToggleSwitch from "../../../elements/ToggleSwitch";
import { IS_MAC } from "../../../../../Keyboard";
import SettingsTab from "../SettingsTab";
import { SettingsSection } from "../../shared/SettingsSection";
import SettingsSubsection from "../../shared/SettingsSubsection";
interface IProps {
closeSettingsFn: () => void;
@ -492,27 +495,24 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
private renderManagementSection(): JSX.Element {
// TODO: Improve warning text for account deactivation
return (
<div className="mx_SettingsTab_section" data-testid="account-management-section">
<span className="mx_SettingsTab_subheading">{_t("Account management")}</span>
<span className="mx_SettingsTab_subsectionText">
{_t("Deactivating your account is a permanent action — be careful!")}
</span>
<AccessibleButton onClick={this.onDeactivateClicked} kind="danger">
{_t("Deactivate Account")}
</AccessibleButton>
</div>
<SettingsSection heading={_t("Deactivate account")}>
<SettingsSubsection
heading={_t("Account management")}
data-testid="account-management-section"
description={_t("Deactivating your account is a permanent action — be careful!")}
>
<AccessibleButton onClick={this.onDeactivateClicked} kind="danger">
{_t("Deactivate Account")}
</AccessibleButton>
</SettingsSubsection>
</SettingsSection>
);
}
private renderIntegrationManagerSection(): ReactNode {
if (!SettingsStore.getValue(UIFeature.Widgets)) return null;
return (
<div className="mx_SettingsTab_section">
{/* has its own heading as it includes the current integration manager */}
<SetIntegrationManager />
</div>
);
return <SetIntegrationManager />;
}
public render(): React.ReactNode {
@ -531,12 +531,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
let accountManagementSection: JSX.Element | undefined;
if (SettingsStore.getValue(UIFeature.Deactivate)) {
accountManagementSection = (
<>
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
{this.renderManagementSection()}
</>
);
accountManagementSection = this.renderManagementSection();
}
let discoverySection;
@ -552,7 +547,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
}
return (
<div className="mx_SettingsTab mx_GeneralUserSettingsTab">
<SettingsTab data-testid="mx_GeneralUserSettingsTab">
<div className="mx_SettingsTab_heading" data-testid="general">
{_t("General")}
</div>
@ -561,9 +556,9 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
{this.renderLanguageSection()}
{supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null}
{discoverySection}
{this.renderIntegrationManagerSection() /* Has its own title */}
{this.renderIntegrationManagerSection()}
{accountManagementSection}
</div>
</SettingsTab>
);
}
}

View file

@ -1568,10 +1568,10 @@
"Language and region": "Language and region",
"Spell check": "Spell check",
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.",
"Deactivate account": "Deactivate account",
"Account management": "Account management",
"Deactivating your account is a permanent action — be careful!": "Deactivating your account is a permanent action — be careful!",
"Deactivate Account": "Deactivate Account",
"Deactivate account": "Deactivate account",
"Discovery": "Discovery",
"%(brand)s version:": "%(brand)s version:",
"Olm version:": "Olm version:",

View file

@ -10,9 +10,11 @@ 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 { render } from "@testing-library/react";
import { fireEvent, render, screen, within } from "@testing-library/react";
import React from "react";
import { M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import GeneralUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/GeneralUserSettingsTab";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
@ -24,6 +26,8 @@ import {
mockPlatformPeg,
flushPromises,
} from "../../../../../test-utils";
import { UIFeature } from "../../../../../../src/settings/UIFeature";
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
describe("<GeneralUserSettingsTab />", () => {
const defaultProps = {
@ -49,6 +53,8 @@ describe("<GeneralUserSettingsTab />", () => {
mockPlatformPeg();
jest.clearAllMocks();
clientWellKnownSpy.mockReturnValue({});
jest.spyOn(SettingsStore, "getValue").mockRestore();
jest.spyOn(logger, "error").mockRestore();
});
it("does not show account management link when not available", () => {
@ -74,4 +80,83 @@ describe("<GeneralUserSettingsTab />", () => {
expect(getByTestId("external-account-management-outer").textContent).toMatch(/.*id\.server\.org/);
expect(getByTestId("external-account-management-link").getAttribute("href")).toMatch(accountManagementLink);
});
describe("Manage integrations", () => {
it("should not render manage integrations section when widgets feature is disabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName !== UIFeature.Widgets,
);
render(getComponent());
expect(screen.queryByTestId("mx_SetIntegrationManager")).not.toBeInTheDocument();
expect(SettingsStore.getValue).toHaveBeenCalledWith(UIFeature.Widgets);
});
it("should render manage integrations sections", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === UIFeature.Widgets,
);
render(getComponent());
expect(screen.getByTestId("mx_SetIntegrationManager")).toMatchSnapshot();
});
it("should update integrations provisioning on toggle", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === UIFeature.Widgets,
);
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
render(getComponent());
const integrationSection = screen.getByTestId("mx_SetIntegrationManager");
fireEvent.click(within(integrationSection).getByRole("switch"));
expect(SettingsStore.setValue).toHaveBeenCalledWith(
"integrationProvisioning",
null,
SettingLevel.ACCOUNT,
true,
);
expect(within(integrationSection).getByRole("switch")).toBeChecked();
});
it("handles error when updating setting fails", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === UIFeature.Widgets,
);
jest.spyOn(logger, "error").mockImplementation(() => {});
jest.spyOn(SettingsStore, "setValue").mockRejectedValue("oups");
render(getComponent());
const integrationSection = screen.getByTestId("mx_SetIntegrationManager");
fireEvent.click(within(integrationSection).getByRole("switch"));
await flushPromises();
expect(logger.error).toHaveBeenCalledWith("Error changing integration manager provisioning");
expect(logger.error).toHaveBeenCalledWith("oups");
expect(within(integrationSection).getByRole("switch")).not.toBeChecked();
});
});
describe("deactive account", () => {
it("should not render section when account deactivation feature is disabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName !== UIFeature.Deactivate,
);
render(getComponent());
expect(screen.queryByText("Deactivate account")).not.toBeInTheDocument();
expect(SettingsStore.getValue).toHaveBeenCalledWith(UIFeature.Deactivate);
});
it("should render section when account deactivation feature is enabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === UIFeature.Deactivate,
);
render(getComponent());
expect(screen.getByText("Deactivate account").parentElement!).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,106 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<GeneralUserSettingsTab /> Manage integrations should render manage integrations sections 1`] = `
<label
class="mx_SetIntegrationManager"
data-testid="mx_SetIntegrationManager"
for="toggle_integration"
>
<div
class="mx_SettingsFlag"
>
<div
class="mx_SetIntegrationManager_heading_manager"
>
<h2
class="mx_Heading_h2"
>
Manage integrations
</h2>
<h3
class="mx_Heading_h3"
>
(scalar.vector.im)
</h3>
</div>
<div
aria-checked="false"
aria-disabled="false"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
id="toggle_integration"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsSubsection_text"
>
<span>
Use an integration manager
<b>
(scalar.vector.im)
</b>
to manage bots, widgets, and sticker packs.
</span>
</div>
<div
class="mx_SettingsSubsection_text"
>
Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.
</div>
</label>
`;
exports[`<GeneralUserSettingsTab /> deactive account should render section when account deactivation feature is enabled 1`] = `
<div
class="mx_SettingsSection"
>
<h2
class="mx_Heading_h2"
>
Deactivate account
</h2>
<div
class="mx_SettingsSection_subSections"
>
<div
class="mx_SettingsSubsection"
data-testid="account-management-section"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
>
Account management
</h3>
</div>
<div
class="mx_SettingsSubsection_description"
>
<div
class="mx_SettingsSubsection_text"
>
Deactivating your account is a permanent action — be careful!
</div>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
role="button"
tabindex="0"
>
Deactivate Account
</div>
</div>
</div>
</div>
</div>
`;