зеркало из https://github.com/mozilla/fxa.git
feat(fxa-content-server): fix #1071 - new delete account view to display apps & subscriptions
change getSettingsData to settingsData Remove duplicate account key Fix missed shrinkwrap conflicts Fix mocha warning Finish functional tests for delete account remove extra getSettingsData
This commit is contained in:
Родитель
efbd1290c5
Коммит
25bc624747
|
@ -1,8 +1,11 @@
|
|||
<div id="delete-account" class="settings-unit">
|
||||
<div id="delete-account" class="settings-unit {{#isPanelOpen}}open{{/isPanelOpen}}">
|
||||
<div class="settings-unit-stub">
|
||||
<header class="settings-unit-summary">
|
||||
<h2 class="settings-unit-title">{{#t}}Delete account{{/t}}</h2>
|
||||
</header>
|
||||
<button class="settings-button secondary-button settings-unit-loading" disabled>
|
||||
<div class="spinner spinner-settings-fetch"></div>
|
||||
</button>
|
||||
<button class="settings-button secondary-button settings-unit-toggle"
|
||||
data-href="settings/delete_account">{{#t}}Delete…{{/t}}</button>
|
||||
</div>
|
||||
|
@ -11,7 +14,57 @@
|
|||
<div class="error"></div>
|
||||
|
||||
<form novalidate>
|
||||
<p>{{#t}}If you use this account for Mozilla apps and services like AMO, Pocket, and Screenshots you should first log in and remove any saved personal information. Once you delete your account here you may lose access to it everywhere.{{/t}}</p>
|
||||
<div class="delete-account-product-container {{#productListError}}hide{{/productListError}}">
|
||||
<p>{{#t}}You've connected your Firefox account to Mozilla products that keep you secure and productive on the web:{{/t}}</p>
|
||||
|
||||
<ul class="delete-account-product-list {{#hasTwoColumnProductList}}two-col{{/hasTwoColumnProductList}}">
|
||||
{{#subscriptions}}
|
||||
{{#plan_name}}
|
||||
<li class="delete-account-product-subscription" title="{{plan_name}}">
|
||||
{{plan_name}}
|
||||
</li>
|
||||
{{/plan_name}}
|
||||
{{/subscriptions}}
|
||||
|
||||
{{#clients}}
|
||||
{{#isWebSession}}
|
||||
{{#title}}
|
||||
<li class="delete-account-product-client" title="{{title}}">
|
||||
{{title}}
|
||||
</li>
|
||||
{{/title}}
|
||||
{{/isWebSession}}
|
||||
|
||||
{{#isOAuthApp}}
|
||||
{{#name}}
|
||||
<li class="delete-account-product-client" title="{{title}}">
|
||||
{{name}}
|
||||
</li>
|
||||
{{/name}}
|
||||
{{/isOAuthApp}}
|
||||
|
||||
{{/clients}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>{{#t}}Please acknowledge that by deleting your account:{{/t}}</p>
|
||||
|
||||
<ul class="delete-account-checkbox-list">
|
||||
<li class='delete-account-checkbox-list-item'>
|
||||
<input id="delete-account-subscriptions" type="checkbox" value="delete-account-subscriptions" class="delete-account-checkbox" required />
|
||||
<label for="delete-account-subscriptions">{{#t}}Any subscriptions you have will be cancelled{{/t}}</label>
|
||||
</li>
|
||||
|
||||
<li class='delete-account-checkbox-list-item'>
|
||||
<input id="delete-account-saved-info" type="checkbox" value="delete-account-saved-info" class="delete-account-checkbox" required />
|
||||
<label for="delete-account-saved-info">{{#t}}You may lose saved information and features within Mozilla products{{/t}}</label>
|
||||
</li>
|
||||
|
||||
<li class='delete-account-checkbox-list-item'>
|
||||
<input id="delete-account-reactivate" type="checkbox" value="delete-account-reactivate" class="delete-account-checkbox" required />
|
||||
<label for="delete-account-reactivate">{{#t}}Reactivating with this email may not restore your saved information{{/t}}</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="input-row password-row">
|
||||
<!-- add the autocomplete=off attribute - the user is deleting the account, they most likely don't want the password saved. -->
|
||||
|
@ -19,7 +72,7 @@
|
|||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button type="submit" class="settings-button warning-button">{{#t}}Delete account{{/t}}</button>
|
||||
<button type="submit" class="settings-button warning-button delete-account-button" disabled>{{#t}}Delete account{{/t}}</button>
|
||||
<button class="settings-button secondary-button cancel">{{#t}}Cancel{{/t}}</button>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -10,21 +10,127 @@ import ServiceMixin from '../mixins/settings-panel-mixin';
|
|||
import Session from '../../lib/session';
|
||||
import SettingsPanelMixin from '../mixins/service-mixin';
|
||||
import Template from 'templates/settings/delete_account.mustache';
|
||||
import AttachedClients from '../../models/attached-clients';
|
||||
import { CLIENT_TYPE_WEB_SESSION } from '../../lib/constants';
|
||||
|
||||
const t = msg => msg;
|
||||
|
||||
const LOADING_INDICATOR_BUTTON = '.settings-button.settings-unit-loading';
|
||||
const DELETE_ACCOUNT_BUTTON = '.delete-account-button';
|
||||
const UNIT_DETAILS = '.settings-unit-details';
|
||||
const CHECKBOXES = '.delete-account-checkbox';
|
||||
|
||||
var View = FormView.extend({
|
||||
template: Template,
|
||||
className: 'delete-account',
|
||||
viewName: 'settings.delete-account',
|
||||
|
||||
initialize(options) {
|
||||
this._attachedClients = options.attachedClients;
|
||||
if (!this._attachedClients) {
|
||||
this._attachedClients = new AttachedClients([], {
|
||||
notifier: options.notifier,
|
||||
});
|
||||
}
|
||||
this._activeSubscriptions = [];
|
||||
this._hasTwoColumnProductList = false;
|
||||
this._productListError = false;
|
||||
},
|
||||
|
||||
_formatTitle(items) {
|
||||
return items.map(item => {
|
||||
item.title = item.name;
|
||||
if (item.clientType === CLIENT_TYPE_WEB_SESSION && item.userAgent) {
|
||||
item.title = item.userAgent;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
},
|
||||
|
||||
setInitialContext(context) {
|
||||
context.set('email', this.getSignedInAccount().get('email'));
|
||||
const clients = this._attachedClients.toJSON();
|
||||
context.set({
|
||||
email: this.getSignedInAccount().get('email'),
|
||||
clients: this._formatTitle(clients),
|
||||
isPanelOpen: this.isPanelOpen(),
|
||||
subscriptions: this._activeSubscriptions,
|
||||
hasTwoColumnProductList: this._hasTwoColumnProductList,
|
||||
productListError: this._productListError,
|
||||
});
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .delete-account-checkbox': '_toggleEnableSubmit',
|
||||
},
|
||||
|
||||
openPanel() {
|
||||
this.logViewEvent('open');
|
||||
this.$el.find(UNIT_DETAILS).hide();
|
||||
this.$el.find(LOADING_INDICATOR_BUTTON).show();
|
||||
|
||||
return Promise.all([
|
||||
this._fetchAttachedClients(),
|
||||
this._fetchActiveSubscriptions(),
|
||||
])
|
||||
.then(() => {
|
||||
this._hasTwoColumnProductList = this._setHasTwoColumnProductList();
|
||||
})
|
||||
.catch(err => {
|
||||
this.model.set('error', err);
|
||||
this.logError(err);
|
||||
this._productListError = true;
|
||||
})
|
||||
.finally(() => this.render());
|
||||
},
|
||||
|
||||
_fetchActiveSubscriptions() {
|
||||
const account = this.getSignedInAccount();
|
||||
const start = Date.now();
|
||||
return account.settingsData().then(({ subscriptions }) => {
|
||||
this.logFlowEvent(`timing.settings.fetch.${Date.now() - start}`);
|
||||
this._activeSubscriptions = subscriptions.filter(
|
||||
subscription => subscription.status === 'active'
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
_fetchAttachedClients() {
|
||||
const start = Date.now();
|
||||
return this._attachedClients.fetchClients(this.user).then(() => {
|
||||
this.logFlowEvent(`timing.clients.fetch.${Date.now() - start}`);
|
||||
});
|
||||
},
|
||||
|
||||
_setHasTwoColumnProductList() {
|
||||
let numberOfProducts = 0;
|
||||
for (const client of this._attachedClients.toJSON()) {
|
||||
if (client.isOAuthApp === true || client.isWebSession) {
|
||||
numberOfProducts++;
|
||||
}
|
||||
}
|
||||
for (const sub of this._activeSubscriptions) {
|
||||
if (sub.plan_id && sub.status === 'active') {
|
||||
numberOfProducts++;
|
||||
}
|
||||
}
|
||||
return numberOfProducts >= 4;
|
||||
},
|
||||
|
||||
_toggleEnableSubmit() {
|
||||
if (this._allCheckboxesAreChecked()) {
|
||||
this.$(DELETE_ACCOUNT_BUTTON).attr('disabled', false);
|
||||
} else {
|
||||
this.$(DELETE_ACCOUNT_BUTTON).attr('disabled', true);
|
||||
}
|
||||
},
|
||||
|
||||
_allCheckboxesAreChecked() {
|
||||
return this.$(`${CHECKBOXES}:checked`).length === this.$(CHECKBOXES).length;
|
||||
},
|
||||
|
||||
submit() {
|
||||
var account = this.getSignedInAccount();
|
||||
var password = this.getElementValue('.password');
|
||||
const account = this.getSignedInAccount();
|
||||
const password = this.getElementValue('.password');
|
||||
|
||||
return this.user
|
||||
.deleteAccount(account, password)
|
||||
|
|
|
@ -52,7 +52,7 @@ button {
|
|||
background: $button-background-color-primary-hover;
|
||||
}
|
||||
|
||||
&:active {
|
||||
&:active:enabled {
|
||||
background: $button-background-color-primary-active;
|
||||
}
|
||||
}
|
||||
|
@ -62,12 +62,12 @@ button {
|
|||
border: 0;
|
||||
color: $message-text-color;
|
||||
|
||||
&:hover {
|
||||
&:hover:enabled {
|
||||
background: darken($error-background-color, 5);
|
||||
}
|
||||
|
||||
&:hover:active {
|
||||
background: darken($error-background-color, 10);
|
||||
&:active {
|
||||
background: darken($error-background-color, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -344,6 +344,7 @@ body.settings #stage .settings {
|
|||
clear: both;
|
||||
color: $faint-text-color;
|
||||
margin: 6px 0 24px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -384,6 +385,39 @@ section.modal-panel {
|
|||
}
|
||||
}
|
||||
|
||||
.delete-account {
|
||||
&-product-container {
|
||||
&.hide { // error during fetch for subscriptions and/or clients
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-product-list {
|
||||
padding-left: 14px;
|
||||
margin: 0 0 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&-checkbox-list {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: 0 0 32px;
|
||||
|
||||
&-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
color: $faint-text-color;
|
||||
}
|
||||
|
||||
.two-col {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.client-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import $ from 'jquery';
|
||||
import AuthErrors from 'lib/auth-errors';
|
||||
import Broker from 'models/auth_brokers/base';
|
||||
import AttachedClients from 'models/attached-clients';
|
||||
import chai from 'chai';
|
||||
import Metrics from 'lib/metrics';
|
||||
import Notifier from 'lib/channels/notifier';
|
||||
|
@ -13,175 +14,500 @@ import Relier from 'models/reliers/relier';
|
|||
import sinon from 'sinon';
|
||||
import TestHelpers from '../../../lib/helpers';
|
||||
import User from 'models/user';
|
||||
import Translator from 'lib/translator';
|
||||
import View from 'views/settings/delete_account';
|
||||
import BaseView from '../../../../scripts/views/base';
|
||||
|
||||
var assert = chai.assert;
|
||||
var wrapAssertion = TestHelpers.wrapAssertion;
|
||||
|
||||
describe('views/settings/delete_account', function() {
|
||||
var UID = '123';
|
||||
var account;
|
||||
var broker;
|
||||
var email;
|
||||
var metrics;
|
||||
var notifier;
|
||||
var password = 'password';
|
||||
var relier;
|
||||
var tabChannelMock;
|
||||
var user;
|
||||
var view;
|
||||
const UID = '123';
|
||||
const password = 'password';
|
||||
let account;
|
||||
let broker;
|
||||
let email;
|
||||
let metrics;
|
||||
let notifier;
|
||||
let relier;
|
||||
let translator;
|
||||
let tabChannelMock;
|
||||
let user;
|
||||
let view;
|
||||
let attachedClients;
|
||||
let parentView;
|
||||
let activeSubscriptions;
|
||||
|
||||
beforeEach(function() {
|
||||
function initView() {
|
||||
view = new View({
|
||||
attachedClients,
|
||||
broker,
|
||||
metrics,
|
||||
notifier,
|
||||
parentView,
|
||||
translator,
|
||||
user,
|
||||
relier,
|
||||
activeSubscriptions,
|
||||
});
|
||||
|
||||
sinon.spy(view, 'logFlowEvent');
|
||||
|
||||
return view.render();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
relier = new Relier();
|
||||
tabChannelMock = new NullChannel();
|
||||
user = new User();
|
||||
|
||||
broker = new Broker({
|
||||
relier: relier,
|
||||
});
|
||||
|
||||
notifier = new Notifier({
|
||||
tabChannel: tabChannelMock,
|
||||
});
|
||||
parentView = new BaseView();
|
||||
translator = new Translator({ forceEnglish: true });
|
||||
broker = new Broker({ relier });
|
||||
notifier = new Notifier({ tabChannel: tabChannelMock });
|
||||
metrics = new Metrics({ notifier });
|
||||
|
||||
view = new View({
|
||||
broker: broker,
|
||||
metrics: metrics,
|
||||
notifier: notifier,
|
||||
relier: relier,
|
||||
user: user,
|
||||
account = user.initAccount({
|
||||
email: email,
|
||||
sessionToken: 'abc123',
|
||||
uid: UID,
|
||||
verified: true,
|
||||
});
|
||||
|
||||
const clientList = [
|
||||
{
|
||||
deviceId: 'device-1',
|
||||
os: 'Windows',
|
||||
isCurrentSession: true,
|
||||
isWebSession: true,
|
||||
name: 'alpha',
|
||||
deviceType: 'tablet',
|
||||
},
|
||||
{
|
||||
deviceId: 'device-2',
|
||||
os: 'iOS',
|
||||
isCurrentSession: true,
|
||||
isWebSession: false,
|
||||
name: 'beta',
|
||||
deviceType: 'mobile',
|
||||
},
|
||||
{
|
||||
name: 'omega',
|
||||
clientType: 'webSession',
|
||||
userAgent: 'Firefox 40',
|
||||
},
|
||||
{
|
||||
clientId: 'app-1',
|
||||
lastAccessTime: Date.now(),
|
||||
name: 'Pocket',
|
||||
scope: ['profile', 'profile:write'],
|
||||
},
|
||||
];
|
||||
|
||||
attachedClients = new AttachedClients(clientList, {
|
||||
notifier,
|
||||
});
|
||||
|
||||
sinon
|
||||
.stub(attachedClients, 'fetchClients')
|
||||
.callsFake(() => Promise.resolve());
|
||||
|
||||
activeSubscriptions = [
|
||||
{
|
||||
plan_id: '321doneProMonthly',
|
||||
plan_name: '321done Pro Monthly',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
plan_id: '321doneProYearly',
|
||||
plan_name: '321done Pro Yearly',
|
||||
status: 'inactive',
|
||||
},
|
||||
];
|
||||
|
||||
sinon.stub(account, 'settingsData').callsFake(() =>
|
||||
Promise.resolve({
|
||||
subscriptions: activeSubscriptions,
|
||||
})
|
||||
);
|
||||
|
||||
return initView();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
metrics.destroy();
|
||||
metrics = null;
|
||||
|
||||
$(view.el).remove();
|
||||
|
||||
if ($.prototype.trigger.restore) {
|
||||
$.prototype.trigger.restore();
|
||||
}
|
||||
|
||||
view.destroy();
|
||||
view = null;
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
beforeEach(() => {
|
||||
view = new View({
|
||||
notifier,
|
||||
parentView,
|
||||
user,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates an `AttachedClients` instance if one not passed in', () => {
|
||||
assert.ok(view._attachedClients);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with session', function() {
|
||||
beforeEach(function() {
|
||||
beforeEach(() => {
|
||||
email = TestHelpers.createEmail();
|
||||
|
||||
account = user.initAccount({
|
||||
email: email,
|
||||
sessionToken: 'abc123',
|
||||
uid: UID,
|
||||
verified: true,
|
||||
});
|
||||
sinon.stub(account, 'isSignedIn').callsFake(function() {
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
sinon.stub(view, 'getSignedInAccount').callsFake(() => account);
|
||||
sinon.stub(notifier, 'trigger').callsFake(() => {});
|
||||
|
||||
sinon.stub(view, 'getSignedInAccount').callsFake(function() {
|
||||
return account;
|
||||
});
|
||||
sinon.stub(notifier, 'trigger').callsFake(function() {});
|
||||
|
||||
return view.render().then(function() {
|
||||
return view.render().then(() => {
|
||||
$('body').append(view.el);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValid', function() {
|
||||
it('returns true if password is filled out', function() {
|
||||
$('form input[type=password]').val(password);
|
||||
|
||||
assert.equal(view.isValid(), true);
|
||||
describe('render', () => {
|
||||
beforeEach(() => {
|
||||
sinon.spy(attachedClients, 'fetch');
|
||||
});
|
||||
|
||||
it('returns false if password is too short', function() {
|
||||
$('form input[type=password]').val('passwor');
|
||||
it('does not fetch the clients list immediately to avoid startup XHR requests', () => {
|
||||
assert.isFalse(attachedClients.fetch.called);
|
||||
});
|
||||
|
||||
assert.equal(view.isValid(), false);
|
||||
it('does not fetch the subscriptions list immediately to avoid startup XHR requests', () => {
|
||||
assert.isFalse(account.settingsData.called);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showValidationErrors', function() {
|
||||
it('shows an error if the password is invalid', function(done) {
|
||||
view.$('[type=email]').val('testuser@testuser.com');
|
||||
view.$('[type=password]').val('passwor');
|
||||
describe('_fetchAttachedClients', () => {
|
||||
beforeEach(() => {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
assert.equal(view.logFlowEvent.callCount, 0);
|
||||
return view._fetchAttachedClients();
|
||||
})
|
||||
.then(() => {
|
||||
assert.equal(view.logFlowEvent.callCount, 1);
|
||||
const args = view.logFlowEvent.args[0];
|
||||
assert.lengthOf(args, 1);
|
||||
const eventParts = args[0].split('.');
|
||||
assert.lengthOf(eventParts, 4);
|
||||
assert.equal(eventParts[0], 'timing');
|
||||
assert.equal(eventParts[1], 'clients');
|
||||
assert.equal(eventParts[2], 'fetch');
|
||||
assert.match(eventParts[3], /^[0-9]+$/);
|
||||
});
|
||||
});
|
||||
|
||||
view.on('validation_error', function(which, msg) {
|
||||
wrapAssertion(function() {
|
||||
assert.ok(msg);
|
||||
}, done);
|
||||
});
|
||||
|
||||
view.showValidationErrors();
|
||||
it('delegates to the user to fetch the device list', () => {
|
||||
assert.isTrue(attachedClients.fetchClients.calledWith(user));
|
||||
});
|
||||
});
|
||||
|
||||
describe('submit', function() {
|
||||
beforeEach(function() {
|
||||
$('form input[type=email]').val(email);
|
||||
$('form input[type=password]').val(password);
|
||||
describe('_fetchActiveSubscriptions', () => {
|
||||
beforeEach(() => {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
assert.equal(view.logFlowEvent.callCount, 0);
|
||||
return view._fetchActiveSubscriptions();
|
||||
})
|
||||
.then(() => {
|
||||
assert.equal(view.logFlowEvent.callCount, 1);
|
||||
const args = view.logFlowEvent.args[0];
|
||||
assert.lengthOf(args, 1);
|
||||
const eventParts = args[0].split('.');
|
||||
assert.lengthOf(eventParts, 4);
|
||||
assert.equal(eventParts[0], 'timing');
|
||||
assert.equal(eventParts[1], 'settings');
|
||||
assert.equal(eventParts[2], 'fetch');
|
||||
assert.match(eventParts[3], /^[0-9]+$/);
|
||||
});
|
||||
});
|
||||
it('delegates to the account to fetch the subscriptions list from settingsData', () => {
|
||||
assert.isTrue(account.settingsData.called);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_toggleEnableSubmit', () => {
|
||||
beforeEach(() => {
|
||||
sinon.spy(view, '_toggleEnableSubmit');
|
||||
});
|
||||
|
||||
describe('success', function() {
|
||||
beforeEach(function() {
|
||||
sinon.stub(user, 'deleteAccount').callsFake(function() {
|
||||
return Promise.resolve();
|
||||
});
|
||||
it('should be disabled if checkboxes are not checked', () => {
|
||||
assert.isTrue(view.$('.delete-account-button').is(':disabled'));
|
||||
});
|
||||
|
||||
sinon.spy(broker, 'afterDeleteAccount');
|
||||
sinon.spy(view, 'logViewEvent');
|
||||
sinon.spy(view, 'navigate');
|
||||
it('enables the submit button if all checkboxes are checked', () => {
|
||||
view.$('.delete-account-checkbox').each(function() {
|
||||
$(this).prop('checked', true);
|
||||
});
|
||||
view._toggleEnableSubmit();
|
||||
|
||||
return view.submit();
|
||||
assert.isFalse(view.$('.delete-account-button').is(':disabled'));
|
||||
});
|
||||
|
||||
it('disables the submit button if all checkboxes are checked and one is unchecked', () => {
|
||||
view.$('.delete-account-checkbox').each(function() {
|
||||
$(this).prop('checked', true);
|
||||
});
|
||||
view._toggleEnableSubmit();
|
||||
|
||||
view
|
||||
.$('.delete-account-checkbox')
|
||||
.first()
|
||||
.prop('checked', false);
|
||||
view._toggleEnableSubmit();
|
||||
|
||||
assert.isTrue(view.$('.delete-account-button').is(':disabled'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('openPanel - failed fetch of attachedClients or activeSubscriptions', () => {
|
||||
beforeEach(() => {
|
||||
sinon.spy($.prototype, 'trigger');
|
||||
sinon.stub(view, 'logError');
|
||||
sinon
|
||||
.stub(view, '_fetchAttachedClients')
|
||||
.callsFake(() =>
|
||||
Promise.reject(AuthErrors.toError('UNEXPECTED_ERROR'))
|
||||
);
|
||||
sinon
|
||||
.stub(view, '_fetchActiveSubscriptions')
|
||||
.callsFake(() => Promise.resolve());
|
||||
return view.openPanel();
|
||||
});
|
||||
|
||||
it('logs the error', () => {
|
||||
assert.isTrue(view.logError.calledOnce);
|
||||
const err = view.logError.args[0][0];
|
||||
assert.isTrue(AuthErrors.is(err, 'UNEXPECTED_ERROR'));
|
||||
});
|
||||
|
||||
it('adds `hide` class to `delete-account-product-container` if client or subscriptions fetch fails', () => {
|
||||
assert.isTrue(
|
||||
view.$('.delete-account-product-container').hasClass('hide')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openPanel', () => {
|
||||
beforeEach(() => {
|
||||
sinon.spy($.prototype, 'trigger');
|
||||
sinon.spy(view, '_fetchAttachedClients');
|
||||
sinon.spy(view, '_fetchActiveSubscriptions');
|
||||
return view.openPanel();
|
||||
});
|
||||
|
||||
it('fetches the device and subscriptions list', () => {
|
||||
assert.isTrue(
|
||||
TestHelpers.isEventLogged(metrics, 'settings.delete-account.open')
|
||||
);
|
||||
assert.isTrue(view._fetchAttachedClients.calledOnce);
|
||||
assert.isTrue(view._fetchActiveSubscriptions.calledOnce);
|
||||
});
|
||||
|
||||
it('renders only `status: "active"` subscriptions', () => {
|
||||
assert.isTrue(
|
||||
view.$('.delete-account-product-subscription').length === 1
|
||||
);
|
||||
});
|
||||
|
||||
it('renders subscription title attributes', () => {
|
||||
assert.equal(
|
||||
view.$('.delete-account-product-subscription').attr('title'),
|
||||
'321done Pro Monthly'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders attachedClients already in the collection', () => {
|
||||
assert.ok(view.$('.delete-account-product-client').length, 2);
|
||||
});
|
||||
|
||||
it('renders only `isWebSession: true` and `isOAuthApp: true` clients', () => {
|
||||
assert.isTrue(view.$('.delete-account-product-client').length === 3);
|
||||
});
|
||||
|
||||
it('renders client title attributes, tests _formatTitle', () => {
|
||||
assert.equal(
|
||||
view
|
||||
.$('.delete-account-product-client')
|
||||
.eq(0)
|
||||
.attr('title'),
|
||||
'alpha'
|
||||
);
|
||||
assert.equal(
|
||||
view
|
||||
.$('.delete-account-product-client')
|
||||
.eq(1)
|
||||
.attr('title'),
|
||||
'Pocket'
|
||||
);
|
||||
assert.equal(
|
||||
view
|
||||
.$('.delete-account-product-client')
|
||||
.eq(2)
|
||||
.attr('title'),
|
||||
'Firefox 40'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders only `active: true` subscriptions', () => {
|
||||
assert.isTrue(
|
||||
view.$('.delete-account-product-subscription').length === 1
|
||||
);
|
||||
});
|
||||
|
||||
it('renders subscription title attributes', () => {
|
||||
assert.equal(
|
||||
view.$('.delete-account-product-subscription').attr('title'),
|
||||
'321done Pro Monthly'
|
||||
);
|
||||
});
|
||||
|
||||
describe('_setHasTwoColumnProductList', () => {
|
||||
it('does not add `two-col` class to `delete-account-product-list` if count of rendered products is 3', () => {
|
||||
activeSubscriptions = [];
|
||||
|
||||
return view
|
||||
.render()
|
||||
.then(() => view.openPanel())
|
||||
.then(() => {
|
||||
assert.isTrue($('.delete-account-product-list li').length === 3);
|
||||
assert.isFalse(
|
||||
view.$('.delete-account-product-list').hasClass('two-col')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('delegates to the user model', function() {
|
||||
assert.isTrue(user.deleteAccount.calledOnce);
|
||||
assert.isTrue(user.deleteAccount.calledWith(account, password));
|
||||
});
|
||||
|
||||
it('notifies the broker', function() {
|
||||
assert.isTrue(broker.afterDeleteAccount.calledOnce);
|
||||
assert.isTrue(broker.afterDeleteAccount.calledWith(account));
|
||||
});
|
||||
|
||||
it('redirects to signup, clearing query params', function() {
|
||||
assert.equal(view.navigate.args[0][0], 'signup');
|
||||
|
||||
assert.ok(view.navigate.args[0][1].success);
|
||||
assert.isTrue(view.navigate.args[0][2].clearQueryParams);
|
||||
});
|
||||
|
||||
it('logs success', function() {
|
||||
assert.isTrue(view.logViewEvent.calledOnce);
|
||||
assert.isTrue(view.logViewEvent.calledWith('deleted'));
|
||||
it('adds `two-col` class to `delete-account-product-list` if count of rendered products 4', () => {
|
||||
assert.isTrue(view.$('.delete-account-product-list li').length === 4);
|
||||
assert.isTrue(
|
||||
view.$('.delete-account-product-list').hasClass('two-col')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', function() {
|
||||
beforeEach(function() {
|
||||
view.$('#password').val('bad_password');
|
||||
|
||||
sinon.stub(user, 'deleteAccount').callsFake(function() {
|
||||
return Promise.reject(AuthErrors.toError('INCORRECT_PASSWORD'));
|
||||
describe('isValid', function() {
|
||||
it('returns true if all checkboxes are checked and password is filled out', function() {
|
||||
$('form input[type=password]').val(password);
|
||||
$('.delete-account-checkbox').each(function() {
|
||||
$(this).prop('checked', true);
|
||||
});
|
||||
|
||||
sinon.stub(view, 'showValidationError').callsFake(function() {});
|
||||
return view.submit();
|
||||
assert.equal(view.isValid(), true);
|
||||
});
|
||||
|
||||
it('display an error message', function() {
|
||||
assert.isTrue(view.showValidationError.called);
|
||||
it('returns false if password is too short', function() {
|
||||
$('form input[type=password]').val('passwor');
|
||||
|
||||
assert.equal(view.isValid(), false);
|
||||
});
|
||||
|
||||
it('returns false if all 3 checkboxes are not checked', function() {
|
||||
$('.delete-account-checkbox').each(function(i) {
|
||||
if (i !== 0) {
|
||||
// leave the first one unchecked
|
||||
$(this).prop('checked', true);
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(view.isValid(), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('other errors', function() {
|
||||
describe('showValidationErrors', function() {
|
||||
it('shows an error if the password is invalid', function(done) {
|
||||
view.$('[type=email]').val('testuser@testuser.com');
|
||||
view.$('[type=password]').val('passwor');
|
||||
|
||||
view.on('validation_error', function(which, msg) {
|
||||
wrapAssertion(function() {
|
||||
assert.ok(msg);
|
||||
}, done);
|
||||
});
|
||||
|
||||
view.showValidationErrors();
|
||||
});
|
||||
});
|
||||
|
||||
describe('submit', function() {
|
||||
beforeEach(function() {
|
||||
sinon.stub(user, 'deleteAccount').callsFake(function() {
|
||||
return Promise.reject(AuthErrors.toError('UNEXPECTED_ERROR'));
|
||||
$('form input[type=email]').val(email);
|
||||
$('form input[type=password]').val(password);
|
||||
});
|
||||
|
||||
describe('success', function() {
|
||||
beforeEach(function() {
|
||||
sinon.stub(user, 'deleteAccount').callsFake(function() {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
sinon.spy(broker, 'afterDeleteAccount');
|
||||
sinon.spy(view, 'logViewEvent');
|
||||
sinon.spy(view, 'navigate');
|
||||
|
||||
return view.submit();
|
||||
});
|
||||
|
||||
it('delegates to the user model', function() {
|
||||
assert.isTrue(user.deleteAccount.calledOnce);
|
||||
assert.isTrue(user.deleteAccount.calledWith(account, password));
|
||||
});
|
||||
|
||||
it('notifies the broker', function() {
|
||||
assert.isTrue(broker.afterDeleteAccount.calledOnce);
|
||||
assert.isTrue(broker.afterDeleteAccount.calledWith(account));
|
||||
});
|
||||
|
||||
it('redirects to signup, clearing query params', function() {
|
||||
assert.equal(view.navigate.args[0][0], 'signup');
|
||||
|
||||
assert.ok(view.navigate.args[0][1].success);
|
||||
assert.isTrue(view.navigate.args[0][2].clearQueryParams);
|
||||
});
|
||||
|
||||
it('logs success', function() {
|
||||
assert.isTrue(view.logViewEvent.calledOnce);
|
||||
assert.isTrue(view.logViewEvent.calledWith('deleted'));
|
||||
});
|
||||
});
|
||||
|
||||
it('are re-thrown', function() {
|
||||
return view.submit().then(assert.fail, function(err) {
|
||||
assert.isTrue(AuthErrors.is(err, 'UNEXPECTED_ERROR'));
|
||||
describe('error', function() {
|
||||
beforeEach(function() {
|
||||
view.$('#password').val('bad_password');
|
||||
|
||||
sinon.stub(user, 'deleteAccount').callsFake(function() {
|
||||
return Promise.reject(AuthErrors.toError('INCORRECT_PASSWORD'));
|
||||
});
|
||||
|
||||
sinon.stub(view, 'showValidationError').callsFake(function() {});
|
||||
return view.submit();
|
||||
});
|
||||
|
||||
it('display an error message', function() {
|
||||
assert.isTrue(view.showValidationError.called);
|
||||
});
|
||||
});
|
||||
|
||||
describe('other errors', function() {
|
||||
beforeEach(function() {
|
||||
sinon.stub(user, 'deleteAccount').callsFake(function() {
|
||||
return Promise.reject(AuthErrors.toError('UNEXPECTED_ERROR'));
|
||||
});
|
||||
});
|
||||
|
||||
it('are re-thrown', function() {
|
||||
return view.submit().then(assert.fail, function(err) {
|
||||
assert.isTrue(AuthErrors.is(err, 'UNEXPECTED_ERROR'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1669,10 +1669,13 @@ const fillOutDeleteAccount = thenify(function(password) {
|
|||
return (
|
||||
this.parent
|
||||
.setFindTimeout(intern._config.pageLoadTimeout)
|
||||
|
||||
.then(type('#delete-account form input.password', password))
|
||||
// check all required checkboxes
|
||||
.findAllByCssSelector(selectors.SETTINGS_DELETE_ACCOUNT.CHECKBOXES)
|
||||
.then(checkboxes => checkboxes.map(checkbox => checkbox.click()))
|
||||
.end()
|
||||
.then(type(selectors.SETTINGS_DELETE_ACCOUNT.INPUT_PASSWORD, password))
|
||||
// delete account
|
||||
.then(click('#delete-account button[type="submit"]'))
|
||||
.then(click(selectors.SETTINGS_DELETE_ACCOUNT.SUBMIT))
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -221,6 +221,9 @@ module.exports = {
|
|||
SETTINGS_DELETE_ACCOUNT: {
|
||||
DETAILS: '#delete-account .settings-unit-details',
|
||||
MENU_BUTTON: '#delete-account .settings-unit-toggle',
|
||||
CHECKBOXES: '#delete-account .delete-account-checkbox',
|
||||
INPUT_PASSWORD: '#delete-account form input.password',
|
||||
SUBMIT: '#delete-account button[type="submit"]',
|
||||
},
|
||||
SETTINGS_DISPLAY_NAME: {
|
||||
INPUT_DISPLAY_NAME: '#display-name input[type=text]',
|
||||
|
|
Загрузка…
Ссылка в новой задаче