feat(emails): Add support for change email (#5242) r=shane-tomlinson,vladikoff

This commit is contained in:
Vijay Budhram 2017-07-25 09:57:10 -04:00 коммит произвёл Vlad Filippov
Родитель 413d65c9a8
Коммит 39bb7715c0
16 изменённых файлов: 568 добавлений и 73 удалений

Просмотреть файл

@ -268,6 +268,10 @@ define(function (require, exports, module) {
signInOptions.skipCaseError = options.skipCaseError;
}
if (options.originalLoginEmail) {
signInOptions.originalLoginEmail = options.originalLoginEmail;
}
setMetricsContext(signInOptions, options);
return client.signIn(email, password, signInOptions)
@ -472,6 +476,9 @@ define(function (require, exports, module) {
* @param {String} code
* @param {Object} relier
* @param {Object} [options={}]
* @param {String} [options.emailToHashWith]
* If specified, the password is hashed with this email address, otherwise
* the user's password is hashed with their current email.
* @return {Promise} resolves when complete
*/
completePasswordReset: withClient((client, originalEmail, newPassword, token, code, relier, options = {}) => {
@ -488,7 +495,18 @@ define(function (require, exports, module) {
return client.passwordForgotVerifyCode(code, token, passwordVerifyCodeOptions)
.then(result => {
return client.accountReset(email,
let emailToHashWith = email;
// The `emailToHashWith` option is returned by the auth-server to let the content-server
// know what to hash the new password with. This is important in the scenario where a user
// has changed their primary email address. In this case, they must still hash with the
// account's original email because this will maintain backwards compatibility with
// how account password hashing works previously.
if (options.emailToHashWith) {
emailToHashWith = trim(options.emailToHashWith);
}
return client.accountReset(emailToHashWith,
newPassword,
result.accountResetToken,
accountResetOptions
@ -844,7 +862,16 @@ define(function (require, exports, module) {
* @param {String} sessionToken User session token
* @return {Promise} resolves when complete
*/
recoveryEmailSecondaryEmailEnabled: createClientDelegate('recoveryEmailSecondaryEmailEnabled')
recoveryEmailSecondaryEmailEnabled: createClientDelegate('recoveryEmailSecondaryEmailEnabled'),
/**
* Set the new primary email address for a user.
*
* @param {String} sessionToken User session token
* @param {String} email The new primary email address
* @return {Promise} resolves when complete
*/
recoveryEmailSetPrimaryEmail: createClientDelegate('recoveryEmailSetPrimaryEmail')
};
module.exports = FxaClientWrapper;

Просмотреть файл

@ -462,15 +462,15 @@ define(function (require, exports, module) {
* @param {String} [options.resume] - Resume token to send
* in verification email if user is unverified.
* @param {String} [options.unblockCode] - Unblock code.
* @param {String} [options.originalLoginEmail] - Login used to login with originally.
* @returns {Promise} - resolves when complete
*/
signIn (password, relier, options = {}) {
var email = this.get('email');
return p().then(() => {
var email = this.get('email');
var sessionToken = this.get('sessionToken');
if (password) {
return this._fxaClient.signIn(email, password, relier, {
const signinOptions = {
metricsContext: this._metrics.getFlowEventMetadata(),
reason: options.reason || SignInReasons.SIGN_IN,
resume: options.resume,
@ -478,7 +478,16 @@ define(function (require, exports, module) {
// can be updated with the correct case.
skipCaseError: true,
unblockCode: options.unblockCode
});
};
// `originalLoginEmail` is specified when the account's primary email has changed.
// This param lets the auth-server known that it should check that this email
// is the current primary for the account.
if (options.originalLoginEmail) {
signinOptions.originalLoginEmail = options.originalLoginEmail;
}
return this._fxaClient.signIn(email, password, relier, signinOptions);
} else if (sessionToken) {
// We have a cached Sync session so just check that it hasn't expired.
// The result includes the latest verified state
@ -488,11 +497,27 @@ define(function (require, exports, module) {
}
})
.then((updatedSessionData) => {
// If a different email case or primary email was used to login,
// the session won't have correct email. Update the session to use the one
// originally used for login.
if (options.originalLoginEmail && email.toLowerCase() !== options.originalLoginEmail.toLowerCase()) {
updatedSessionData.email = options.originalLoginEmail;
}
this.set(updatedSessionData);
return updatedSessionData;
})
.fail((err) => {
// The `INCORRECT_EMAIL_CASE` can be returned if a user is attempting to login with a different
// email case than what the account was created with or if they changed their primary email address.
// In both scenarios, the content-server needs to know the original account email to hash
// the user's password with.
if (AuthErrors.is(err, 'INCORRECT_EMAIL_CASE')) {
// Save the original email that was used for login so that the auth-server
// can verify that this is the accounts primary email address.
options.originalLoginEmail = email;
// The server will respond with the canonical email
// for this account. Use it hereafter.
this.set('email', err.email);
@ -1202,7 +1227,7 @@ define(function (require, exports, module) {
},
/**
* Associates a new email to a users account.
* Deletes an email from a users account.
*
* @param {String} email
*
@ -1213,6 +1238,20 @@ define(function (require, exports, module) {
this.get('sessionToken'),
email
);
},
/**
* Sets the primary email address of the user.
*
* @param {String} email
*
* @returns {Promise}
*/
setPrimaryEmail (email) {
return this._fxaClient.recoveryEmailSetPrimaryEmail(
this.get('sessionToken'),
email
);
}
}, {
ALLOWED_KEYS: ALLOWED_KEYS,

Просмотреть файл

@ -22,6 +22,7 @@ define(function (require, exports, module) {
validation: {
code: Vat.verificationCode().required(),
email: Vat.email().required(),
emailToHashWith: Vat.email().optional(),
token: Vat.token().required()
}
});

Просмотреть файл

@ -28,13 +28,38 @@
<div class="not-verified">{{#t}}verification required{{/t}}</div>
{{/verified}}
</div>
<button class="settings-button warning email-disconnect" data-id="{{email}}">
{{#t}}Remove{{/t}}
</button>
<div class="settings-button-group">
{{#verified}}
{{#canChangePrimaryEmail }}
<button class="settings-button secondary set-primary" data-id="{{email}}">
{{#t}}Make primary{{/t}}
</button>
{{/canChangePrimaryEmail }}
{{/verified}}
<button class="settings-button warning email-disconnect" data-id="{{email}}">
{{#t}}Remove{{/t}}
</button>
</div>
</li>
<li class="email-options">
<div class="settings-button-group">
{{#verified}}
{{#canChangePrimaryEmail }}
<button class="settings-button secondary set-primary" data-id="{{email}}">
{{#t}}Make primary{{/t}}
</button>
{{/canChangePrimaryEmail }}
{{/verified}}
<button class="settings-button warning email-disconnect unpaired" data-id="{{email}}">
{{#t}}Remove{{/t}}
</button>
</div>
</li>
{{^verified}}
<a class="resend" data-id="{{email}}">{{#t}}Not in inbox or spam folder? Resend?{{/t}}</a>
{{/verified}}
{{/isPrimary}}
{{/emails}}
</ul>

Просмотреть файл

@ -183,7 +183,14 @@ define(function (require, exports, module) {
var account = this.getSignedInAccount();
account.set('displayName', displayName);
return this.user.setAccount(account)
.then(_.bind(this._notifyProfileUpdate, this, account.get('uid')));
.then(() => this._notifyProfileUpdate(account.get('uid')));
},
updateDisplayEmail (email) {
var account = this.getSignedInAccount();
account.set('email', email);
return this.user.setAccount(account)
.then(() => this._notifyProfileUpdate(account.get('uid')));
},
_notifyProfileUpdate (uid) {

Просмотреть файл

@ -6,6 +6,7 @@ define(function (require, exports, module) {
'use strict';
const $ = require('jquery');
const AvatarMixin = require('views/mixins/avatar-mixin');
const BaseView = require('views/base');
const Cocktail = require('cocktail');
const Email = require('models/email');
@ -13,6 +14,8 @@ define(function (require, exports, module) {
const FormView = require('views/form');
const preventDefaultThen = require('views/base').preventDefaultThen;
const SettingsPanelMixin = require('views/mixins/settings-panel-mixin');
const SearchParamMixin = require('lib/search-param-mixin');
const Strings = require('lib/strings');
const showProgressIndicator = require('views/decorators/progress_indicator');
const Template = require('stache!templates/settings/emails');
@ -30,7 +33,8 @@ define(function (require, exports, module) {
events: {
'click .email-disconnect': preventDefaultThen('_onDisconnectEmail'),
'click .email-refresh.enabled': preventDefaultThen('refresh'),
'click .resend': preventDefaultThen('resend')
'click .resend': preventDefaultThen('resend'),
'click .set-primary': preventDefaultThen('setPrimary')
},
initialize (options) {
@ -44,6 +48,7 @@ define(function (require, exports, module) {
setInitialContext (context) {
context.set({
buttonClass: this._hasSecondaryEmail() ? 'secondary' : 'primary',
canChangePrimaryEmail: this._canChangePrimaryEmail(),
emails: this._emails,
hasSecondaryEmail: this._hasSecondaryEmail(),
hasSecondaryVerifiedEmail: this._hasSecondaryVerifiedEmail(),
@ -64,6 +69,14 @@ define(function (require, exports, module) {
}
},
_canChangePrimaryEmail () {
if (this.getSearchParam('canChangeEmail')) {
return true;
}
return false;
},
_isSecondaryEmailEnabled () {
// Only show secondary email panel if the user is in a verified session and feature is enabled.
const account = this.getSignedInAccount();
@ -120,7 +133,7 @@ define(function (require, exports, module) {
resend (event) {
const email = $(event.currentTarget).data('id');
const account = this.getSignedInAccount();
return account.resendEmailCode(email)
return account.resendEmailCode({ email })
.then(() => {
this.displaySuccess(t('Verification email sent'), {
closePanel: false
@ -143,12 +156,28 @@ define(function (require, exports, module) {
.fail((err) => this.showValidationError(this.$(EMAIL_INPUT_SELECTOR), err));
}
},
setPrimary (event) {
const email = $(event.currentTarget).data('id');
const account = this.getSignedInAccount();
return account.setPrimaryEmail(email)
.then(() => {
this.updateDisplayEmail(email);
this.displaySuccess(Strings.interpolate(t('Primary email set to %(email)s'), { email }), {
closePanel: false
});
this.render();
});
}
});
Cocktail.mixin(
View,
AvatarMixin,
SettingsPanelMixin,
FloatingPlaceholderMixin
FloatingPlaceholderMixin,
SearchParamMixin
);
module.exports = View;

Просмотреть файл

@ -655,67 +655,121 @@ section.modal-panel {
}
.email-list {
padding: 0;
}
padding: 0;
.email-address {
.email-address {
height: 40px;
list-style: none;
margin: 10px 0;
position: relative;
html[dir='ltr'] & {
background-position: left 2px;
background-position: left 2px;
}
html[dir='rtl'] & {
background-position: right 2px;
background-position: right 2px;
}
.address {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: calc(95% - 95px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: calc(95% - 95px);
}
.details {
color: $color-grey;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: calc(95% - 95px);
color: $color-grey;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: calc(95% - 95px);
& .not-verified {
color: $color-red;
}
& .not-verified {
color: $color-red;
}
& .verified {
color: $color-green;
}
& .verified {
color: $color-green;
}
}
.settings-button {
@media screen and (max-width: 400px) {
.settings-button-group {
/*!important is used here because on small screens these options are displayed*/
/*in another area of the panel. They are to the right of the email.*/
display: none !important;
}
}
.settings-button-group {
position: absolute;
top: 0;
html[dir='ltr'] & {
right: 0;
}
html[dir='rtl'] & {
left: 0;
}
.settings-button {
height: 35px;
margin-left: 10px;
/*minimum width required for the button to look good without occupying too much space*/
/*is also the default computed width on desktop screen*/
min-width: 100px;
position: absolute;
text-align: center;
top: 0;
width: 20%;
}
}
}
html[dir='ltr'] & {
right: 0;
}
@media screen and (min-width: 400px) {
.email-options {
/*!important is used here because on large screens these options are not displayed.*/
/*They sit below the email address.*/
display: none !important;
}
}
html[dir='rtl'] & {
left: 0;
}
.email-options {
height: 40px;
list-style: none;
margin: 10px 0;
position: relative;
html[dir='ltr'] & {
background-position: left 2px;
}
a.settings-button {
padding-top: 8px;
html[dir='rtl'] & {
background-position: right 2px;
}
.settings-button-group {
position: absolute;
top: 0;
html[dir='ltr'] & {
left: 0;
}
html[dir='rtl'] & {
right: 0;
}
.settings-button {
height: 35px;
margin-left: 10px;
/*minimum width required for the button to look good without occupying too much space*/
/*is also the default computed width on desktop screen*/
min-width: 100px;
text-align: center;
width: 20%;
}
}
}
}

Просмотреть файл

@ -488,7 +488,7 @@ define(function (require, exports, module) {
});
it('re-tries with the normalized email, updates model with normalized email', function () {
const expectedOptions = {
const firstExpectedOptions = {
metricsContext: {
baz: 'qux',
foo: 'bar'
@ -499,11 +499,23 @@ define(function (require, exports, module) {
unblockCode: 'unblock code'
};
const secondExpectedOptions = {
metricsContext: {
baz: 'qux',
foo: 'bar'
},
originalLoginEmail: upperCaseEmail,
reason: SignInReasons.SIGN_IN,
resume: 'resume token',
skipCaseError: true,
unblockCode: 'unblock code'
};
assert.equal(fxaClient.signIn.callCount, 2);
assert.isTrue(
fxaClient.signIn.calledWith(upperCaseEmail, PASSWORD, relier, expectedOptions));
fxaClient.signIn.calledWith(upperCaseEmail, PASSWORD, relier, firstExpectedOptions));
assert.isTrue(
fxaClient.signIn.calledWith(EMAIL, PASSWORD, relier, expectedOptions));
fxaClient.signIn.calledWith(EMAIL, PASSWORD, relier, secondExpectedOptions));
assert.equal(account.get('email'), EMAIL);
});

Просмотреть файл

@ -78,6 +78,17 @@ define(function (require, exports, module) {
assert.isFalse(model.isValid());
});
it('returns false if emailToHasWith is invalid', function () {
var model = new Model({
code: validCode,
email: validEmail,
emailToHashWith: invalidEmail,
token: validToken
});
assert.isFalse(model.isValid());
});
it('returns true otherwise', function () {
var model = new Model({
code: validCode,

Просмотреть файл

@ -57,6 +57,7 @@ define(function (require, exports, module) {
translator = new Translator({forceEnglish: true});
user = new User();
windowMock = new WindowMock();
windowMock.location.search = '?canChangeEmail=true';
account = user.initAccount({
email: email,
@ -148,6 +149,11 @@ define(function (require, exports, module) {
sinon.stub(account, 'resendEmailCode', () => {
return p();
});
sinon.stub(account, 'setPrimaryEmail', (newEmail) => {
email = newEmail;
return p();
});
});
describe('with no secondary email', () => {
@ -197,21 +203,24 @@ define(function (require, exports, module) {
it('can render', () => {
assert.equal(view.$('.email-address').length, 1);
assert.equal(view.$('.email-address .address').length, 1);
assert.equal(view.$('.email-address .address')[0].innerHTML, 'another@one.com');
assert.lengthOf(view.$('.email-address .address'), 1);
assert.equal(view.$('.email-address .address').html(), 'another@one.com');
assert.equal(view.$('.email-address .details .not-verified').length, 1);
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').length, 1);
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').attr('data-id'), 'another@one.com');
assert.equal(view.$('.email-address .settings-button.secondary.set-primary').length, 0);
});
it('can disconnect email and navigate to /emails', (done) => {
$('.email-address .settings-button.warning.email-disconnect').click();
setTimeout(function () {
assert.isTrue(view.navigate.calledOnce);
const args = view.navigate.args[0];
assert.equal(args.length, 1);
assert.equal(args[0], '/settings/emails');
done();
TestHelpers.wrapAssertion(() => {
assert.isTrue(view.navigate.calledOnce);
const args = view.navigate.args[0];
assert.equal(args.length, 1);
assert.equal(args[0], '/settings/emails');
}, done);
}, 150);
});
@ -219,8 +228,9 @@ define(function (require, exports, module) {
$('.email-refresh').click();
sinon.spy(view, 'render');
setTimeout(function () {
assert.isTrue(view.render.calledOnce);
done();
TestHelpers.wrapAssertion(() => {
assert.isTrue(view.render.calledOnce);
}, done);
}, 450); // Delay is higher here because refresh has a min delay of 350
});
@ -228,11 +238,12 @@ define(function (require, exports, module) {
$('.resend').click();
sinon.spy(view, 'render');
setTimeout(function () {
assert.isTrue(view.navigate.calledOnce);
const args = view.navigate.args[0];
assert.equal(args.length, 1);
assert.equal(args[0], '/settings/emails');
done();
TestHelpers.wrapAssertion(() => {
assert.isTrue(view.navigate.calledOnce);
const args = view.navigate.args[0];
assert.equal(args.length, 1);
assert.equal(args[0], '/settings/emails');
}, done);
}, 150);
});
@ -263,8 +274,8 @@ define(function (require, exports, module) {
it('can render', () => {
assert.equal(view.$('.email-address').length, 1);
assert.equal(view.$('.email-address .address').length, 1);
assert.equal(view.$('.email-address .address')[0].innerHTML, 'another@one.com');
assert.lengthOf(view.$('.email-address .address'), 1);
assert.equal(view.$('.email-address .address').html(), 'another@one.com');
assert.equal(view.$('.email-address .details .verified').length, 1);
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').length, 1);
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').attr('data-id'), 'another@one.com');
@ -273,11 +284,12 @@ define(function (require, exports, module) {
it('can disconnect email and navigate to /emails', (done) => {
$('.email-address .settings-button.warning.email-disconnect').click();
setTimeout(() => {
assert.isTrue(view.navigate.calledOnce);
const args = view.navigate.args[0];
assert.equal(args.length, 1);
assert.equal(args[0], '/settings/emails');
done();
TestHelpers.wrapAssertion(() => {
assert.isTrue(view.navigate.calledOnce);
const args = view.navigate.args[0];
assert.equal(args.length, 1);
assert.equal(args[0], '/settings/emails');
}, done);
}, 150);
});
@ -285,6 +297,82 @@ define(function (require, exports, module) {
assert.equal(view.isPanelOpen(), false);
});
});
describe('does not show change email when `canChangeEmail` not set', () => {
const newEmail = 'secondary@email.com';
beforeEach(() => {
emails = [{
email: 'primary@email.com',
isPrimary: true,
verified: true
}, {
email: newEmail,
isPrimary: false,
verified: true
}];
windowMock.location.search = '';
return initView()
.then(function () {
// click events require the view to be in the DOM
$('#container').html(view.el);
});
});
it('can render', () => {
assert.equal(view.$('.email-address').length, 1);
assert.lengthOf(view.$('.email-address .address'), 1);
assert.equal(view.$('.email-address .address').html(), 'secondary@email.com');
assert.equal(view.$('.email-address .details .verified').length, 1);
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').length, 1);
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').attr('data-id'), 'secondary@email.com');
assert.equal(view.$('.email-address .settings-button.secondary.set-primary').length, 0);
});
});
describe('can change email', () => {
const newEmail = 'secondary@email.com';
beforeEach(() => {
emails = [{
email: 'primary@email.com',
isPrimary: true,
verified: true
}, {
email: newEmail,
isPrimary: false,
verified: true
}];
return initView()
.then(function () {
// click events require the view to be in the DOM
$('#container').html(view.el);
});
});
it('can render', () => {
assert.equal(view.$('.email-address').length, 1);
assert.lengthOf(view.$('.email-address .address'), 1);
assert.equal(view.$('.email-address .address').html(), 'secondary@email.com');
assert.equal(view.$('.email-address .details .verified').length, 1);
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').length, 1);
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').attr('data-id'), 'secondary@email.com');
assert.equal(view.$('.email-address .settings-button.secondary.set-primary').length, 1);
assert.equal(view.$('.email-address .settings-button.secondary.set-primary').attr('data-id'), 'secondary@email.com');
});
it('can change email', (done) => {
$('.email-address .settings-button.secondary.set-primary').click();
setTimeout(() => {
TestHelpers.wrapAssertion(() => {
assert.equal(account.get('email'), newEmail, 'account email updated');
}, done);
}, 150);
});
});
});
});
});

Просмотреть файл

@ -10,7 +10,7 @@
"cocktail": "0.5.9",
"easteregg": "https://github.com/mozilla/fxa-easter-egg.git#ab20cd517cf8ae9feee115e48745189d28e13bc3",
"fxa-checkbox": "mozilla/fxa-checkbox#7f856afffd394a144f718e28e6fb79092d6ccddd",
"fxa-js-client": "https://github.com/mozilla/fxa-js-client.git#0.1.60",
"fxa-js-client": "https://github.com/mozilla/fxa-js-client.git#0.1.61",
"html5shiv": "3.7.2",
"jquery": "3.1.0",
"jquery-modal": "https://github.com/shane-tomlinson/jquery-modal.git#0576775d1b4590314b114386019f4c7421c77503",

Просмотреть файл

@ -340,3 +340,25 @@ Used to skip the confirmation form to reset a password
#### When to use
Should not be used by reliers.
Should only be used for accounts that must be reset.
### `emailToHashWith`
Allows you to override the default email that a reset password is hashed with.
#### Options
* user's current primary email (default)
#### When to use
After a user has changed their primary email you need to hash with the original account email
if they perform a reset password.
## Secondary email parameters
### `canChangeEmail`
Shows the option to change a user's primary email address.
#### Options
* `true`
* `false` (default)
#### When to specify
* /settings/emails

Просмотреть файл

@ -50,6 +50,7 @@ define([
'./functional/settings',
'./functional/settings_clients',
'./functional/settings_common',
'./functional/settings_change_email.js',
'./functional/settings_secondary_emails.js',
'./functional/sync_settings',
'./functional/change_password',

Просмотреть файл

@ -14,6 +14,9 @@ define([], function () {
ERROR: '.error',
HEADER: '#fxa-400-header'
},
CHANGE_PASSWORD: {
MENU_BUTTON: '#change-password .settings-unit-toggle'
},
CHOOSE_WHAT_TO_SYNC: {
ENGINE_ADDRESSES: '#sync-engine-addresses',
ENGINE_CREDIT_CARDS: '#sync-engine-creditcards',
@ -22,6 +25,12 @@ define([], function () {
HEADER: '#fxa-choose-what-to-sync-header',
SUBMIT: 'button[type=submit]'
},
COMPLETE_RESET_PASSWORD: {
HEADER: '#fxa-complete-reset-password-header'
},
CONFIRM_RESET_PASSWORD: {
HEADER: '#fxa-confirm-reset-password-header'
},
CONFIRM_SIGNIN: {
HEADER: '#fxa-confirm-signin-header'
},
@ -31,6 +40,15 @@ define([], function () {
CONNECT_ANOTHER_DEVICE: {
HEADER: '#fxa-connect-another-device-header'
},
EMAIL: {
ADDRESS_LABEL: '#emails .address',
ADD_BUTTON: '.email-add:not(.disabled)',
INPUT: '.new-email',
MENU_BUTTON: '#emails .settings-unit-stub button',
NOT_VERIFIED_LABEL: '.not-verified',
SET_PRIMARY_EMAIL_BUTTON: '.email-address .set-primary',
VERIFIED_LABEL: '.verified'
},
FORCE_AUTH: {
EMAIL: 'input[type=email]',
HEADER: '#fxa-force-auth-header'
@ -49,7 +67,8 @@ define([], function () {
EMAIL_NOT_EDITABLE: '.prefillEmail',
HEADER: '#fxa-signin-header',
PASSWORD: 'input[type=password]',
SUBMIT: 'button[type=submit]'
SUBMIT: 'button[type=submit]',
TOOLTIP: '.tooltip',
},
SIGNIN_COMPLETE: {
HEADER: '#fxa-sign-in-complete-header'

Просмотреть файл

@ -0,0 +1,160 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
define([
'intern',
'intern!object',
'tests/lib/helpers',
'tests/functional/lib/helpers',
'tests/functional/lib/selectors',
], function (intern, registerSuite, TestHelpers, FunctionalHelpers, selectors) {
const config = intern.config;
const SIGNUP_URL = config.fxaContentRoot + 'signup?canChangeEmail=true';
const SIGNIN_URL = config.fxaContentRoot + 'signin?canChangeEmail=true';
const SIGNIN_URL_NO_CHANGE_EMAIL = config.fxaContentRoot + 'signin';
const PASSWORD = 'password';
const NEW_PASSWORD = 'password1';
let email;
let secondaryEmail;
const clearBrowserState = FunctionalHelpers.clearBrowserState;
const click = FunctionalHelpers.click;
const fillOutChangePassword = FunctionalHelpers.fillOutChangePassword;
const fillOutResetPassword = FunctionalHelpers.fillOutResetPassword;
const fillOutCompleteResetPassword = FunctionalHelpers.fillOutCompleteResetPassword;
const fillOutSignUp = FunctionalHelpers.fillOutSignUp;
const fillOutSignIn = FunctionalHelpers.fillOutSignIn;
const openPage = FunctionalHelpers.openPage;
const openVerificationLinkInNewTab = FunctionalHelpers.openVerificationLinkInNewTab;
const openVerificationLinkInSameTab = FunctionalHelpers.openVerificationLinkInSameTab;
const noSuchElement = FunctionalHelpers.noSuchElement;
const testIsBrowserNotified = FunctionalHelpers.testIsBrowserNotified;
const testElementExists = FunctionalHelpers.testElementExists;
const testElementTextEquals = FunctionalHelpers.testElementTextEquals;
const testErrorTextInclude = FunctionalHelpers.testErrorTextInclude;
const testSuccessWasShown = FunctionalHelpers.testSuccessWasShown;
const type = FunctionalHelpers.type;
const visibleByQSA = FunctionalHelpers.visibleByQSA;
registerSuite({
name: 'settings change email',
beforeEach: function () {
email = TestHelpers.createEmail();
secondaryEmail = TestHelpers.createEmail();
return this.remote.then(clearBrowserState())
.then(openPage(SIGNUP_URL, selectors.SIGNUP.HEADER))
.then(fillOutSignUp(email, PASSWORD))
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
.then(openVerificationLinkInSameTab(email, 0))
.then(testElementExists(selectors.SETTINGS.HEADER))
.then(click(selectors.EMAIL.MENU_BUTTON))
// add secondary email, verify
.then(type(selectors.EMAIL.INPUT, secondaryEmail))
.then(click(selectors.EMAIL.ADD_BUTTON))
.then(testElementExists(selectors.EMAIL.NOT_VERIFIED_LABEL))
.then(openVerificationLinkInSameTab(secondaryEmail, 0))
.then(click(selectors.SETTINGS.SIGNOUT))
.then(openPage(SIGNIN_URL, selectors.SIGNIN.HEADER))
.then(fillOutSignIn(email, PASSWORD))
// set new primary email
.then(click(selectors.EMAIL.MENU_BUTTON ))
.then(testElementTextEquals(selectors.EMAIL.ADDRESS_LABEL, secondaryEmail))
.then(testElementExists(selectors.EMAIL.VERIFIED_LABEL))
.then(click(selectors.EMAIL.SET_PRIMARY_EMAIL_BUTTON));
},
afterEach: function () {
return this.remote.then(clearBrowserState());
},
'does no show change email option if query `canChangeEmail` not set': function () {
return this.remote
// sign out
.then(click(selectors.SETTINGS.SIGNOUT))
// sign in and does not show change primary email button
.then(openPage(SIGNIN_URL_NO_CHANGE_EMAIL, selectors.SIGNIN.HEADER))
.then(testElementExists(selectors.SIGNIN.HEADER))
.then(fillOutSignIn(secondaryEmail, PASSWORD))
.then(click(selectors.EMAIL.MENU_BUTTON ))
.then(noSuchElement(selectors.EMAIL.SET_PRIMARY_EMAIL_BUTTON));
},
'can change primary email and login': function () {
return this.remote
// sign out
.then(click(selectors.SETTINGS.SIGNOUT))
// sign in with old primary email fails
.then(openPage(SIGNIN_URL, selectors.SIGNIN.HEADER))
.then(testElementExists(selectors.SIGNIN.HEADER))
.then(fillOutSignIn(email, PASSWORD))
.then(testErrorTextInclude('Primary account email required'))
// sign in with new primary email
.then(testElementExists(selectors.SIGNIN.HEADER))
.then(fillOutSignIn(secondaryEmail, PASSWORD))
// shows new primary email
.then(testElementExists(selectors.SETTINGS.HEADER))
.then(testElementTextEquals(selectors.SETTINGS.PROFILE_HEADER, secondaryEmail));
},
'can change primary email, change password and login': function () {
return this.remote
// change password
.then(click(selectors.CHANGE_PASSWORD.MENU_BUTTON))
.then(fillOutChangePassword(PASSWORD, NEW_PASSWORD))
.then(testIsBrowserNotified('fxaccounts:change_password'))
.then(testElementExists(selectors.SETTINGS.HEADER))
.then(testSuccessWasShown())
// sign out and fails login with old password
.then(click(selectors.SETTINGS.SIGNOUT))
.then(testElementExists(selectors.SIGNIN.HEADER))
.then(fillOutSignIn(secondaryEmail, PASSWORD))
.then(visibleByQSA(selectors.SIGNIN.TOOLTIP))
// sign in with new password
.then(fillOutSignIn(secondaryEmail, NEW_PASSWORD))
.then(testElementTextEquals(selectors.SETTINGS.PROFILE_HEADER, secondaryEmail));
},
'can change primary email, reset password and login': function () {
return this.remote
.then(click(selectors.SETTINGS.SIGNOUT))
// reset password
.then(fillOutResetPassword(secondaryEmail))
.then(testElementExists(selectors.CONFIRM_RESET_PASSWORD.HEADER))
.then(openVerificationLinkInNewTab(secondaryEmail, 1))
// complete the reset password in the new tab
.switchToWindow('newwindow')
.then(testElementExists(selectors.COMPLETE_RESET_PASSWORD.HEADER))
.then(fillOutCompleteResetPassword(NEW_PASSWORD, NEW_PASSWORD))
.then(testElementTextEquals(selectors.SETTINGS.PROFILE_HEADER, secondaryEmail))
// sign out and fails login with old password
.then(click(selectors.SETTINGS.SIGNOUT))
.then(testElementExists(selectors.SIGNIN.HEADER))
.then(fillOutSignIn(secondaryEmail, PASSWORD))
.then(visibleByQSA(selectors.SIGNIN.TOOLTIP))
// sign in with new password succeeds
.then(fillOutSignIn(secondaryEmail, NEW_PASSWORD))
.then(testElementTextEquals(selectors.SETTINGS.PROFILE_HEADER, secondaryEmail));
}
});
});

Просмотреть файл

@ -76,7 +76,7 @@ define([
.then(type('.new-email', TestHelpers.createEmail()))
.then(click('.email-add:not(.disabled)'))
.then(testElementExists('.not-verified'))
.then(click('.email-disconnect'))
.then(click('.email-address .settings-button.warning.email-disconnect'))
// add secondary email, verify
.then(type('.new-email', secondaryEmail))