Merge pull request #61 from yougotwill/fix/no-ref/display_name_validation

pull/3281/head
Will G 2 months ago committed by GitHub
commit edb86b2e2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -18,8 +18,10 @@ import { Spacer2XL, Spacer3XL, SpacerLG, SpacerSM, SpacerXL } from '../../basic/
import { CopyToClipboardButton } from '../../buttons/CopyToClipboardButton'; import { CopyToClipboardButton } from '../../buttons/CopyToClipboardButton';
import { SessionInput } from '../../inputs'; import { SessionInput } from '../../inputs';
import { SessionSpinner } from '../../loading'; import { SessionSpinner } from '../../loading';
import { sanitizeDisplayNameOrToast } from '../../registration/utils';
import { ProfileHeader, ProfileName, QRView } from './components'; import { ProfileHeader, ProfileName, QRView } from './components';
import { EmptyDisplayNameError, RetrieveDisplayNameError } from '../../../session/utils/errors';
import { localize } from '../../../localization/localeTools';
import { sanitizeDisplayNameOrToast } from '../../registration/utils';
// #region Shortcuts // #region Shortcuts
const handleKeyQRMode = ( const handleKeyQRMode = (
@ -173,6 +175,7 @@ export const EditProfileDialog = () => {
const [profileName, setProfileName] = useState(_profileName); const [profileName, setProfileName] = useState(_profileName);
const [updatedProfileName, setUpdateProfileName] = useState(profileName); const [updatedProfileName, setUpdateProfileName] = useState(profileName);
const [profileNameError, setProfileNameError] = useState<string | undefined>(undefined); const [profileNameError, setProfileNameError] = useState<string | undefined>(undefined);
const [cannotContinue, setCannotContinue] = useState(true);
const copyButtonRef = useRef<HTMLButtonElement>(null); const copyButtonRef = useRef<HTMLButtonElement>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -207,12 +210,15 @@ export const EditProfileDialog = () => {
: undefined; : undefined;
const onClickOK = async () => { const onClickOK = async () => {
if (isEmpty(profileName) || !isEmpty(profileNameError)) {
return;
}
try { try {
setLoading(true); setLoading(true);
const sanitizedName = sanitizeDisplayNameOrToast(profileName);
// this should never happen, but just in case
if (isEmpty(sanitizedName)) {
return;
}
// Note: this will not throw, but just truncate the display name if it is too long. // Note: this will not throw, but just truncate the display name if it is too long.
// I guess it is expected as there is no UI to show anything else than a generic error? // I guess it is expected as there is no UI to show anything else than a generic error?
const validName = await ProfileManager.updateOurProfileDisplayName(profileName); const validName = await ProfileManager.updateOurProfileDisplayName(profileName);
@ -221,7 +227,13 @@ export const EditProfileDialog = () => {
setMode('default'); setMode('default');
} catch (err) { } catch (err) {
window.log.error('Profile update error', err); window.log.error('Profile update error', err);
setProfileNameError(window.i18n('errorUnknown')); setCannotContinue(true);
if (err instanceof EmptyDisplayNameError || err instanceof RetrieveDisplayNameError) {
setProfileNameError(localize('displayNameErrorDescription').toString());
} else {
setProfileNameError(localize('errorUnknown').toString());
}
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -324,8 +336,8 @@ export const EditProfileDialog = () => {
placeholder={window.i18n('displayNameEnter')} placeholder={window.i18n('displayNameEnter')}
value={profileName} value={profileName}
onValueChanged={(name: string) => { onValueChanged={(name: string) => {
const sanitizedName = sanitizeDisplayNameOrToast(name, setProfileNameError); setProfileName(name);
setProfileName(sanitizedName); setCannotContinue(false);
}} }}
editable={!loading} editable={!loading}
tabIndex={0} tabIndex={0}
@ -381,7 +393,7 @@ export const EditProfileDialog = () => {
<SessionButton <SessionButton
text={window.i18n('save')} text={window.i18n('save')}
onClick={onClickOK} onClick={onClickOK}
disabled={loading} disabled={cannotContinue}
dataTestId="save-button-profile-update" dataTestId="save-button-profile-update"
/> />
) )

@ -1,6 +1,7 @@
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import useMount from 'react-use/lib/useMount'; import useMount from 'react-use/lib/useMount';
import { useState } from 'react';
import { SettingsKey } from '../../../data/settings-key'; import { SettingsKey } from '../../../data/settings-key';
import { mnDecode } from '../../../session/crypto/mnemonic'; import { mnDecode } from '../../../session/crypto/mnemonic';
import { ProfileManager } from '../../../session/profile_manager/ProfileManager'; import { ProfileManager } from '../../../session/profile_manager/ProfileManager';
@ -32,22 +33,21 @@ import { SessionInput } from '../../inputs';
import { resetRegistration } from '../RegistrationStages'; import { resetRegistration } from '../RegistrationStages';
import { ContinueButton, OnboardDescription, OnboardHeading } from '../components'; import { ContinueButton, OnboardDescription, OnboardHeading } from '../components';
import { BackButtonWithinContainer } from '../components/BackButton'; import { BackButtonWithinContainer } from '../components/BackButton';
import { displayNameIsValid, sanitizeDisplayNameOrToast } from '../utils'; import { sanitizeDisplayNameOrToast } from '../utils';
import { RetrieveDisplayNameError } from '../../../session/utils/errors'; import { EmptyDisplayNameError, RetrieveDisplayNameError } from '../../../session/utils/errors';
import { localize } from '../../../localization/localeTools'; import { localize } from '../../../localization/localeTools';
export type AccountDetails = { type AccountCreateDetails = {
recoveryPassword: string; recoveryPassword: string;
displayName?: string; displayName: string;
}; };
async function signUp(signUpDetails: AccountDetails) { async function signUp(signUpDetails: AccountCreateDetails) {
const { displayName, recoveryPassword } = signUpDetails; const { displayName, recoveryPassword } = signUpDetails;
try { try {
const validDisplayName = displayNameIsValid(displayName);
await resetRegistration(); await resetRegistration();
await registerSingleDevice(recoveryPassword, 'english', validDisplayName); await registerSingleDevice(recoveryPassword, 'english', displayName);
await Storage.put(SettingsKey.hasSyncedInitialConfigurationItem, Date.now()); await Storage.put(SettingsKey.hasSyncedInitialConfigurationItem, Date.now());
await setSignWithRecoveryPhrase(false); await setSignWithRecoveryPhrase(false);
trigger('openInbox'); trigger('openInbox');
@ -64,6 +64,8 @@ export const CreateAccount = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [cannotContinue, setCannotContinue] = useState(true);
const generateMnemonicAndKeyPair = async () => { const generateMnemonicAndKeyPair = async () => {
if (recoveryPassword === '') { if (recoveryPassword === '') {
const mnemonic = await generateMnemonic(); const mnemonic = await generateMnemonic();
@ -89,13 +91,16 @@ export const CreateAccount = () => {
}); });
const signUpWithDetails = async () => { const signUpWithDetails = async () => {
if (isEmpty(displayName) || !isEmpty(displayNameError)) {
return;
}
try { try {
const sanitizedName = sanitizeDisplayNameOrToast(displayName);
// this should never happen, but just in case
if (isEmpty(sanitizedName)) {
return;
}
// this throws if the display name is too long // this throws if the display name is too long
const validName = await ProfileManager.updateOurProfileDisplayNameOnboarding(displayName); const validName = await ProfileManager.updateOurProfileDisplayNameOnboarding(sanitizedName);
await signUp({ await signUp({
displayName: validName, displayName: validName,
@ -107,16 +112,17 @@ export const CreateAccount = () => {
window.log.error( window.log.error(
`[onboarding] create account: signUpWithDetails failed! Error: ${err.message || String(err)}` `[onboarding] create account: signUpWithDetails failed! Error: ${err.message || String(err)}`
); );
setCannotContinue(true);
dispatch(setAccountCreationStep(AccountCreation.DisplayName)); dispatch(setAccountCreationStep(AccountCreation.DisplayName));
if (err instanceof RetrieveDisplayNameError) { if (err instanceof EmptyDisplayNameError || err instanceof RetrieveDisplayNameError) {
dispatch(setDisplayNameError(localize('displayNameErrorDescription').toString())); dispatch(setDisplayNameError(localize('displayNameErrorDescription').toString()));
return; } else {
// Note: we have to assume here that libsession threw an error because the name was too long since we covered the other cases.
// The error reported by libsession is not localized
dispatch(setDisplayNameError(localize('displayNameErrorDescriptionShorter').toString()));
} }
// Note: we have to assume here that libsession threw an error because the name was too long since we covered the other cases.
// The error reported by libsession is not localized
dispatch(setDisplayNameError(localize('displayNameErrorDescriptionShorter').toString()));
} }
}; };
@ -150,18 +156,15 @@ export const CreateAccount = () => {
placeholder={window.i18n('displayNameEnter')} placeholder={window.i18n('displayNameEnter')}
value={displayName} value={displayName}
onValueChanged={(name: string) => { onValueChanged={(name: string) => {
const sanitizedName = sanitizeDisplayNameOrToast(name, setDisplayNameError, dispatch); dispatch(setDisplayName(name));
dispatch(setDisplayName(sanitizedName)); setCannotContinue(false);
}} }}
onEnterPressed={signUpWithDetails} onEnterPressed={signUpWithDetails}
error={displayNameError} error={displayNameError}
inputDataTestId="display-name-input" inputDataTestId="display-name-input"
/> />
<SpacerLG /> <SpacerLG />
<ContinueButton <ContinueButton onClick={signUpWithDetails} disabled={cannotContinue} />
onClick={signUpWithDetails}
disabled={isEmpty(displayName) || !isEmpty(displayNameError)}
/>
</Flex> </Flex>
</BackButtonWithinContainer> </BackButtonWithinContainer>
); );

@ -1,12 +1,17 @@
import { Dispatch } from '@reduxjs/toolkit'; import { Dispatch } from '@reduxjs/toolkit';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useState } from 'react';
import { ONBOARDING_TIMES } from '../../../session/constants'; import { ONBOARDING_TIMES } from '../../../session/constants';
import { InvalidWordsError, NotEnoughWordsError } from '../../../session/crypto/mnemonic'; import { InvalidWordsError, NotEnoughWordsError } from '../../../session/crypto/mnemonic';
import { ProfileManager } from '../../../session/profile_manager/ProfileManager'; import { ProfileManager } from '../../../session/profile_manager/ProfileManager';
import { PromiseUtils } from '../../../session/utils'; import { PromiseUtils } from '../../../session/utils';
import { TaskTimedOutError } from '../../../session/utils/Promise'; import { TaskTimedOutError } from '../../../session/utils/Promise';
import { NotFoundError, RetrieveDisplayNameError } from '../../../session/utils/errors'; import {
EmptyDisplayNameError,
NotFoundError,
RetrieveDisplayNameError,
} from '../../../session/utils/errors';
import { trigger } from '../../../shims/events'; import { trigger } from '../../../shims/events';
import { import {
AccountRestoration, AccountRestoration,
@ -41,11 +46,14 @@ import { resetRegistration } from '../RegistrationStages';
import { ContinueButton, OnboardDescription, OnboardHeading } from '../components'; import { ContinueButton, OnboardDescription, OnboardHeading } from '../components';
import { BackButtonWithinContainer } from '../components/BackButton'; import { BackButtonWithinContainer } from '../components/BackButton';
import { useRecoveryProgressEffect } from '../hooks'; import { useRecoveryProgressEffect } from '../hooks';
import { displayNameIsValid, sanitizeDisplayNameOrToast } from '../utils';
import { AccountDetails } from './CreateAccount';
import { localize } from '../../../localization/localeTools'; import { localize } from '../../../localization/localeTools';
import { sanitizeDisplayNameOrToast } from '../utils';
type AccountRestoreDetails = AccountDetails & { dispatch: Dispatch; abortSignal?: AbortSignal }; type AccountRestoreDetails = {
recoveryPassword: string;
dispatch: Dispatch;
abortSignal?: AbortSignal;
};
export async function finishRestore(pubkey: string, displayName: string) { export async function finishRestore(pubkey: string, displayName: string) {
await setSignWithRecoveryPhrase(true); await setSignWithRecoveryPhrase(true);
@ -96,21 +104,14 @@ async function signInWithNewDisplayName({
displayName, displayName,
recoveryPassword, recoveryPassword,
dispatch, dispatch,
}: AccountRestoreDetails) { }: AccountRestoreDetails & { displayName: string }) {
try { try {
const validDisplayName = displayNameIsValid(displayName);
await resetRegistration(); await resetRegistration();
await registerSingleDevice( await registerSingleDevice(recoveryPassword, 'english', displayName, async (pubkey: string) => {
recoveryPassword, dispatch(setHexGeneratedPubKey(pubkey));
'english', dispatch(setDisplayName(displayName));
validDisplayName, await finishRestore(pubkey, displayName);
async (pubkey: string) => { });
dispatch(setHexGeneratedPubKey(pubkey));
dispatch(setDisplayName(validDisplayName));
await finishRestore(pubkey, validDisplayName);
}
);
} catch (e) { } catch (e) {
await resetRegistration(); await resetRegistration();
throw e; throw e;
@ -129,6 +130,8 @@ export const RestoreAccount = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [cannotContinue, setCannotContinue] = useState(true);
useRecoveryProgressEffect(); useRecoveryProgressEffect();
const recoverAndFetchDisplayName = async () => { const recoverAndFetchDisplayName = async () => {
@ -171,18 +174,20 @@ export const RestoreAccount = () => {
}; };
const recoverAndEnterDisplayName = async () => { const recoverAndEnterDisplayName = async () => {
if ( if (isEmpty(recoveryPassword) || !isEmpty(recoveryPasswordError)) {
isEmpty(recoveryPassword) ||
!isEmpty(recoveryPasswordError) ||
isEmpty(displayName) ||
!isEmpty(displayNameError)
) {
return; return;
} }
try { try {
const sanitizedName = sanitizeDisplayNameOrToast(displayName);
// this should never happen, but just in case
if (isEmpty(sanitizedName)) {
return;
}
// this will throw if the display name is too long // this will throw if the display name is too long
const validName = await ProfileManager.updateOurProfileDisplayNameOnboarding(displayName); const validName = await ProfileManager.updateOurProfileDisplayNameOnboarding(sanitizedName);
const trimmedPassword = recoveryPassword.trim(); const trimmedPassword = recoveryPassword.trim();
setRecoveryPassword(trimmedPassword); setRecoveryPassword(trimmedPassword);
@ -196,16 +201,17 @@ export const RestoreAccount = () => {
window.log.error( window.log.error(
`[onboarding] restore account: Failed with new display name! Error: ${err.message || String(err)}` `[onboarding] restore account: Failed with new display name! Error: ${err.message || String(err)}`
); );
setCannotContinue(true);
dispatch(setAccountRestorationStep(AccountRestoration.DisplayName)); dispatch(setAccountRestorationStep(AccountRestoration.DisplayName));
if (err instanceof RetrieveDisplayNameError) { if (err instanceof EmptyDisplayNameError || err instanceof RetrieveDisplayNameError) {
dispatch(setDisplayNameError(localize('displayNameErrorDescription').toString())); dispatch(setDisplayNameError(localize('displayNameErrorDescription').toString()));
return; } else {
// Note: we have to assume here that libsession threw an error because the name was too long since we covered the other cases.
// The error reported by libsession is not localized
dispatch(setDisplayNameError(localize('displayNameErrorDescriptionShorter').toString()));
} }
// Note: we have to assume here that libsession threw an error because the name was too long since we covered the other cases.
// The error reported by libsession is not localized
dispatch(setDisplayNameError(localize('displayNameErrorDescriptionShorter').toString()));
} }
}; };
@ -313,12 +319,8 @@ export const RestoreAccount = () => {
placeholder={window.i18n('displayNameEnter')} placeholder={window.i18n('displayNameEnter')}
value={displayName} value={displayName}
onValueChanged={(name: string) => { onValueChanged={(name: string) => {
const sanitizedName = sanitizeDisplayNameOrToast( dispatch(setDisplayName(name));
name, setCannotContinue(false);
setDisplayNameError,
dispatch
);
dispatch(setDisplayName(sanitizedName));
}} }}
onEnterPressed={recoverAndEnterDisplayName} onEnterPressed={recoverAndEnterDisplayName}
error={displayNameError} error={displayNameError}
@ -328,10 +330,7 @@ export const RestoreAccount = () => {
<ContinueButton <ContinueButton
onClick={recoverAndEnterDisplayName} onClick={recoverAndEnterDisplayName}
disabled={ disabled={
isEmpty(recoveryPassword) || isEmpty(recoveryPassword) || !isEmpty(recoveryPasswordError) || cannotContinue
!isEmpty(recoveryPasswordError) ||
isEmpty(displayName) ||
!isEmpty(displayNameError)
} }
/> />
</Flex> </Flex>

@ -1,37 +1,12 @@
import { Dispatch } from '@reduxjs/toolkit'; import { EmptyDisplayNameError } from '../../../session/utils/errors';
import { sanitizeSessionUsername } from '../../../session/utils/String'; import { sanitizeSessionUsername } from '../../../session/utils/String';
export function sanitizeDisplayNameOrToast( export function sanitizeDisplayNameOrToast(displayName: string) {
displayName: string, const sanitizedName = sanitizeSessionUsername(displayName).trim();
// can be a useState or redux function
onDisplayNameError: (error: string | undefined) => any, if (!sanitizedName) {
dispatch?: Dispatch throw new EmptyDisplayNameError();
) {
const sanitizedName = sanitizeSessionUsername(displayName);
const errorString = !sanitizedName ? window.i18n('displayNameErrorDescription') : undefined;
if (dispatch) {
dispatch(onDisplayNameError(errorString));
} else {
onDisplayNameError(errorString); // this is is either calling dispatch in the caller or just `setDisplayNameError`
} }
return sanitizedName; return sanitizedName;
} }
/**
* Returns undefined if an error happened, or the trim userName.
*
* Be sure to use the trimmed userName for creating the account.
*/
export const displayNameIsValid = (displayName?: string): string => {
if (!displayName) {
throw new Error(window.i18n('displayNameErrorDescription'));
}
const trimName = displayName.trim();
if (!trimName) {
throw new Error(window.i18n('displayNameErrorDescription'));
}
return trimName;
};

@ -100,18 +100,17 @@ async function updateProfileOfContact(
* When registering a user/linking a device, we want to enforce a limit on the displayName length. * When registering a user/linking a device, we want to enforce a limit on the displayName length.
* That limit is enforced by libsession when calling `setName` on the `UserConfigWrapper`. * That limit is enforced by libsession when calling `setName` on the `UserConfigWrapper`.
* `updateOurProfileDisplayNameOnboarding` is used to create a temporary `UserConfigWrapper`, call `setName` on it and release the memory used by the wrapper. * `updateOurProfileDisplayNameOnboarding` is used to create a temporary `UserConfigWrapper`, call `setName` on it and release the memory used by the wrapper.
* @returns the set displayName set if no error where thrown. * @returns the set displayName set if no error where thrown
* @note Make sure the displayName has been trimmed and validated first.
*/ */
async function updateOurProfileDisplayNameOnboarding(newName: string) { async function updateOurProfileDisplayNameOnboarding(newName: string) {
const cleanName = sanitizeSessionUsername(newName).trim();
try { try {
// create a temp user config wrapper to test the display name with libsession // create a temp user config wrapper to test the display name with libsession
const privKey = new Uint8Array(64); const privKey = new Uint8Array(64);
crypto.getRandomValues(privKey); crypto.getRandomValues(privKey);
await UserConfigWrapperActions.init(privKey, null); await UserConfigWrapperActions.init(privKey, null);
// this throws if the name is too long // this throws if the name is too long
await UserConfigWrapperActions.setName(cleanName); await UserConfigWrapperActions.setName(newName);
const appliedName = await UserConfigWrapperActions.getName(); const appliedName = await UserConfigWrapperActions.getName();
if (isNil(appliedName)) { if (isNil(appliedName)) {

@ -67,12 +67,17 @@ export class HTTPError extends Error {
} }
} }
/**
* Base error class for all errors in the session module.
*
* @note if you make a custom error with a custom message, make sure to restore the prototype chain again using the new class prototype.
*/
class BaseError extends Error { class BaseError extends Error {
constructor(message: string) { constructor(message: string) {
super(message); super(message);
this.name = this.constructor.name; this.name = this.constructor.name;
// restore prototype chain // NOTE Restores prototype chain. Make sure to reference the new class prototype!
Object.setPrototypeOf(this, SnodeResponseError.prototype); Object.setPrototypeOf(this, BaseError.prototype);
} }
} }
@ -85,12 +90,19 @@ export class InvalidMessage extends BaseError {}
export class SnodeResponseError extends BaseError { export class SnodeResponseError extends BaseError {
constructor(message = 'sessionRpc could not talk to node') { constructor(message = 'sessionRpc could not talk to node') {
super(message); super(message);
Object.setPrototypeOf(this, SnodeResponseError.prototype);
} }
} }
export class RetrieveDisplayNameError extends Error { export class RetrieveDisplayNameError extends BaseError {
constructor(message = 'failed to retrieve display name after setting it') { constructor(message = 'failed to retrieve display name after setting it') {
super(message); super(message);
// restore prototype chain Object.setPrototypeOf(this, RetrieveDisplayNameError.prototype);
Object.setPrototypeOf(this, SnodeResponseError.prototype); }
}
export class EmptyDisplayNameError extends BaseError {
constructor(message = 'display name is empty') {
super(message);
Object.setPrototypeOf(this, EmptyDisplayNameError.prototype);
} }
} }

@ -1,6 +1,5 @@
import { expect } from 'chai'; import { expect } from 'chai';
import Sinon from 'sinon'; import Sinon from 'sinon';
import { displayNameIsValid } from '../../../../components/registration/utils';
import { getSwarmPollingInstance } from '../../../../session/apis/snode_api'; import { getSwarmPollingInstance } from '../../../../session/apis/snode_api';
import { PubKey } from '../../../../session/types'; import { PubKey } from '../../../../session/types';
import { import {
@ -10,6 +9,8 @@ import {
} from '../../../../util/accountManager'; } from '../../../../util/accountManager';
import { TestUtils } from '../../../test-utils'; import { TestUtils } from '../../../test-utils';
import { stubWindow } from '../../../test-utils/utils'; import { stubWindow } from '../../../test-utils/utils';
import { sanitizeDisplayNameOrToast } from '../../../../components/registration/utils';
import { EmptyDisplayNameError } from '../../../../session/utils/errors';
describe('Onboarding', () => { describe('Onboarding', () => {
const polledDisplayName = 'Hello World'; const polledDisplayName = 'Hello World';
@ -28,31 +29,28 @@ describe('Onboarding', () => {
Sinon.restore(); Sinon.restore();
}); });
describe('displayNameIsValid', () => { describe('sanitizeDisplayNameOrToast', () => {
it('should throw an error if the display name is undefined', async () => { it('should throw an error if the display name is undefined', async () => {
try { try {
displayNameIsValid(undefined); sanitizeDisplayNameOrToast('');
} catch (error) { } catch (error) {
error.should.be.an.instanceOf(Error); error.should.be.an.instanceOf(EmptyDisplayNameError);
error.message.should.equal(window.i18n('displayNameErrorDescription'));
} }
}); });
it('should throw an error if the display name is empty after trimming', async () => { it('should throw an error if the display name is empty after trimming', async () => {
try { try {
displayNameIsValid(' '); sanitizeDisplayNameOrToast(' ');
} catch (error) { } catch (error) {
error.should.be.an.instanceOf(Error); error.should.be.an.instanceOf(EmptyDisplayNameError);
error.message.should.equal(window.i18n('displayNameErrorDescription'));
} }
}); });
it('if the display name is valid it should be returned', async () => { it('if the display name is valid it should be returned', async () => {
try { try {
const displayName = 'Hello World'; const displayName = 'Hello World';
const validDisplayName = displayNameIsValid(displayName); const validDisplayName = sanitizeDisplayNameOrToast(displayName);
expect(validDisplayName, `should equal ${displayName}`).to.equal(displayName); expect(validDisplayName, `should equal ${displayName}`).to.equal(displayName);
} catch (error) { } catch (error) {
error.should.not.be.an.instanceOf(Error); error.should.not.be.an.instanceOf(EmptyDisplayNameError);
error.message.should.not.equal(window.i18n('displayNameErrorDescription'));
} }
}); });
}); });

Loading…
Cancel
Save