Notifications: inline error message on notifications saving error (#10288)
* basic sync setup * formatting * get loudest value for synced rules * more types * test synced rules in notifications settings * type fixes * noimplicitany fixes * remove debug * tidying * extract updatePushRuleActions fn to utils * extract update synced rules * just synchronise in one place? * monitor account data changes AND trigger changes sync in notifications form * lint * setup LoggedInView test with enough mocks * test rule syncing in LoggedInView * strict fixes * more comments * one more comment * add error variant for caption component * tests for new error message * tweak styles * noImplicitAny * revert out of date prettier changes to unrelated files * limit inline message to radios only, tests * strict fix
This commit is contained in:
parent
72404d7216
commit
9f66082486
8 changed files with 233 additions and 33 deletions
|
@ -17,4 +17,8 @@ limitations under the License.
|
||||||
.mx_Caption {
|
.mx_Caption {
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
color: $secondary-content;
|
color: $secondary-content;
|
||||||
|
|
||||||
|
&.mx_Caption_error {
|
||||||
|
color: $alert;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,14 @@ limitations under the License.
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
font-weight: $font-semi-bold;
|
font-weight: $font-semi-bold;
|
||||||
}
|
}
|
||||||
|
.mx_UserNotifSettings_gridRowError {
|
||||||
|
// occupy full row
|
||||||
|
grid-column: 1/-1;
|
||||||
|
justify-self: start;
|
||||||
|
padding-right: 30%;
|
||||||
|
// collapse half of the grid-gap
|
||||||
|
margin-top: -$spacing-4;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_UserNotifSettings {
|
.mx_UserNotifSettings {
|
||||||
color: $primary-content; /* override from default settings page styles */
|
color: $primary-content; /* override from default settings page styles */
|
||||||
|
|
|
@ -47,6 +47,7 @@ import {
|
||||||
updateExistingPushRulesWithActions,
|
updateExistingPushRulesWithActions,
|
||||||
updatePushRuleActions,
|
updatePushRuleActions,
|
||||||
} from "../../../utils/pushRules/updatePushRuleActions";
|
} from "../../../utils/pushRules/updatePushRuleActions";
|
||||||
|
import { Caption } from "../typography/Caption";
|
||||||
|
|
||||||
// TODO: this "view" component still has far too much application logic in it,
|
// TODO: this "view" component still has far too much application logic in it,
|
||||||
// which should be factored out to other files.
|
// which should be factored out to other files.
|
||||||
|
@ -55,7 +56,10 @@ enum Phase {
|
||||||
Loading = "loading",
|
Loading = "loading",
|
||||||
Ready = "ready",
|
Ready = "ready",
|
||||||
Persisting = "persisting", // technically a meta-state for Ready, but whatever
|
Persisting = "persisting", // technically a meta-state for Ready, but whatever
|
||||||
|
// unrecoverable error - eg can't load push rules
|
||||||
Error = "error",
|
Error = "error",
|
||||||
|
// error saving individual rule
|
||||||
|
SavingError = "savingError",
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RuleClass {
|
enum RuleClass {
|
||||||
|
@ -121,6 +125,8 @@ interface IState {
|
||||||
audioNotifications: boolean;
|
audioNotifications: boolean;
|
||||||
|
|
||||||
clearingNotifications: boolean;
|
clearingNotifications: boolean;
|
||||||
|
|
||||||
|
ruleIdsWithError: Record<RuleId | string, boolean>;
|
||||||
}
|
}
|
||||||
const findInDefaultRules = (
|
const findInDefaultRules = (
|
||||||
ruleId: RuleId | string,
|
ruleId: RuleId | string,
|
||||||
|
@ -194,6 +200,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
desktopShowBody: SettingsStore.getValue("notificationBodyEnabled"),
|
desktopShowBody: SettingsStore.getValue("notificationBodyEnabled"),
|
||||||
audioNotifications: SettingsStore.getValue("audioNotificationsEnabled"),
|
audioNotifications: SettingsStore.getValue("audioNotificationsEnabled"),
|
||||||
clearingNotifications: false,
|
clearingNotifications: false,
|
||||||
|
ruleIdsWithError: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.settingWatchers = [
|
this.settingWatchers = [
|
||||||
|
@ -243,13 +250,9 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
).reduce((p, c) => Object.assign(c, p), {});
|
).reduce((p, c) => Object.assign(c, p), {});
|
||||||
|
|
||||||
this.setState<
|
this.setState<
|
||||||
keyof Omit<
|
keyof Pick<
|
||||||
IState,
|
IState,
|
||||||
| "deviceNotificationsEnabled"
|
"phase" | "vectorKeywordRuleInfo" | "vectorPushRules" | "pushers" | "threepids" | "masterPushRule"
|
||||||
| "desktopNotifications"
|
|
||||||
| "desktopShowBody"
|
|
||||||
| "audioNotifications"
|
|
||||||
| "clearingNotifications"
|
|
||||||
>
|
>
|
||||||
>({
|
>({
|
||||||
...newState,
|
...newState,
|
||||||
|
@ -393,8 +396,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
private onMasterRuleChanged = async (checked: boolean): Promise<void> => {
|
private onMasterRuleChanged = async (checked: boolean): Promise<void> => {
|
||||||
this.setState({ phase: Phase.Persisting });
|
this.setState({ phase: Phase.Persisting });
|
||||||
|
|
||||||
|
const masterRule = this.state.masterPushRule!;
|
||||||
try {
|
try {
|
||||||
const masterRule = this.state.masterPushRule!;
|
|
||||||
await MatrixClientPeg.get().setPushRuleEnabled("global", masterRule.kind, masterRule.rule_id, !checked);
|
await MatrixClientPeg.get().setPushRuleEnabled("global", masterRule.kind, masterRule.rule_id, !checked);
|
||||||
await this.refreshFromServer();
|
await this.refreshFromServer();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -404,6 +407,13 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private setSavingError = (ruleId: RuleId | string): void => {
|
||||||
|
this.setState(({ ruleIdsWithError }) => ({
|
||||||
|
phase: Phase.SavingError,
|
||||||
|
ruleIdsWithError: { ...ruleIdsWithError, [ruleId]: true },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
private updateDeviceNotifications = async (checked: boolean): Promise<void> => {
|
private updateDeviceNotifications = async (checked: boolean): Promise<void> => {
|
||||||
await SettingsStore.setValue("deviceNotificationsEnabled", null, SettingLevel.DEVICE, checked);
|
await SettingsStore.setValue("deviceNotificationsEnabled", null, SettingLevel.DEVICE, checked);
|
||||||
};
|
};
|
||||||
|
@ -455,11 +465,18 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRadioChecked = async (rule: IVectorPushRule, checkedState: VectorState): Promise<void> => {
|
private onRadioChecked = async (rule: IVectorPushRule, checkedState: VectorState): Promise<void> => {
|
||||||
this.setState({ phase: Phase.Persisting });
|
this.setState(({ ruleIdsWithError }) => ({
|
||||||
|
phase: Phase.Persisting,
|
||||||
|
ruleIdsWithError: { ...ruleIdsWithError, [rule.ruleId]: false },
|
||||||
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (rule.ruleId === KEYWORD_RULE_ID) {
|
if (rule.ruleId === KEYWORD_RULE_ID) {
|
||||||
|
// should not encounter this
|
||||||
|
if (!this.state.vectorKeywordRuleInfo) {
|
||||||
|
throw new Error("Notification data is incomplete.");
|
||||||
|
}
|
||||||
// Update all the keywords
|
// Update all the keywords
|
||||||
for (const rule of this.state.vectorKeywordRuleInfo.rules) {
|
for (const rule of this.state.vectorKeywordRuleInfo.rules) {
|
||||||
let enabled: boolean | undefined;
|
let enabled: boolean | undefined;
|
||||||
|
@ -505,9 +522,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
await this.refreshFromServer();
|
await this.refreshFromServer();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({ phase: Phase.Error });
|
this.setSavingError(rule.ruleId);
|
||||||
logger.error("Error updating push rule:", e);
|
logger.error("Error updating push rule:", e);
|
||||||
this.showSaveError();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -618,14 +634,16 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
private renderTopSection(): JSX.Element {
|
private renderTopSection(): JSX.Element {
|
||||||
const masterSwitch = (
|
const masterSwitch = (
|
||||||
<LabelledToggleSwitch
|
<>
|
||||||
data-testid="notif-master-switch"
|
<LabelledToggleSwitch
|
||||||
value={!this.isInhibited}
|
data-testid="notif-master-switch"
|
||||||
label={_t("Enable notifications for this account")}
|
value={!this.isInhibited}
|
||||||
caption={_t("Turn off to disable notifications on all your devices and sessions")}
|
label={_t("Enable notifications for this account")}
|
||||||
onChange={this.onMasterRuleChanged}
|
caption={_t("Turn off to disable notifications on all your devices and sessions")}
|
||||||
disabled={this.state.phase === Phase.Persisting}
|
onChange={this.onMasterRuleChanged}
|
||||||
/>
|
disabled={this.state.phase === Phase.Persisting}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
// If all the rules are inhibited, don't show anything.
|
// If all the rules are inhibited, don't show anything.
|
||||||
|
@ -639,7 +657,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
<LabelledToggleSwitch
|
<LabelledToggleSwitch
|
||||||
data-testid="notif-email-switch"
|
data-testid="notif-email-switch"
|
||||||
key={e.address}
|
key={e.address}
|
||||||
value={this.state.pushers.some((p) => p.kind === "email" && p.pushkey === e.address)}
|
value={!!this.state.pushers?.some((p) => p.kind === "email" && p.pushkey === e.address)}
|
||||||
label={_t("Enable email notifications for %(email)s", { email: e.address })}
|
label={_t("Enable email notifications for %(email)s", { email: e.address })}
|
||||||
onChange={this.onEmailNotificationsChanged.bind(this, e.address)}
|
onChange={this.onEmailNotificationsChanged.bind(this, e.address)}
|
||||||
disabled={this.state.phase === Phase.Persisting}
|
disabled={this.state.phase === Phase.Persisting}
|
||||||
|
@ -768,6 +786,15 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
{makeRadio(r, VectorState.Off)}
|
{makeRadio(r, VectorState.Off)}
|
||||||
{makeRadio(r, VectorState.On)}
|
{makeRadio(r, VectorState.On)}
|
||||||
{makeRadio(r, VectorState.Loud)}
|
{makeRadio(r, VectorState.Loud)}
|
||||||
|
{this.state.ruleIdsWithError[r.ruleId] && (
|
||||||
|
<div className="mx_UserNotifSettings_gridRowError">
|
||||||
|
<Caption isError>
|
||||||
|
{_t(
|
||||||
|
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -14,15 +14,22 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import classNames from "classnames";
|
||||||
import React, { HTMLAttributes } from "react";
|
import React, { HTMLAttributes } from "react";
|
||||||
|
|
||||||
interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "className"> {
|
interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "className"> {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
isError?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Caption: React.FC<Props> = ({ children, ...rest }) => {
|
export const Caption: React.FC<Props> = ({ children, isError, ...rest }) => {
|
||||||
return (
|
return (
|
||||||
<span className="mx_Caption" {...rest}>
|
<span
|
||||||
|
className={classNames("mx_Caption", {
|
||||||
|
mx_Caption_error: isError,
|
||||||
|
})}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1443,6 +1443,7 @@
|
||||||
"On": "On",
|
"On": "On",
|
||||||
"Off": "Off",
|
"Off": "Off",
|
||||||
"Noisy": "Noisy",
|
"Noisy": "Noisy",
|
||||||
|
"An error occurred when updating your notification preferences. Please try to toggle your option again.": "An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||||
"Global": "Global",
|
"Global": "Global",
|
||||||
"Mentions & keywords": "Mentions & keywords",
|
"Mentions & keywords": "Mentions & keywords",
|
||||||
"Notification targets": "Notification targets",
|
"Notification targets": "Notification targets",
|
||||||
|
|
|
@ -28,7 +28,7 @@ import {
|
||||||
IPushRuleCondition,
|
IPushRuleCondition,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||||
import { act, fireEvent, getByTestId, render, screen, waitFor } from "@testing-library/react";
|
import { act, fireEvent, getByTestId, render, screen, waitFor, within } from "@testing-library/react";
|
||||||
|
|
||||||
import Notifications from "../../../../src/components/views/settings/Notifications";
|
import Notifications from "../../../../src/components/views/settings/Notifications";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
|
@ -90,6 +90,15 @@ const encryptedGroupRule: IPushRule = {
|
||||||
default: true,
|
default: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
} as IPushRule;
|
} as IPushRule;
|
||||||
|
|
||||||
|
const bananaRule = {
|
||||||
|
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }],
|
||||||
|
pattern: "banana",
|
||||||
|
rule_id: "banana",
|
||||||
|
default: false,
|
||||||
|
enabled: true,
|
||||||
|
} as IPushRule;
|
||||||
|
|
||||||
const pushRules: IPushRules = {
|
const pushRules: IPushRules = {
|
||||||
global: {
|
global: {
|
||||||
underride: [
|
underride: [
|
||||||
|
@ -130,13 +139,7 @@ const pushRules: IPushRules = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
content: [
|
content: [
|
||||||
{
|
bananaRule,
|
||||||
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }],
|
|
||||||
pattern: "banana",
|
|
||||||
rule_id: "banana",
|
|
||||||
default: false,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
actions: [
|
actions: [
|
||||||
PushRuleActionName.Notify,
|
PushRuleActionName.Notify,
|
||||||
|
@ -272,7 +275,7 @@ describe("<Notifications />", () => {
|
||||||
mockClient.getPushers.mockClear().mockResolvedValue({ pushers: [] });
|
mockClient.getPushers.mockClear().mockResolvedValue({ pushers: [] });
|
||||||
mockClient.getThreePids.mockClear().mockResolvedValue({ threepids: [] });
|
mockClient.getThreePids.mockClear().mockResolvedValue({ threepids: [] });
|
||||||
mockClient.setPusher.mockClear().mockResolvedValue({});
|
mockClient.setPusher.mockClear().mockResolvedValue({});
|
||||||
mockClient.setPushRuleActions.mockClear().mockResolvedValue({});
|
mockClient.setPushRuleActions.mockReset().mockResolvedValue({});
|
||||||
mockClient.pushRules = pushRules;
|
mockClient.pushRules = pushRules;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -395,6 +398,18 @@ describe("<Notifications />", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("toggles master switch correctly", async () => {
|
||||||
|
await getComponentAndWait();
|
||||||
|
|
||||||
|
// master switch is on
|
||||||
|
expect(screen.getByLabelText("Enable notifications for this account")).toBeChecked();
|
||||||
|
fireEvent.click(screen.getByLabelText("Enable notifications for this account"));
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith("global", "override", ".m.rule.master", true);
|
||||||
|
});
|
||||||
|
|
||||||
it("toggles and sets settings correctly", async () => {
|
it("toggles and sets settings correctly", async () => {
|
||||||
await getComponentAndWait();
|
await getComponentAndWait();
|
||||||
let audioNotifsToggle!: HTMLDivElement;
|
let audioNotifsToggle!: HTMLDivElement;
|
||||||
|
@ -473,6 +488,73 @@ describe("<Notifications />", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds an error message when updating notification level fails", async () => {
|
||||||
|
await getComponentAndWait();
|
||||||
|
const section = "vector_global";
|
||||||
|
|
||||||
|
const error = new Error("oups");
|
||||||
|
mockClient.setPushRuleEnabled.mockRejectedValue(error);
|
||||||
|
|
||||||
|
// oneToOneRule is set to 'on'
|
||||||
|
// and is kind: 'underride'
|
||||||
|
const offToggle = screen.getByTestId(section + oneToOneRule.rule_id).querySelector('input[type="radio"]')!;
|
||||||
|
fireEvent.click(offToggle);
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// error message attached to oneToOne rule
|
||||||
|
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||||
|
// old value still shown as selected
|
||||||
|
expect(within(oneToOneRuleElement).getByLabelText("On")).toBeChecked();
|
||||||
|
expect(
|
||||||
|
within(oneToOneRuleElement).getByText(
|
||||||
|
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears error message for notification rule on retry", async () => {
|
||||||
|
await getComponentAndWait();
|
||||||
|
const section = "vector_global";
|
||||||
|
|
||||||
|
const error = new Error("oups");
|
||||||
|
mockClient.setPushRuleEnabled.mockRejectedValueOnce(error).mockResolvedValue({});
|
||||||
|
|
||||||
|
// oneToOneRule is set to 'on'
|
||||||
|
// and is kind: 'underride'
|
||||||
|
const offToggle = screen.getByTestId(section + oneToOneRule.rule_id).querySelector('input[type="radio"]')!;
|
||||||
|
fireEvent.click(offToggle);
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// error message attached to oneToOne rule
|
||||||
|
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||||
|
expect(
|
||||||
|
within(oneToOneRuleElement).getByText(
|
||||||
|
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// retry
|
||||||
|
fireEvent.click(offToggle);
|
||||||
|
|
||||||
|
// error removed as soon as we start request
|
||||||
|
expect(
|
||||||
|
within(oneToOneRuleElement).queryByText(
|
||||||
|
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// no error after after successful change
|
||||||
|
expect(
|
||||||
|
within(oneToOneRuleElement).queryByText(
|
||||||
|
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
describe("synced rules", () => {
|
describe("synced rules", () => {
|
||||||
const pollStartOneToOne = {
|
const pollStartOneToOne = {
|
||||||
conditions: [
|
conditions: [
|
||||||
|
@ -554,7 +636,11 @@ describe("<Notifications />", () => {
|
||||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(1);
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
// no error
|
// no error
|
||||||
expect(screen.queryByTestId("error-message")).not.toBeInTheDocument();
|
expect(
|
||||||
|
within(oneToOneRuleElement).queryByText(
|
||||||
|
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates synced rules when they exist for user", async () => {
|
it("updates synced rules when they exist for user", async () => {
|
||||||
|
@ -585,7 +671,11 @@ describe("<Notifications />", () => {
|
||||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(2);
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
// no error
|
// no error
|
||||||
expect(screen.queryByTestId("error-message")).not.toBeInTheDocument();
|
expect(
|
||||||
|
within(oneToOneRuleElement).queryByText(
|
||||||
|
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not update synced rules when main rule update fails", async () => {
|
it("does not update synced rules when main rule update fails", async () => {
|
||||||
|
@ -610,7 +700,11 @@ describe("<Notifications />", () => {
|
||||||
// only called for parent rule
|
// only called for parent rule
|
||||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(1);
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
expect(screen.queryByTestId("error-message")).toBeInTheDocument();
|
expect(
|
||||||
|
within(oneToOneRuleElement).getByText(
|
||||||
|
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets the UI toggle to rule value when no synced rule exist for the user", async () => {
|
it("sets the UI toggle to rule value when no synced rule exist for the user", async () => {
|
||||||
|
@ -664,6 +758,47 @@ describe("<Notifications />", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("keywords", () => {
|
||||||
|
// keywords rule is not a real rule, but controls actions on keywords content rules
|
||||||
|
const keywordsRuleId = "_keywords";
|
||||||
|
it("updates individual keywords content rules when keywords rule is toggled", async () => {
|
||||||
|
await getComponentAndWait();
|
||||||
|
const section = "vector_mentions";
|
||||||
|
|
||||||
|
fireEvent.click(within(screen.getByTestId(section + keywordsRuleId)).getByLabelText("Off"));
|
||||||
|
|
||||||
|
expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith("global", "content", bananaRule.rule_id, false);
|
||||||
|
|
||||||
|
fireEvent.click(within(screen.getByTestId(section + keywordsRuleId)).getByLabelText("Noisy"));
|
||||||
|
|
||||||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||||
|
"global",
|
||||||
|
"content",
|
||||||
|
bananaRule.rule_id,
|
||||||
|
StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders an error when updating keywords fails", async () => {
|
||||||
|
await getComponentAndWait();
|
||||||
|
const section = "vector_mentions";
|
||||||
|
|
||||||
|
mockClient.setPushRuleEnabled.mockRejectedValueOnce("oups");
|
||||||
|
|
||||||
|
fireEvent.click(within(screen.getByTestId(section + keywordsRuleId)).getByLabelText("Off"));
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const rule = screen.getByTestId(section + keywordsRuleId);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
within(rule).getByText(
|
||||||
|
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("clear all notifications", () => {
|
describe("clear all notifications", () => {
|
||||||
it("clears all notifications", async () => {
|
it("clears all notifications", async () => {
|
||||||
const room = new Room("room123", mockClient, "@alice:example.org");
|
const room = new Room("room123", mockClient, "@alice:example.org");
|
||||||
|
|
|
@ -31,6 +31,11 @@ describe("<Caption />", () => {
|
||||||
expect({ container }).toMatchSnapshot();
|
expect({ container }).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders an error message", () => {
|
||||||
|
const { container } = render(getComponent({ isError: true }));
|
||||||
|
expect({ container }).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders react children", () => {
|
it("renders react children", () => {
|
||||||
const children = (
|
const children = (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,5 +1,18 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<Caption /> renders an error message 1`] = `
|
||||||
|
{
|
||||||
|
"container": <div>
|
||||||
|
<span
|
||||||
|
class="mx_Caption mx_Caption_error"
|
||||||
|
data-testid="test test id"
|
||||||
|
>
|
||||||
|
test
|
||||||
|
</span>
|
||||||
|
</div>,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`<Caption /> renders plain text children 1`] = `
|
exports[`<Caption /> renders plain text children 1`] = `
|
||||||
{
|
{
|
||||||
"container": <div>
|
"container": <div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue