Skip to content

Commit 0d7e0c7

Browse files
committed
fix(password): Add inline confirm password validation
1 parent c41c918 commit 0d7e0c7

5 files changed

Lines changed: 113 additions & 13 deletions

File tree

packages/fxa-content-server/app/scripts/views/sign_up_password.js

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
import { assign } from 'underscore';
5+
import { assign, debounce } from 'underscore';
66
import AuthErrors from '../lib/auth-errors';
77
import CWTSOnSignupPasswordExperimentMixin from './mixins/cwts-on-signup-password-experiment-mixin';
88
import Cocktail from 'cocktail';
@@ -22,6 +22,10 @@ import Template from 'templates/sign_up_password.mustache';
2222

2323
const t = msg => msg;
2424

25+
const PASSWORD_INPUT_SELECTOR = '#password';
26+
const VPASSWORD_INPUT_SELECTOR = '#vpassword';
27+
const DELAY_BEFORE_PASSWORD_CHECK_MS = 1500;
28+
2529
const proto = FormView.prototype;
2630
const SignUpPasswordView = FormView.extend({
2731
template: Template,
@@ -32,6 +36,7 @@ const SignUpPasswordView = FormView.extend({
3236

3337
events: assign({}, FormView.prototype.events, {
3438
'click .use-different': preventDefaultThen('useDifferentAccount'),
39+
'keyup #vpassword': '_onConfirmPasswordKeyUp',
3540
}),
3641

3742
useDifferentAccount() {
@@ -63,6 +68,14 @@ const SignUpPasswordView = FormView.extend({
6368
canChangeAccount: !this.model.get('forceEmail'),
6469
email: this.getAccount().get('email'),
6570
});
71+
72+
// We debounce the password check function to give the password input
73+
// some smarts. There will be a slight delay to show the tooltip which
74+
// makes the experience less janky,
75+
this.checkPasswordsMatchDebounce = debounce(
76+
this._checkPasswordsMatch,
77+
DELAY_BEFORE_PASSWORD_CHECK_MS
78+
);
6679
},
6780

6881
isValidEnd() {
@@ -75,7 +88,10 @@ const SignUpPasswordView = FormView.extend({
7588

7689
showValidationErrorsEnd() {
7790
if (!this._doPasswordsMatch()) {
78-
this.displayError(AuthErrors.toError('PASSWORDS_DO_NOT_MATCH'));
91+
this.showValidationError(
92+
this.$(VPASSWORD_INPUT_SELECTOR),
93+
AuthErrors.toError('PASSWORDS_DO_NOT_MATCH')
94+
);
7995
}
8096
},
8197

@@ -97,16 +113,26 @@ const SignUpPasswordView = FormView.extend({
97113
},
98114

99115
_getPassword() {
100-
return this.getElementValue('#password');
116+
return this.getElementValue(PASSWORD_INPUT_SELECTOR);
101117
},
102118

103119
_getVPassword() {
104-
return this.getElementValue('#vpassword');
120+
return this.getElementValue(VPASSWORD_INPUT_SELECTOR);
105121
},
106122

107123
_doPasswordsMatch() {
108124
return this._getPassword() === this._getVPassword();
109125
},
126+
127+
_onConfirmPasswordKeyUp() {
128+
this.checkPasswordsMatchDebounce();
129+
},
130+
131+
_checkPasswordsMatch() {
132+
if (this._getVPassword() !== '' && this._getPassword() !== '') {
133+
this.showValidationErrorsEnd();
134+
}
135+
},
110136
});
111137

112138
Cocktail.mixin(

packages/fxa-content-server/app/tests/spec/views/sign_up_password.js

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ describe('views/sign_up_password', () => {
142142
beforeEach(() => {
143143
sinon.stub(view, 'signUp').callsFake(() => Promise.resolve());
144144
sinon.stub(view, 'tooYoung');
145-
sinon.spy(view, 'displayError');
145+
sinon.spy(view, 'showValidationError');
146146
});
147147

148148
describe('password and vpassword do not match', () => {
@@ -155,10 +155,13 @@ describe('views/sign_up_password', () => {
155155
assert.fail,
156156
() => {
157157
assert.isFalse(view.signUp.called);
158-
assert.isTrue(view.displayError.calledOnce);
159-
const displayedError = view.displayError.args[0][0];
158+
assert.isTrue(view.showValidationError.calledOnce);
159+
const displayedError = view.showValidationError.args[0][1];
160160
assert.isTrue(
161-
AuthErrors.is(displayedError, 'PASSWORDS_DO_NOT_MATCH')
161+
AuthErrors.is(
162+
displayedError,
163+
AuthErrors.toError('PASSWORDS_DO_NOT_MATCH')
164+
)
162165
);
163166
}
164167
);
@@ -174,7 +177,7 @@ describe('views/sign_up_password', () => {
174177
return Promise.resolve(view.validateAndSubmit()).then(() => {
175178
assert.isTrue(view.tooYoung.calledOnce);
176179
assert.isFalse(view.signUp.called);
177-
assert.isFalse(view.displayError.called);
180+
assert.isFalse(view.showValidationError.called);
178181
});
179182
});
180183
});
@@ -196,7 +199,7 @@ describe('views/sign_up_password', () => {
196199
'take-action-for-the-internet',
197200
]);
198201

199-
assert.isFalse(view.displayError.called);
202+
assert.isFalse(view.showValidationError.called);
200203
});
201204
});
202205
});
@@ -216,6 +219,66 @@ describe('views/sign_up_password', () => {
216219
});
217220
});
218221

222+
describe('_checkPasswordsMatch', () => {
223+
beforeEach(() => {
224+
sinon.stub(view, 'signUp').callsFake(() => Promise.resolve());
225+
sinon.stub(view, 'tooYoung');
226+
sinon.spy(view, 'showValidationError');
227+
});
228+
229+
describe('password and vpassword do not match', () => {
230+
it('displays an error', () => {
231+
view.$(Selectors.PASSWORD).val('password123123');
232+
view.$(Selectors.VPASSWORD).val('different_password');
233+
view.$(Selectors.AGE).val('21');
234+
235+
return Promise.resolve(view._checkPasswordsMatch()).then(() => {
236+
assert.isTrue(view.showValidationError.calledOnce);
237+
const displayedError = view.showValidationError.args[0][1];
238+
assert.isTrue(
239+
AuthErrors.is(
240+
displayedError,
241+
AuthErrors.toError('PASSWORDS_DO_NOT_MATCH')
242+
)
243+
);
244+
});
245+
});
246+
});
247+
248+
describe('password and vpassword do match', () => {
249+
it('displays an error', () => {
250+
view.$(Selectors.PASSWORD).val('password123123');
251+
view.$(Selectors.VPASSWORD).val('password123123');
252+
view.$(Selectors.AGE).val('21');
253+
254+
return Promise.resolve(view._checkPasswordsMatch()).then(() => {
255+
assert.isFalse(view.showValidationError.called);
256+
});
257+
});
258+
});
259+
});
260+
261+
describe('inline confirm password validation', () => {
262+
beforeEach(() => {
263+
sinon.stub(view, 'signUp').callsFake(() => Promise.resolve());
264+
sinon.spy(view, 'showValidationError');
265+
sinon.spy(view, 'checkPasswordsMatchDebounce');
266+
});
267+
268+
describe('check password and vpassword', () => {
269+
it('calls debounced password check', () => {
270+
view.$(Selectors.PASSWORD).val('password123123');
271+
view.$(Selectors.VPASSWORD).val('different_password');
272+
view.$(Selectors.AGE).val('21');
273+
274+
return Promise.resolve(view.$(Selectors.VPASSWORD).keyup()).then(() => {
275+
assert.isFalse(view.signUp.called);
276+
assert.isTrue(view.checkPasswordsMatchDebounce.calledOnce);
277+
});
278+
});
279+
});
280+
});
281+
219282
describe('useDifferentAccount', () => {
220283
it('navigates to `/` with the account', () => {
221284
sinon.spy(view, 'navigate');

packages/fxa-content-server/tests/functional/lib/selectors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ module.exports = {
422422
SUBMIT: 'button[type="submit"]',
423423
TOOLTIP_AGE_REQUIRED: '#age ~ .tooltip',
424424
TOS: '#fxa-tos',
425+
TOOLTIP: '.tooltip',
425426
VPASSWORD: '#vpassword',
426427
},
427428
SMS_LEARN_MORE: {

packages/fxa-content-server/tests/functional/sign_up.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const {
3131
openVerificationLinkInSameTab,
3232
switchToWindow,
3333
testElementExists,
34+
testElementTextEquals,
3435
testElementTextInclude,
3536
testElementValueEquals,
3637
testErrorTextInclude,
@@ -421,7 +422,12 @@ registerSuite('signup', {
421422
// wait five seconds to allow any errant navigation to occur
422423
.then(noPageTransition(selectors.SIGNUP_PASSWORD.HEADER))
423424
// the validation tooltip should be visible
424-
.then(visibleByQSA(selectors.SIGNUP_PASSWORD.ERROR))
425+
.then(
426+
testElementTextEquals(
427+
selectors.SIGNUP_PASSWORD.TOOLTIP,
428+
'Passwords do not match'
429+
)
430+
)
425431
);
426432
},
427433

packages/fxa-content-server/tests/functional/sync_v3_email_first.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ const {
3636
testElementTextEquals,
3737
testElementTextInclude,
3838
testElementValueEquals,
39-
testErrorTextInclude,
4039
testIsBrowserNotified,
4140
type,
4241
visibleByQSA,
@@ -173,7 +172,12 @@ registerSuite('Firefox Desktop Sync v3 email first', {
173172
selectors.SIGNUP_PASSWORD.ERROR_PASSWORDS_DO_NOT_MATCH
174173
)
175174
)
176-
.then(testErrorTextInclude('Passwords do not match'))
175+
.then(
176+
testElementTextEquals(
177+
selectors.SIGNUP_PASSWORD.TOOLTIP,
178+
'Passwords do not match'
179+
)
180+
)
177181

178182
// fix the password mismatch
179183
.then(type(selectors.SIGNUP_PASSWORD.VPASSWORD, PASSWORD))

0 commit comments

Comments
 (0)