Apply strictNullChecks
to src/components/views/settings
(#10724)
This commit is contained in:
parent
a4f0b80692
commit
1f4d857283
11 changed files with 772 additions and 48 deletions
|
@ -32,7 +32,12 @@ export const Tag: React.FC<IProps> = ({ icon, label, onDeleteClick, disabled = f
|
||||||
{icon?.()}
|
{icon?.()}
|
||||||
{label}
|
{label}
|
||||||
{onDeleteClick && (
|
{onDeleteClick && (
|
||||||
<AccessibleButton className="mx_Tag_delete" onClick={onDeleteClick} disabled={disabled}>
|
<AccessibleButton
|
||||||
|
aria-label="Remove"
|
||||||
|
className="mx_Tag_delete"
|
||||||
|
onClick={onDeleteClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
<CancelRounded />
|
<CancelRounded />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -36,7 +36,7 @@ import { Action } from "../../../dispatcher/actions";
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import { doesRoomVersionSupport, PreferredRoomVersions } from "../../../utils/PreferredRoomVersions";
|
import { doesRoomVersionSupport, PreferredRoomVersions } from "../../../utils/PreferredRoomVersions";
|
||||||
|
|
||||||
interface IProps {
|
export interface JoinRuleSettingsProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
promptUpgrade?: boolean;
|
promptUpgrade?: boolean;
|
||||||
closeSettingsFn(): void;
|
closeSettingsFn(): void;
|
||||||
|
@ -45,7 +45,7 @@ interface IProps {
|
||||||
aliasWarning?: ReactNode;
|
aliasWarning?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const JoinRuleSettings: React.FC<IProps> = ({
|
const JoinRuleSettings: React.FC<JoinRuleSettingsProps> = ({
|
||||||
room,
|
room,
|
||||||
promptUpgrade,
|
promptUpgrade,
|
||||||
aliasWarning,
|
aliasWarning,
|
||||||
|
@ -287,7 +287,10 @@ const JoinRuleSettings: React.FC<IProps> = ({
|
||||||
fn(_t("Upgrading room"), 0, total);
|
fn(_t("Upgrading room"), 0, total);
|
||||||
} else if (!progress.roomSynced) {
|
} else if (!progress.roomSynced) {
|
||||||
fn(_t("Loading new room"), 1, total);
|
fn(_t("Loading new room"), 1, total);
|
||||||
} else if (progress.inviteUsersProgress < progress.inviteUsersTotal) {
|
} else if (
|
||||||
|
progress.inviteUsersProgress !== undefined &&
|
||||||
|
progress.inviteUsersProgress < progress.inviteUsersTotal
|
||||||
|
) {
|
||||||
fn(
|
fn(
|
||||||
_t("Sending invites... (%(progress)s out of %(count)s)", {
|
_t("Sending invites... (%(progress)s out of %(count)s)", {
|
||||||
progress: progress.inviteUsersProgress,
|
progress: progress.inviteUsersProgress,
|
||||||
|
@ -296,13 +299,16 @@ const JoinRuleSettings: React.FC<IProps> = ({
|
||||||
2 + progress.inviteUsersProgress,
|
2 + progress.inviteUsersProgress,
|
||||||
total,
|
total,
|
||||||
);
|
);
|
||||||
} else if (progress.updateSpacesProgress < progress.updateSpacesTotal) {
|
} else if (
|
||||||
|
progress.updateSpacesProgress !== undefined &&
|
||||||
|
progress.updateSpacesProgress < progress.updateSpacesTotal
|
||||||
|
) {
|
||||||
fn(
|
fn(
|
||||||
_t("Updating spaces... (%(progress)s out of %(count)s)", {
|
_t("Updating spaces... (%(progress)s out of %(count)s)", {
|
||||||
progress: progress.updateSpacesProgress,
|
progress: progress.updateSpacesProgress,
|
||||||
count: progress.updateSpacesTotal,
|
count: progress.updateSpacesTotal,
|
||||||
}),
|
}),
|
||||||
2 + progress.inviteUsersProgress + progress.updateSpacesProgress,
|
2 + (progress.inviteUsersProgress ?? 0) + progress.updateSpacesProgress,
|
||||||
total,
|
total,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,7 +167,7 @@ const maximumVectorState = (
|
||||||
if (!definition.syncedRuleIds?.length) {
|
if (!definition.syncedRuleIds?.length) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const vectorState = definition.syncedRuleIds.reduce<VectorState | undefined>((maxVectorState, ruleId) => {
|
const vectorState = definition.syncedRuleIds.reduce<VectorState>((maxVectorState, ruleId) => {
|
||||||
// already set to maximum
|
// already set to maximum
|
||||||
if (maxVectorState === VectorState.Loud) {
|
if (maxVectorState === VectorState.Loud) {
|
||||||
return maxVectorState;
|
return maxVectorState;
|
||||||
|
@ -177,12 +177,15 @@ const maximumVectorState = (
|
||||||
const syncedRuleVectorState = definition.ruleToVectorState(syncedRule);
|
const syncedRuleVectorState = definition.ruleToVectorState(syncedRule);
|
||||||
// if syncedRule is 'louder' than current maximum
|
// if syncedRule is 'louder' than current maximum
|
||||||
// set maximum to louder vectorState
|
// set maximum to louder vectorState
|
||||||
if (OrderedVectorStates.indexOf(syncedRuleVectorState) > OrderedVectorStates.indexOf(maxVectorState)) {
|
if (
|
||||||
|
syncedRuleVectorState &&
|
||||||
|
OrderedVectorStates.indexOf(syncedRuleVectorState) > OrderedVectorStates.indexOf(maxVectorState)
|
||||||
|
) {
|
||||||
return syncedRuleVectorState;
|
return syncedRuleVectorState;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return maxVectorState;
|
return maxVectorState;
|
||||||
}, definition.ruleToVectorState(rule));
|
}, definition.ruleToVectorState(rule)!);
|
||||||
|
|
||||||
return vectorState;
|
return vectorState;
|
||||||
};
|
};
|
||||||
|
@ -281,7 +284,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshRules(): Promise<Partial<IState>> {
|
private async refreshRules(): Promise<Partial<IState>> {
|
||||||
const ruleSets = await MatrixClientPeg.get().getPushRules();
|
const ruleSets = await MatrixClientPeg.get().getPushRules()!;
|
||||||
const categories: Record<string, RuleClass> = {
|
const categories: Record<string, RuleClass> = {
|
||||||
[RuleId.Master]: RuleClass.Master,
|
[RuleId.Master]: RuleClass.Master,
|
||||||
|
|
||||||
|
@ -316,7 +319,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
// noinspection JSUnfilteredForInLoop
|
// noinspection JSUnfilteredForInLoop
|
||||||
const kind = k as PushRuleKind;
|
const kind = k as PushRuleKind;
|
||||||
|
|
||||||
for (const r of ruleSets.global[kind]) {
|
for (const r of ruleSets.global[kind]!) {
|
||||||
const rule: IAnnotatedPushRule = Object.assign(r, { kind });
|
const rule: IAnnotatedPushRule = Object.assign(r, { kind });
|
||||||
const category = categories[rule.rule_id] ?? RuleClass.Other;
|
const category = categories[rule.rule_id] ?? RuleClass.Other;
|
||||||
|
|
||||||
|
@ -344,7 +347,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
preparedNewState.vectorPushRules[category] = [];
|
preparedNewState.vectorPushRules[category] = [];
|
||||||
for (const rule of defaultRules[category]) {
|
for (const rule of defaultRules[category]) {
|
||||||
const definition: VectorPushRuleDefinition = VectorPushRulesDefinitions[rule.rule_id];
|
const definition: VectorPushRuleDefinition = VectorPushRulesDefinitions[rule.rule_id];
|
||||||
const vectorState = definition.ruleToVectorState(rule);
|
const vectorState = definition.ruleToVectorState(rule)!;
|
||||||
preparedNewState.vectorPushRules[category]!.push({
|
preparedNewState.vectorPushRules[category]!.push({
|
||||||
ruleId: rule.rule_id,
|
ruleId: rule.rule_id,
|
||||||
rule,
|
rule,
|
||||||
|
@ -441,8 +444,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
} else {
|
} else {
|
||||||
const pusher = this.state.pushers?.find((p) => p.kind === "email" && p.pushkey === email);
|
const pusher = this.state.pushers?.find((p) => p.kind === "email" && p.pushkey === email);
|
||||||
if (pusher) {
|
if (pusher) {
|
||||||
pusher.kind = null; // flag for delete
|
await MatrixClientPeg.get().removePusher(pusher.pushkey, pusher.app_id);
|
||||||
await MatrixClientPeg.get().setPusher(pusher);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -539,17 +541,20 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private async setKeywords(keywords: string[], originalRules: IAnnotatedPushRule[]): Promise<void> {
|
private async setKeywords(
|
||||||
|
unsafeKeywords: (string | undefined)[],
|
||||||
|
originalRules: IAnnotatedPushRule[],
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// De-duplicate and remove empties
|
// De-duplicate and remove empties
|
||||||
keywords = filterBoolean(Array.from(new Set(keywords)));
|
const keywords = filterBoolean<string>(Array.from(new Set(unsafeKeywords)));
|
||||||
const oldKeywords = filterBoolean(Array.from(new Set(originalRules.map((r) => r.pattern))));
|
const oldKeywords = filterBoolean<string>(Array.from(new Set(originalRules.map((r) => r.pattern))));
|
||||||
|
|
||||||
// Note: Technically because of the UI interaction (at the time of writing), the diff
|
// Note: Technically because of the UI interaction (at the time of writing), the diff
|
||||||
// will only ever be +/-1 so we don't really have to worry about efficiently handling
|
// will only ever be +/-1 so we don't really have to worry about efficiently handling
|
||||||
// tons of keyword changes.
|
// tons of keyword changes.
|
||||||
|
|
||||||
const diff = arrayDiff(oldKeywords, keywords);
|
const diff = arrayDiff<string>(oldKeywords, keywords);
|
||||||
|
|
||||||
for (const word of diff.removed) {
|
for (const word of diff.removed) {
|
||||||
for (const rule of originalRules.filter((r) => r.pattern === word)) {
|
for (const rule of originalRules.filter((r) => r.pattern === word)) {
|
||||||
|
@ -557,16 +562,16 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ruleVectorState = this.state.vectorKeywordRuleInfo?.vectorState;
|
let ruleVectorState = this.state.vectorKeywordRuleInfo!.vectorState;
|
||||||
if (ruleVectorState === VectorState.Off) {
|
if (ruleVectorState === VectorState.Off) {
|
||||||
// When the current global keywords rule is OFF, we need to look at
|
// When the current global keywords rule is OFF, we need to look at
|
||||||
// the flavor of existing rules to apply the same actions
|
// the flavor of existing rules to apply the same actions
|
||||||
// when creating the new rule.
|
// when creating the new rule.
|
||||||
if (originalRules.length) {
|
const existingRuleVectorState = originalRules.length
|
||||||
ruleVectorState = PushRuleVectorState.contentRuleVectorStateKind(originalRules[0]) ?? undefined;
|
? PushRuleVectorState.contentRuleVectorStateKind(originalRules[0])
|
||||||
} else {
|
: undefined;
|
||||||
ruleVectorState = VectorState.On; // default
|
// set to same state as existing rule, or default to On
|
||||||
}
|
ruleVectorState = existingRuleVectorState ?? VectorState.On; //default
|
||||||
}
|
}
|
||||||
const kind = PushRuleKind.ContentSpecific;
|
const kind = PushRuleKind.ContentSpecific;
|
||||||
for (const word of diff.added) {
|
for (const word of diff.added) {
|
||||||
|
@ -588,6 +593,10 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onKeywordAdd = (keyword: string): void => {
|
private onKeywordAdd = (keyword: string): void => {
|
||||||
|
// should not encounter this
|
||||||
|
if (!this.state.vectorKeywordRuleInfo) {
|
||||||
|
throw new Error("Notification data is incomplete.");
|
||||||
|
}
|
||||||
const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules);
|
const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules);
|
||||||
|
|
||||||
// We add the keyword immediately as a sort of local echo effect
|
// We add the keyword immediately as a sort of local echo effect
|
||||||
|
@ -606,7 +615,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
},
|
},
|
||||||
async (): Promise<void> => {
|
async (): Promise<void> => {
|
||||||
await this.setKeywords(
|
await this.setKeywords(
|
||||||
this.state.vectorKeywordRuleInfo.rules.map((r) => r.pattern),
|
this.state.vectorKeywordRuleInfo!.rules.map((r) => r.pattern),
|
||||||
originalRules,
|
originalRules,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -614,6 +623,10 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onKeywordRemove = (keyword: string): void => {
|
private onKeywordRemove = (keyword: string): void => {
|
||||||
|
// should not encounter this
|
||||||
|
if (!this.state.vectorKeywordRuleInfo) {
|
||||||
|
throw new Error("Notification data is incomplete.");
|
||||||
|
}
|
||||||
const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules);
|
const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules);
|
||||||
|
|
||||||
// We remove the keyword immediately as a sort of local echo effect
|
// We remove the keyword immediately as a sort of local echo effect
|
||||||
|
@ -627,7 +640,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
},
|
},
|
||||||
async (): Promise<void> => {
|
async (): Promise<void> => {
|
||||||
await this.setKeywords(
|
await this.setKeywords(
|
||||||
this.state.vectorKeywordRuleInfo.rules.map((r) => r.pattern),
|
this.state.vectorKeywordRuleInfo!.rules.map((r) => r.pattern),
|
||||||
originalRules,
|
originalRules,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -749,9 +762,10 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
let keywordComposer: JSX.Element | undefined;
|
let keywordComposer: JSX.Element | undefined;
|
||||||
if (category === RuleClass.VectorMentions) {
|
if (category === RuleClass.VectorMentions) {
|
||||||
|
const tags = filterBoolean<string>(this.state.vectorKeywordRuleInfo?.rules.map((r) => r.pattern) || []);
|
||||||
keywordComposer = (
|
keywordComposer = (
|
||||||
<TagComposer
|
<TagComposer
|
||||||
tags={this.state.vectorKeywordRuleInfo?.rules.map((r) => r.pattern)}
|
tags={tags}
|
||||||
onAdd={this.onKeywordAdd}
|
onAdd={this.onKeywordAdd}
|
||||||
onRemove={this.onKeywordRemove}
|
onRemove={this.onKeywordRemove}
|
||||||
disabled={this.state.phase === Phase.Persisting}
|
disabled={this.state.phase === Phase.Persisting}
|
||||||
|
|
|
@ -65,7 +65,9 @@ export default class ProfileSettings extends React.Component<{}, IState> {
|
||||||
|
|
||||||
private removeAvatar = (): void => {
|
private removeAvatar = (): void => {
|
||||||
// clear file upload field so same file can be selected
|
// clear file upload field so same file can be selected
|
||||||
this.avatarUpload.current.value = "";
|
if (this.avatarUpload.current) {
|
||||||
|
this.avatarUpload.current.value = "";
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
avatarUrl: undefined,
|
avatarUrl: undefined,
|
||||||
avatarFile: null,
|
avatarFile: null,
|
||||||
|
|
|
@ -99,12 +99,12 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||||
private async checkKeyBackupStatus(): Promise<void> {
|
private async checkKeyBackupStatus(): Promise<void> {
|
||||||
this.getUpdatedDiagnostics();
|
this.getUpdatedDiagnostics();
|
||||||
try {
|
try {
|
||||||
const { backupInfo, trustInfo } = await MatrixClientPeg.get().checkKeyBackup();
|
const keyBackupResult = await MatrixClientPeg.get().checkKeyBackup();
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
backupInfo,
|
backupInfo: keyBackupResult?.backupInfo ?? null,
|
||||||
backupSigStatus: trustInfo,
|
backupSigStatus: keyBackupResult?.trustInfo ?? null,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.log("Unable to fetch check backup status", e);
|
logger.log("Unable to fetch check backup status", e);
|
||||||
|
@ -123,7 +123,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||||
this.getUpdatedDiagnostics();
|
this.getUpdatedDiagnostics();
|
||||||
try {
|
try {
|
||||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||||
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo!);
|
const backupSigStatus = backupInfo ? await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo) : null;
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -192,7 +192,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||||
if (!proceed) return;
|
if (!proceed) return;
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
MatrixClientPeg.get()
|
MatrixClientPeg.get()
|
||||||
.deleteKeyBackupVersion(this.state.backupInfo.version)
|
.deleteKeyBackupVersion(this.state.backupInfo!.version!)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.loadBackupStatus();
|
this.loadBackupStatus();
|
||||||
});
|
});
|
||||||
|
@ -285,7 +285,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let backupSigStatuses: React.ReactNode = backupSigStatus?.sigs.map((sig, i) => {
|
let backupSigStatuses: React.ReactNode | undefined = backupSigStatus?.sigs?.map((sig, i) => {
|
||||||
const deviceName = sig.device ? sig.device.getDisplayName() || sig.device.deviceId : null;
|
const deviceName = sig.device ? sig.device.getDisplayName() || sig.device.deviceId : null;
|
||||||
const validity = (sub: string): JSX.Element => (
|
const validity = (sub: string): JSX.Element => (
|
||||||
<span className={sig.valid ? "mx_SecureBackupPanel_sigValid" : "mx_SecureBackupPanel_sigInvalid"}>
|
<span className={sig.valid ? "mx_SecureBackupPanel_sigValid" : "mx_SecureBackupPanel_sigInvalid"}>
|
||||||
|
@ -354,7 +354,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||||
{},
|
{},
|
||||||
{ validity, verify, device },
|
{ validity, verify, device },
|
||||||
);
|
);
|
||||||
} else if (sig.valid && !sig.deviceTrust.isVerified()) {
|
} else if (sig.valid && !sig.deviceTrust?.isVerified()) {
|
||||||
sigStatus = _t(
|
sigStatus = _t(
|
||||||
"Backup has a <validity>valid</validity> signature from " +
|
"Backup has a <validity>valid</validity> signature from " +
|
||||||
"<verify>unverified</verify> session <device></device>",
|
"<verify>unverified</verify> session <device></device>",
|
||||||
|
@ -368,7 +368,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||||
{},
|
{},
|
||||||
{ validity, verify, device },
|
{ validity, verify, device },
|
||||||
);
|
);
|
||||||
} else if (!sig.valid && !sig.deviceTrust.isVerified()) {
|
} else if (!sig.valid && !sig.deviceTrust?.isVerified()) {
|
||||||
sigStatus = _t(
|
sigStatus = _t(
|
||||||
"Backup has an <validity>invalid</validity> signature from " +
|
"Backup has an <validity>invalid</validity> signature from " +
|
||||||
"<verify>unverified</verify> session <device></device>",
|
"<verify>unverified</verify> session <device></device>",
|
||||||
|
|
|
@ -83,7 +83,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
|
||||||
|
|
||||||
private setDevice = (deviceId: string, kind: MediaDeviceKindEnum): void => {
|
private setDevice = (deviceId: string, kind: MediaDeviceKindEnum): void => {
|
||||||
MediaDeviceHandler.instance.setDevice(deviceId, kind);
|
MediaDeviceHandler.instance.setDevice(deviceId, kind);
|
||||||
this.setState<null>({ [kind]: deviceId });
|
this.setState<any>({ [kind]: deviceId });
|
||||||
};
|
};
|
||||||
|
|
||||||
private changeWebRtcMethod = (p2p: boolean): void => {
|
private changeWebRtcMethod = (p2p: boolean): void => {
|
||||||
|
|
250
test/components/views/settings/JoinRuleSettings-test.tsx
Normal file
250
test/components/views/settings/JoinRuleSettings-test.tsx
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
/*
|
||||||
|
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 React from "react";
|
||||||
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
||||||
|
import {
|
||||||
|
EventType,
|
||||||
|
GuestAccess,
|
||||||
|
HistoryVisibility,
|
||||||
|
JoinRule,
|
||||||
|
MatrixEvent,
|
||||||
|
Room,
|
||||||
|
ClientEvent,
|
||||||
|
RoomMember,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
import { defer, IDeferred } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
|
import {
|
||||||
|
clearAllModals,
|
||||||
|
flushPromises,
|
||||||
|
getMockClientWithEventEmitter,
|
||||||
|
mockClientMethodsUser,
|
||||||
|
} from "../../../test-utils";
|
||||||
|
import { filterBoolean } from "../../../../src/utils/arrays";
|
||||||
|
import JoinRuleSettings, { JoinRuleSettingsProps } from "../../../../src/components/views/settings/JoinRuleSettings";
|
||||||
|
import { PreferredRoomVersions } from "../../../../src/utils/PreferredRoomVersions";
|
||||||
|
import SpaceStore from "../../../../src/stores/spaces/SpaceStore";
|
||||||
|
|
||||||
|
describe("<JoinRuleSettings />", () => {
|
||||||
|
const userId = "@alice:server.org";
|
||||||
|
const client = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser(userId),
|
||||||
|
getRoom: jest.fn(),
|
||||||
|
getLocalAliases: jest.fn().mockReturnValue([]),
|
||||||
|
sendStateEvent: jest.fn(),
|
||||||
|
upgradeRoom: jest.fn(),
|
||||||
|
getProfileInfo: jest.fn(),
|
||||||
|
invite: jest.fn().mockResolvedValue(undefined),
|
||||||
|
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
||||||
|
});
|
||||||
|
const roomId = "!room:server.org";
|
||||||
|
const newRoomId = "!roomUpgraded:server.org";
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
room: new Room(roomId, client, userId),
|
||||||
|
closeSettingsFn: jest.fn(),
|
||||||
|
onError: jest.fn(),
|
||||||
|
};
|
||||||
|
const getComponent = (props: Partial<JoinRuleSettingsProps> = {}) =>
|
||||||
|
render(<JoinRuleSettings {...defaultProps} {...props} />);
|
||||||
|
|
||||||
|
const setRoomStateEvents = (
|
||||||
|
room: Room,
|
||||||
|
version = "9",
|
||||||
|
joinRule?: JoinRule,
|
||||||
|
guestAccess?: GuestAccess,
|
||||||
|
history?: HistoryVisibility,
|
||||||
|
): void => {
|
||||||
|
const events = filterBoolean<MatrixEvent>([
|
||||||
|
new MatrixEvent({
|
||||||
|
type: EventType.RoomCreate,
|
||||||
|
content: { version },
|
||||||
|
sender: userId,
|
||||||
|
state_key: "",
|
||||||
|
room_id: room.roomId,
|
||||||
|
}),
|
||||||
|
guestAccess &&
|
||||||
|
new MatrixEvent({
|
||||||
|
type: EventType.RoomGuestAccess,
|
||||||
|
content: { guest_access: guestAccess },
|
||||||
|
sender: userId,
|
||||||
|
state_key: "",
|
||||||
|
room_id: room.roomId,
|
||||||
|
}),
|
||||||
|
history &&
|
||||||
|
new MatrixEvent({
|
||||||
|
type: EventType.RoomHistoryVisibility,
|
||||||
|
content: { history_visibility: history },
|
||||||
|
sender: userId,
|
||||||
|
state_key: "",
|
||||||
|
room_id: room.roomId,
|
||||||
|
}),
|
||||||
|
joinRule &&
|
||||||
|
new MatrixEvent({
|
||||||
|
type: EventType.RoomJoinRules,
|
||||||
|
content: { join_rule: joinRule },
|
||||||
|
sender: userId,
|
||||||
|
state_key: "",
|
||||||
|
room_id: room.roomId,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
room.currentState.setStateEvents(events);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client.sendStateEvent.mockReset().mockResolvedValue({ event_id: "test" });
|
||||||
|
client.isRoomEncrypted.mockReturnValue(false);
|
||||||
|
client.upgradeRoom.mockResolvedValue({ replacement_room: newRoomId });
|
||||||
|
client.getRoom.mockReturnValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Restricted rooms", () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
await clearAllModals();
|
||||||
|
});
|
||||||
|
describe("When room does not support restricted rooms", () => {
|
||||||
|
it("should not show restricted room join rule when upgrade not enabled", () => {
|
||||||
|
// room that doesnt support restricted rooms
|
||||||
|
const v8Room = new Room(roomId, client, userId);
|
||||||
|
setRoomStateEvents(v8Room, "8");
|
||||||
|
|
||||||
|
getComponent({ room: v8Room, promptUpgrade: false });
|
||||||
|
|
||||||
|
expect(screen.queryByText("Space members")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show restricted room join rule when upgrade is enabled", () => {
|
||||||
|
// room that doesnt support restricted rooms
|
||||||
|
const v8Room = new Room(roomId, client, userId);
|
||||||
|
setRoomStateEvents(v8Room, "8");
|
||||||
|
|
||||||
|
getComponent({ room: v8Room, promptUpgrade: true });
|
||||||
|
|
||||||
|
expect(screen.getByText("Space members")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Upgrade required")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("upgrades room when changing join rule to restricted", async () => {
|
||||||
|
const deferredInvites: IDeferred<any>[] = [];
|
||||||
|
// room that doesnt support restricted rooms
|
||||||
|
const v8Room = new Room(roomId, client, userId);
|
||||||
|
const parentSpace = new Room("!parentSpace:server.org", client, userId);
|
||||||
|
jest.spyOn(SpaceStore.instance, "getKnownParents").mockReturnValue(new Set([parentSpace.roomId]));
|
||||||
|
setRoomStateEvents(v8Room, "8");
|
||||||
|
const memberAlice = new RoomMember(roomId, "@alice:server.org");
|
||||||
|
const memberBob = new RoomMember(roomId, "@bob:server.org");
|
||||||
|
const memberCharlie = new RoomMember(roomId, "@charlie:server.org");
|
||||||
|
jest.spyOn(v8Room, "getMembersWithMembership").mockImplementation((membership) =>
|
||||||
|
membership === "join" ? [memberAlice, memberBob] : [memberCharlie],
|
||||||
|
);
|
||||||
|
const upgradedRoom = new Room(newRoomId, client, userId);
|
||||||
|
setRoomStateEvents(upgradedRoom);
|
||||||
|
client.getRoom.mockImplementation((id) => {
|
||||||
|
if (roomId === id) return v8Room;
|
||||||
|
if (parentSpace.roomId === id) return parentSpace;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// resolve invites by hand
|
||||||
|
// flushPromises is too blunt to test reliably
|
||||||
|
client.invite.mockImplementation(() => {
|
||||||
|
const p = defer<{}>();
|
||||||
|
deferredInvites.push(p);
|
||||||
|
return p.promise;
|
||||||
|
});
|
||||||
|
|
||||||
|
getComponent({ room: v8Room, promptUpgrade: true });
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Space members"));
|
||||||
|
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
|
||||||
|
fireEvent.click(within(dialog).getByText("Upgrade"));
|
||||||
|
|
||||||
|
expect(client.upgradeRoom).toHaveBeenCalledWith(roomId, PreferredRoomVersions.RestrictedRooms);
|
||||||
|
|
||||||
|
expect(within(dialog).getByText("Upgrading room")).toBeInTheDocument();
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(within(dialog).getByText("Loading new room")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// "create" our new room, have it come thru sync
|
||||||
|
client.getRoom.mockImplementation((id) => {
|
||||||
|
if (roomId === id) return v8Room;
|
||||||
|
if (newRoomId === id) return upgradedRoom;
|
||||||
|
if (parentSpace.roomId === id) return parentSpace;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
client.emit(ClientEvent.Room, upgradedRoom);
|
||||||
|
|
||||||
|
// invite users
|
||||||
|
expect(await screen.findByText("Sending invites... (0 out of 2)")).toBeInTheDocument();
|
||||||
|
deferredInvites.pop()!.resolve({});
|
||||||
|
expect(await screen.findByText("Sending invites... (1 out of 2)")).toBeInTheDocument();
|
||||||
|
deferredInvites.pop()!.resolve({});
|
||||||
|
|
||||||
|
// update spaces
|
||||||
|
expect(await screen.findByText("Updating space...")).toBeInTheDocument();
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// done, modal closed
|
||||||
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("upgrades room with no parent spaces or members when changing join rule to restricted", async () => {
|
||||||
|
// room that doesnt support restricted rooms
|
||||||
|
const v8Room = new Room(roomId, client, userId);
|
||||||
|
setRoomStateEvents(v8Room, "8");
|
||||||
|
const upgradedRoom = new Room(newRoomId, client, userId);
|
||||||
|
setRoomStateEvents(upgradedRoom);
|
||||||
|
|
||||||
|
getComponent({ room: v8Room, promptUpgrade: true });
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Space members"));
|
||||||
|
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
|
||||||
|
fireEvent.click(within(dialog).getByText("Upgrade"));
|
||||||
|
|
||||||
|
expect(client.upgradeRoom).toHaveBeenCalledWith(roomId, PreferredRoomVersions.RestrictedRooms);
|
||||||
|
|
||||||
|
expect(within(dialog).getByText("Upgrading room")).toBeInTheDocument();
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(within(dialog).getByText("Loading new room")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// "create" our new room, have it come thru sync
|
||||||
|
client.getRoom.mockImplementation((id) => {
|
||||||
|
if (roomId === id) return v8Room;
|
||||||
|
if (newRoomId === id) return upgradedRoom;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
client.emit(ClientEvent.Room, upgradedRoom);
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// done, modal closed
|
||||||
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -26,16 +26,18 @@ import {
|
||||||
TweakName,
|
TweakName,
|
||||||
ConditionKind,
|
ConditionKind,
|
||||||
IPushRuleCondition,
|
IPushRuleCondition,
|
||||||
|
PushRuleKind,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
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, within } from "@testing-library/react";
|
import { act, fireEvent, getByTestId, render, screen, waitFor, within } from "@testing-library/react";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
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";
|
||||||
import { StandardActions } from "../../../../src/notifications/StandardActions";
|
import { StandardActions } from "../../../../src/notifications/StandardActions";
|
||||||
import { getMockClientWithEventEmitter, mkMessage, mockClientMethodsUser } from "../../../test-utils";
|
import { clearAllModals, getMockClientWithEventEmitter, mkMessage, mockClientMethodsUser } from "../../../test-utils";
|
||||||
|
|
||||||
// don't pollute test output with error logs from mock rejections
|
// don't pollute test output with error logs from mock rejections
|
||||||
jest.mock("matrix-js-sdk/src/logger");
|
jest.mock("matrix-js-sdk/src/logger");
|
||||||
|
@ -257,6 +259,7 @@ describe("<Notifications />", () => {
|
||||||
getPushers: jest.fn(),
|
getPushers: jest.fn(),
|
||||||
getThreePids: jest.fn(),
|
getThreePids: jest.fn(),
|
||||||
setPusher: jest.fn(),
|
setPusher: jest.fn(),
|
||||||
|
removePusher: jest.fn(),
|
||||||
setPushRuleEnabled: jest.fn(),
|
setPushRuleEnabled: jest.fn(),
|
||||||
setPushRuleActions: jest.fn(),
|
setPushRuleActions: jest.fn(),
|
||||||
getRooms: jest.fn().mockReturnValue([]),
|
getRooms: jest.fn().mockReturnValue([]),
|
||||||
|
@ -274,10 +277,12 @@ describe("<Notifications />", () => {
|
||||||
sendReadReceipt: jest.fn(),
|
sendReadReceipt: jest.fn(),
|
||||||
supportsThreads: jest.fn().mockReturnValue(true),
|
supportsThreads: jest.fn().mockReturnValue(true),
|
||||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||||
|
addPushRule: jest.fn().mockResolvedValue({}),
|
||||||
|
deletePushRule: jest.fn().mockResolvedValue({}),
|
||||||
});
|
});
|
||||||
mockClient.getPushRules.mockResolvedValue(pushRules);
|
mockClient.getPushRules.mockResolvedValue(pushRules);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
mocked(randomString).mockImplementation(() => {
|
mocked(randomString).mockImplementation(() => {
|
||||||
return "testid_" + i++;
|
return "testid_" + i++;
|
||||||
|
@ -286,9 +291,17 @@ describe("<Notifications />", () => {
|
||||||
mockClient.getPushRules.mockClear().mockResolvedValue(pushRules);
|
mockClient.getPushRules.mockClear().mockResolvedValue(pushRules);
|
||||||
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.mockReset().mockResolvedValue({});
|
||||||
|
mockClient.removePusher.mockClear().mockResolvedValue({});
|
||||||
mockClient.setPushRuleActions.mockReset().mockResolvedValue({});
|
mockClient.setPushRuleActions.mockReset().mockResolvedValue({});
|
||||||
mockClient.pushRules = pushRules;
|
mockClient.pushRules = pushRules;
|
||||||
|
mockClient.getPushRules.mockClear().mockResolvedValue(pushRules);
|
||||||
|
mockClient.addPushRule.mockClear();
|
||||||
|
mockClient.deletePushRule.mockClear();
|
||||||
|
|
||||||
|
userEvent.setup();
|
||||||
|
|
||||||
|
await clearAllModals();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders spinner while loading", async () => {
|
it("renders spinner while loading", async () => {
|
||||||
|
@ -392,21 +405,30 @@ describe("<Notifications />", () => {
|
||||||
// force render
|
// force render
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
within(dialog).getByText("An error occurred whilst saving your notification preferences."),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// dismiss the dialog
|
||||||
|
fireEvent.click(within(dialog).getByText("OK"));
|
||||||
expect(screen.getByTestId("error-message")).toBeInTheDocument();
|
expect(screen.getByTestId("error-message")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("enables email notification when toggling off", async () => {
|
it("enables email notification when toggling off", async () => {
|
||||||
const testPusher = { kind: "email", pushkey: "tester@test.com" } as unknown as IPusher;
|
const testPusher = {
|
||||||
|
kind: "email",
|
||||||
|
pushkey: "tester@test.com",
|
||||||
|
app_id: "testtest",
|
||||||
|
} as unknown as IPusher;
|
||||||
mockClient.getPushers.mockResolvedValue({ pushers: [testPusher] });
|
mockClient.getPushers.mockResolvedValue({ pushers: [testPusher] });
|
||||||
await getComponentAndWait();
|
await getComponentAndWait();
|
||||||
|
|
||||||
const emailToggle = screen.getByTestId("notif-email-switch").querySelector('div[role="switch"]')!;
|
const emailToggle = screen.getByTestId("notif-email-switch").querySelector('div[role="switch"]')!;
|
||||||
fireEvent.click(emailToggle);
|
fireEvent.click(emailToggle);
|
||||||
|
|
||||||
expect(mockClient.setPusher).toHaveBeenCalledWith({
|
expect(mockClient.removePusher).toHaveBeenCalledWith(testPusher.pushkey, testPusher.app_id);
|
||||||
...testPusher,
|
|
||||||
kind: null,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -809,6 +831,66 @@ describe("<Notifications />", () => {
|
||||||
),
|
),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds a new keyword", async () => {
|
||||||
|
await getComponentAndWait();
|
||||||
|
|
||||||
|
await userEvent.type(screen.getByLabelText("Keyword"), "jest");
|
||||||
|
expect(screen.getByLabelText("Keyword")).toHaveValue("jest");
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Add"));
|
||||||
|
|
||||||
|
expect(mockClient.addPushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "jest", {
|
||||||
|
actions: [PushRuleActionName.Notify, { set_tweak: "highlight", value: false }],
|
||||||
|
pattern: "jest",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds a new keyword with same actions as existing rules when keywords rule is off", async () => {
|
||||||
|
const offContentRule = {
|
||||||
|
...bananaRule,
|
||||||
|
enabled: false,
|
||||||
|
actions: [PushRuleActionName.Notify],
|
||||||
|
};
|
||||||
|
const pushRulesWithContentOff = {
|
||||||
|
global: {
|
||||||
|
...pushRules.global,
|
||||||
|
content: [offContentRule],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockClient.pushRules = pushRulesWithContentOff;
|
||||||
|
mockClient.getPushRules.mockClear().mockResolvedValue(pushRulesWithContentOff);
|
||||||
|
|
||||||
|
await getComponentAndWait();
|
||||||
|
|
||||||
|
const keywords = screen.getByTestId("vector_mentions_keywords");
|
||||||
|
|
||||||
|
expect(within(keywords).getByLabelText("Off")).toBeChecked();
|
||||||
|
|
||||||
|
await userEvent.type(screen.getByLabelText("Keyword"), "jest");
|
||||||
|
expect(screen.getByLabelText("Keyword")).toHaveValue("jest");
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Add"));
|
||||||
|
|
||||||
|
expect(mockClient.addPushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "jest", {
|
||||||
|
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }],
|
||||||
|
pattern: "jest",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes keyword", async () => {
|
||||||
|
await getComponentAndWait();
|
||||||
|
|
||||||
|
await userEvent.type(screen.getByLabelText("Keyword"), "jest");
|
||||||
|
|
||||||
|
const keyword = screen.getByText("banana");
|
||||||
|
|
||||||
|
fireEvent.click(within(keyword.parentElement!).getByLabelText("Remove"));
|
||||||
|
|
||||||
|
expect(mockClient.deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "banana");
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("clear all notifications", () => {
|
describe("clear all notifications", () => {
|
||||||
|
|
187
test/components/views/settings/SecureBackupPanel-test.tsx
Normal file
187
test/components/views/settings/SecureBackupPanel-test.tsx
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
/*
|
||||||
|
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 React from "react";
|
||||||
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
|
import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
|
||||||
|
import SecureBackupPanel from "../../../../src/components/views/settings/SecureBackupPanel";
|
||||||
|
import { accessSecretStorage } from "../../../../src/SecurityManager";
|
||||||
|
|
||||||
|
jest.mock("../../../../src/SecurityManager", () => ({
|
||||||
|
accessSecretStorage: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("<SecureBackupPanel />", () => {
|
||||||
|
const userId = "@alice:server.org";
|
||||||
|
const client = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser(userId),
|
||||||
|
checkKeyBackup: jest.fn(),
|
||||||
|
isKeyBackupKeyStored: jest.fn(),
|
||||||
|
isSecretStorageReady: jest.fn(),
|
||||||
|
getKeyBackupEnabled: jest.fn(),
|
||||||
|
getKeyBackupVersion: jest.fn().mockReturnValue("1"),
|
||||||
|
isKeyBackupTrusted: jest.fn().mockResolvedValue(true),
|
||||||
|
getClientWellKnown: jest.fn(),
|
||||||
|
deleteKeyBackupVersion: jest.fn(),
|
||||||
|
});
|
||||||
|
// @ts-ignore allow it
|
||||||
|
client.crypto = {
|
||||||
|
secretStorage: { hasKey: jest.fn() },
|
||||||
|
getSessionBackupPrivateKey: jest.fn(),
|
||||||
|
} as unknown as Crypto;
|
||||||
|
|
||||||
|
const getComponent = () => render(<SecureBackupPanel />);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client.checkKeyBackup.mockResolvedValue({
|
||||||
|
backupInfo: {
|
||||||
|
version: "1",
|
||||||
|
algorithm: "test",
|
||||||
|
auth_data: {
|
||||||
|
public_key: "1234",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trustInfo: {
|
||||||
|
usable: false,
|
||||||
|
sigs: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mocked(client.crypto!.secretStorage.hasKey).mockClear().mockResolvedValue(false);
|
||||||
|
client.deleteKeyBackupVersion.mockClear().mockResolvedValue();
|
||||||
|
client.getKeyBackupVersion.mockClear();
|
||||||
|
client.isKeyBackupTrusted.mockClear();
|
||||||
|
|
||||||
|
mocked(accessSecretStorage).mockClear().mockResolvedValue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays a loader while checking keybackup", async () => {
|
||||||
|
getComponent();
|
||||||
|
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
||||||
|
await flushPromises();
|
||||||
|
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles null backup info", async () => {
|
||||||
|
// checkKeyBackup can fail and return null for various reasons
|
||||||
|
client.checkKeyBackup.mockResolvedValue(null);
|
||||||
|
getComponent();
|
||||||
|
// flush checkKeyBackup promise
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// no backup info
|
||||||
|
expect(screen.getByText("Back up your keys before signing out to avoid losing them.")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suggests connecting session to key backup when backup exists", async () => {
|
||||||
|
const { container } = getComponent();
|
||||||
|
// flush checkKeyBackup promise
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays when session is connected to key backup", async () => {
|
||||||
|
client.getKeyBackupEnabled.mockReturnValue(true);
|
||||||
|
getComponent();
|
||||||
|
// flush checkKeyBackup promise
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(screen.getByText("✅ This session is backing up your keys.")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("asks for confirmation before deleting a backup", async () => {
|
||||||
|
getComponent();
|
||||||
|
// flush checkKeyBackup promise
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Delete Backup"));
|
||||||
|
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
within(dialog).getByText(
|
||||||
|
"Are you sure? You will lose your encrypted messages if your keys are not backed up properly.",
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(within(dialog).getByText("Cancel"));
|
||||||
|
|
||||||
|
expect(client.deleteKeyBackupVersion).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes backup after confirmation", async () => {
|
||||||
|
client.checkKeyBackup
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
backupInfo: {
|
||||||
|
version: "1",
|
||||||
|
algorithm: "test",
|
||||||
|
auth_data: {
|
||||||
|
public_key: "1234",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trustInfo: {
|
||||||
|
usable: false,
|
||||||
|
sigs: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mockResolvedValue(null);
|
||||||
|
getComponent();
|
||||||
|
// flush checkKeyBackup promise
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Delete Backup"));
|
||||||
|
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
within(dialog).getByText(
|
||||||
|
"Are you sure? You will lose your encrypted messages if your keys are not backed up properly.",
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(within(dialog).getByTestId("dialog-primary-button"));
|
||||||
|
|
||||||
|
expect(client.deleteKeyBackupVersion).toHaveBeenCalledWith("1");
|
||||||
|
|
||||||
|
// delete request
|
||||||
|
await flushPromises();
|
||||||
|
// refresh backup info
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets secret storage", async () => {
|
||||||
|
mocked(client.crypto!.secretStorage.hasKey).mockClear().mockResolvedValue(true);
|
||||||
|
getComponent();
|
||||||
|
// flush checkKeyBackup promise
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
client.getKeyBackupVersion.mockClear();
|
||||||
|
client.isKeyBackupTrusted.mockClear();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Reset"));
|
||||||
|
|
||||||
|
// enter loading state
|
||||||
|
expect(accessSecretStorage).toHaveBeenCalled();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// backup status refreshed
|
||||||
|
expect(client.getKeyBackupVersion).toHaveBeenCalled();
|
||||||
|
expect(client.isKeyBackupTrusted).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,116 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<SecureBackupPanel /> suggests connecting session to key backup when backup exists 1`] = `
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
This session is
|
||||||
|
<b>
|
||||||
|
not backing up your keys
|
||||||
|
</b>
|
||||||
|
, but you do have an existing backup you can restore from and add to going forward.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.
|
||||||
|
</p>
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
Advanced
|
||||||
|
</summary>
|
||||||
|
<table
|
||||||
|
class="mx_SecureBackupPanel_statusList"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="row"
|
||||||
|
>
|
||||||
|
Backup key stored:
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
not stored
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="row"
|
||||||
|
>
|
||||||
|
Backup key cached:
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
not found locally
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="row"
|
||||||
|
>
|
||||||
|
Secret storage public key:
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
not found
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="row"
|
||||||
|
>
|
||||||
|
Secret storage:
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
not ready
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="row"
|
||||||
|
>
|
||||||
|
Backup version:
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
1
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="row"
|
||||||
|
>
|
||||||
|
Algorithm:
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
test
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Backup is not signed by any of your sessions
|
||||||
|
</div>
|
||||||
|
<div />
|
||||||
|
</details>
|
||||||
|
<div
|
||||||
|
class="mx_SecureBackupPanel_buttonRow"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Connect this session to Key Backup
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Delete Backup
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -16,10 +16,11 @@ limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { render } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
|
||||||
import VoiceUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/VoiceUserSettingsTab";
|
import VoiceUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/VoiceUserSettingsTab";
|
||||||
import MediaDeviceHandler from "../../../../../../src/MediaDeviceHandler";
|
import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../../src/MediaDeviceHandler";
|
||||||
|
import { flushPromises } from "../../../../../test-utils";
|
||||||
|
|
||||||
jest.mock("../../../../../../src/MediaDeviceHandler");
|
jest.mock("../../../../../../src/MediaDeviceHandler");
|
||||||
const MediaDeviceHandlerMock = mocked(MediaDeviceHandler);
|
const MediaDeviceHandlerMock = mocked(MediaDeviceHandler);
|
||||||
|
@ -27,8 +28,69 @@ const MediaDeviceHandlerMock = mocked(MediaDeviceHandler);
|
||||||
describe("<VoiceUserSettingsTab />", () => {
|
describe("<VoiceUserSettingsTab />", () => {
|
||||||
const getComponent = (): React.ReactElement => <VoiceUserSettingsTab />;
|
const getComponent = (): React.ReactElement => <VoiceUserSettingsTab />;
|
||||||
|
|
||||||
|
const audioIn1 = {
|
||||||
|
deviceId: "1",
|
||||||
|
groupId: "g1",
|
||||||
|
kind: MediaDeviceKindEnum.AudioInput,
|
||||||
|
label: "Audio input test 1",
|
||||||
|
};
|
||||||
|
const videoIn1 = {
|
||||||
|
deviceId: "2",
|
||||||
|
groupId: "g1",
|
||||||
|
kind: MediaDeviceKindEnum.VideoInput,
|
||||||
|
label: "Video input test 1",
|
||||||
|
};
|
||||||
|
const videoIn2 = {
|
||||||
|
deviceId: "3",
|
||||||
|
groupId: "g1",
|
||||||
|
kind: MediaDeviceKindEnum.VideoInput,
|
||||||
|
label: "Video input test 2",
|
||||||
|
};
|
||||||
|
const defaultMediaDevices = {
|
||||||
|
[MediaDeviceKindEnum.AudioOutput]: [],
|
||||||
|
[MediaDeviceKindEnum.AudioInput]: [audioIn1],
|
||||||
|
[MediaDeviceKindEnum.VideoInput]: [videoIn1, videoIn2],
|
||||||
|
} as unknown as IMediaDevices;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
MediaDeviceHandlerMock.hasAnyLabeledDevices.mockResolvedValue(true);
|
||||||
|
MediaDeviceHandlerMock.getDevices.mockResolvedValue(defaultMediaDevices);
|
||||||
|
|
||||||
|
// @ts-ignore bad mocking
|
||||||
|
MediaDeviceHandlerMock.instance = { setDevice: jest.fn() };
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("devices", () => {
|
||||||
|
it("renders dropdowns for input devices", async () => {
|
||||||
|
render(getComponent());
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(screen.getByLabelText("Microphone")).toHaveDisplayValue(audioIn1.label);
|
||||||
|
expect(screen.getByLabelText("Camera")).toHaveDisplayValue(videoIn1.label);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates device", async () => {
|
||||||
|
render(getComponent());
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText("Camera"), { target: { value: videoIn2.deviceId } });
|
||||||
|
|
||||||
|
expect(MediaDeviceHandlerMock.instance.setDevice).toHaveBeenCalledWith(
|
||||||
|
videoIn2.deviceId,
|
||||||
|
MediaDeviceKindEnum.VideoInput,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText("Camera")).toHaveDisplayValue(videoIn2.label);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render dropdown when no devices exist for type", async () => {
|
||||||
|
render(getComponent());
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(screen.getByText("No Audio Outputs detected")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByLabelText("Audio Output")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders audio processing settings", () => {
|
it("renders audio processing settings", () => {
|
||||||
|
|
Loading…
Reference in a new issue