feat(devices): basic device UI for the settings panel
This commit is contained in:
Родитель
3fa1ca8658
Коммит
20c305d52c
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 369 B |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 484 B |
|
@ -20,7 +20,7 @@ define(function (require, exports, module) {
|
|||
DELETE: 'fxaccounts:delete',
|
||||
PROFILE_CHANGE: 'profile:change',
|
||||
SIGNED_IN: 'internal:signed_in',
|
||||
SIGNED_OUT: 'internal:signed_out'
|
||||
SIGNED_OUT: 'fxaccounts:logout'
|
||||
};
|
||||
|
||||
var Notifer = Backbone.Model.extend({
|
||||
|
|
|
@ -470,6 +470,20 @@ define(function (require, exports, module) {
|
|||
.then(function (client) {
|
||||
return client.accountKeys(keyFetchToken, unwrapBKey);
|
||||
});
|
||||
},
|
||||
|
||||
deviceList: function (sessionToken) {
|
||||
return this._getClient()
|
||||
.then(function (client) {
|
||||
return client.deviceList(sessionToken);
|
||||
});
|
||||
},
|
||||
|
||||
deviceDestroy: function (sessionToken, deviceId) {
|
||||
return this._getClient()
|
||||
.then(function (client) {
|
||||
return client.deviceDestroy(sessionToken, deviceId);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ define(function (require, exports, module) {
|
|||
var ConfirmView = require('../views/confirm');
|
||||
var CookiesDisabledView = require('../views/cookies_disabled');
|
||||
var DeleteAccountView = require('../views/settings/delete_account');
|
||||
var DevicesView = require('../views/settings/devices');
|
||||
var DisplayNameView = require('../views/settings/display_name');
|
||||
var ForceAuthView = require('../views/force_auth');
|
||||
var GravatarPermissionsView = require('../views/settings/gravatar_permissions');
|
||||
|
@ -88,6 +89,7 @@ define(function (require, exports, module) {
|
|||
'settings/change_password(/)': createChildViewHandler(ChangePasswordView, SettingsView),
|
||||
'settings/communication_preferences(/)': createChildViewHandler(CommunicationPreferencesView, SettingsView),
|
||||
'settings/delete_account(/)': createChildViewHandler(DeleteAccountView, SettingsView),
|
||||
'settings/devices(/)': createChildViewHandler(DevicesView, SettingsView),
|
||||
'settings/display_name(/)': createChildViewHandler(DisplayNameView, SettingsView),
|
||||
'signin(/)': createViewHandler(SignInView),
|
||||
'signin_complete(/)': createViewHandler(ReadyView, { type: 'sign_in' }),
|
||||
|
|
|
@ -328,6 +328,10 @@ define(function (require, exports, module) {
|
|||
});
|
||||
},
|
||||
|
||||
signOut: function () {
|
||||
return this._fxaClient.signOut(this.get('sessionToken'));
|
||||
},
|
||||
|
||||
saveGrantedPermissions: function (clientId, clientPermissions) {
|
||||
var permissions = this.get('grantedPermissions') || {};
|
||||
permissions[clientId] = clientPermissions;
|
||||
|
@ -415,6 +419,38 @@ define(function (require, exports, module) {
|
|||
.then(function (updatedSessionData) {
|
||||
self.set(updatedSessionData);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch the account's device list and populate the `devices` collection.
|
||||
*
|
||||
* @param {object} devices - Devices collection
|
||||
* @returns {promise} - resolves when complete
|
||||
*/
|
||||
fetchDevices: function (devices) {
|
||||
var sessionToken = this.get('sessionToken');
|
||||
|
||||
return this._fxaClient.deviceList(sessionToken)
|
||||
.then(devices.set.bind(devices));
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the device from the account
|
||||
*
|
||||
* @param {object} device - Device model to remove
|
||||
* @returns {promise} - resolves when complete
|
||||
*
|
||||
* @param {object} devices - Devices collection
|
||||
* @returns {promise} - resolves when complete
|
||||
*/
|
||||
destroyDevice: function (device) {
|
||||
var deviceId = device.get('id');
|
||||
var sessionToken = this.get('sessionToken');
|
||||
|
||||
return this._fxaClient.deviceDestroy(sessionToken, deviceId)
|
||||
.then(function () {
|
||||
device.destroy();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Device information
|
||||
*/
|
||||
|
||||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Backbone = require('backbone');
|
||||
|
||||
var Device = Backbone.Model.extend({
|
||||
defaults: {
|
||||
id: null,
|
||||
isCurrentDevice: null,
|
||||
lastConnected: null,
|
||||
name: null,
|
||||
type: null
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
// Both a sessionToken and deviceId are needed to destroy a device.
|
||||
// An account `has a` device, therefore account destroys the device.
|
||||
this.trigger('destroy', this);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Device;
|
||||
});
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* A collection of devices
|
||||
*/
|
||||
|
||||
|
||||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Backbone = require('backbone');
|
||||
var Device = require('models/device');
|
||||
|
||||
var Devices = Backbone.Collection.extend({
|
||||
model: Device,
|
||||
|
||||
initialize: function (models, options) {
|
||||
options = options || {};
|
||||
},
|
||||
|
||||
comparator: function (a, b) {
|
||||
// 1. the current device is first.
|
||||
// 2. the rest sorted in alphabetical order.
|
||||
if (a.get('isCurrentDevice')) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
var aName = (a.get('name') || '').trim().toLowerCase();
|
||||
var bName = (b.get('name') || '').trim().toLowerCase();
|
||||
|
||||
if (aName < bName) {
|
||||
return -1;
|
||||
} else if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Devices;
|
||||
});
|
||||
|
||||
|
|
@ -115,6 +115,10 @@ define(function (require, exports, module) {
|
|||
return this._cachedSignedInAccount;
|
||||
},
|
||||
|
||||
isSignedInAccount: function (account) {
|
||||
return account.get('uid') === this.getSignedInAccount().get('uid');
|
||||
},
|
||||
|
||||
setSignedInAccountByUid: function (uid) {
|
||||
if (this._accounts()[uid]) {
|
||||
this._setSignedInAccountUid(uid);
|
||||
|
@ -153,8 +157,11 @@ define(function (require, exports, module) {
|
|||
|
||||
// Used to clear the current account, but keeps the account details
|
||||
clearSignedInAccount: function () {
|
||||
var uid = this.getSignedInAccount().get('uid');
|
||||
this.clearSignedInAccountUid();
|
||||
this._notifier.triggerRemote(this._notifier.EVENTS.SIGNED_OUT);
|
||||
this._notifier.triggerRemote(this._notifier.EVENTS.SIGNED_OUT, {
|
||||
uid: uid
|
||||
});
|
||||
},
|
||||
|
||||
removeAllAccounts: function () {
|
||||
|
@ -165,12 +172,13 @@ define(function (require, exports, module) {
|
|||
// Delete the account from storage
|
||||
removeAccount: function (accountData) {
|
||||
var account = this.initAccount(accountData);
|
||||
var uid = account.get('uid');
|
||||
var accounts = this._accounts();
|
||||
|
||||
if (uid === this.getSignedInAccount().get('uid')) {
|
||||
if (this.isSignedInAccount(account)) {
|
||||
this.clearSignedInAccount();
|
||||
}
|
||||
|
||||
var accounts = this._accounts();
|
||||
var uid = account.get('uid');
|
||||
delete accounts[uid];
|
||||
this._storage.set('accounts', accounts);
|
||||
},
|
||||
|
@ -291,6 +299,19 @@ define(function (require, exports, module) {
|
|||
});
|
||||
},
|
||||
|
||||
signOutAccount: function (account) {
|
||||
var self = this;
|
||||
|
||||
return account.signOut()
|
||||
.fin(function () {
|
||||
// Clear the session, even on failure. Everything is A-OK.
|
||||
// See issue #616
|
||||
if (self.isSignedInAccount(account)) {
|
||||
self.clearSignedInAccount();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
changeAccountPassword: function (account, oldPassword, newPassword, relier) {
|
||||
var self = this;
|
||||
return account.changePassword(oldPassword, newPassword, relier)
|
||||
|
@ -305,6 +326,36 @@ define(function (require, exports, module) {
|
|||
.then(function () {
|
||||
return self.setSignedInAccount(account);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch the devices for the given account, populated the passed in
|
||||
* Devices collection.
|
||||
*
|
||||
* @param {object} account - account for which device list is requested
|
||||
* @param {object} devices - Devices collection used to store list.
|
||||
* @returns {promise} resolves when the action completes
|
||||
*/
|
||||
fetchAccountDevices: function (account, devices) {
|
||||
return account.fetchDevices(devices);
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy a device on the given account. If the current device
|
||||
* is destroyed, sign out the user.
|
||||
*
|
||||
* @param {object} account - account with the device
|
||||
* @param {object} device - device to destroy
|
||||
* @returns {promise} resolves when the action completes
|
||||
*/
|
||||
destroyAccountDevice: function (account, device) {
|
||||
var self = this;
|
||||
return account.destroyDevice(device)
|
||||
.then(function () {
|
||||
if (self.isSignedInAccount(account) && device.get('isCurrentDevice')) {
|
||||
self.clearSignedInAccount();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
{{#isPanelEnabled}}
|
||||
<div id="devices" class="settings-unit {{#isPanelOpen}}open{{/isPanelOpen}}">
|
||||
<div class="settings-unit-stub">
|
||||
<header class="settings-unit-summary">
|
||||
<h2 class="settings-unit-title">{{#t}}Devices{{/t}}</h2>
|
||||
</header>
|
||||
<button class="settings-button secondary settings-unit-toggle hidden"
|
||||
data-href="settings/devices">
|
||||
{{#t}}Show{{/t}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-unit-details">
|
||||
<div class="error"></div>
|
||||
|
||||
<form novalidate>
|
||||
{{#t}}Firefox is available for <a href="%(linkWindows)s">Windows</a>, <a href="%(linkOSX)s">OS X</a>, <a href="%(linkAndroid)s">Android</a>, <a href="%(linkIOS)s">iOS</a> and <a href="%(linkLinux)s">Linux</a>.{{/t}}
|
||||
{{#t}}You can manage your devices below.{{/t}}
|
||||
<ul class="device-list button-row">
|
||||
{{#devices}}
|
||||
<li class="device {{type}}" id="{{id}}">
|
||||
<div class="device-name">
|
||||
{{name}} {{#isCurrentDevice}}<span class="device-current">{{#t}}(current){{/t}}</span>{{/isCurrentDevice}}
|
||||
</div>
|
||||
<div class="last-connected">
|
||||
{{lastConnected}}
|
||||
</div>
|
||||
<button class="settings-button warning device-disconnect" data-id="{{id}}">{{#t}}Disconnect{{/t}}</button>
|
||||
</li>
|
||||
{{/devices}}
|
||||
<a href="{{devicesSupportUrl}}">{{#t}}Don't see all your devices?{{/t}}</a>
|
||||
</ul>
|
||||
|
||||
<div class="button-row">
|
||||
<button type="submit" class="settings-button primary devices-refresh">{{#t}}Refresh{{/t}}</button>
|
||||
<button class="settings-button secondary cancel">{{#t}}Done{{/t}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{/isPanelEnabled}}
|
|
@ -198,6 +198,7 @@ define(function (require, exports, module) {
|
|||
.then(_.bind(self.afterRender, self))
|
||||
.then(function () {
|
||||
self.showEphemeralMessages();
|
||||
self.trigger('rendered');
|
||||
|
||||
return true;
|
||||
});
|
||||
|
|
|
@ -29,6 +29,10 @@ define(function (require, exports, module) {
|
|||
// to ensure their data isn't sticking around in memory.
|
||||
this._formPrefill.clear();
|
||||
Session.clear();
|
||||
this.navigateToSignIn();
|
||||
},
|
||||
|
||||
navigateToSignIn: function () {
|
||||
this.navigate('signin', {
|
||||
clearQueryParams: true,
|
||||
success: BaseView.t('Signed out successfully')
|
||||
|
|
|
@ -17,6 +17,7 @@ define(function (require, exports, module) {
|
|||
var Cocktail = require('cocktail');
|
||||
var CommunicationPreferencesView = require('views/settings/communication_preferences');
|
||||
var DeleteAccountView = require('views/settings/delete_account');
|
||||
var DevicesView = require('views/settings/devices');
|
||||
var DisplayNameView = require('views/settings/display_name');
|
||||
var GravatarPermissionsView = require('views/settings/gravatar_permissions');
|
||||
var GravatarView = require('views/settings/avatar_gravatar');
|
||||
|
@ -30,6 +31,7 @@ define(function (require, exports, module) {
|
|||
var PANEL_VIEWS = [
|
||||
AvatarView,
|
||||
DisplayNameView,
|
||||
DevicesView,
|
||||
CommunicationPreferencesView,
|
||||
ChangePasswordView,
|
||||
DeleteAccountView,
|
||||
|
@ -187,19 +189,16 @@ define(function (require, exports, module) {
|
|||
|
||||
signOut: allowOnlyOneSubmit(function () {
|
||||
var self = this;
|
||||
var sessionToken = self.getSignedInAccount().get('sessionToken');
|
||||
var accountToSignOut = self.getSignedInAccount();
|
||||
|
||||
self.logViewEvent('signout.submit');
|
||||
return self.fxaClient.signOut(sessionToken)
|
||||
return self.user.signOutAccount(accountToSignOut)
|
||||
.fail(function () {
|
||||
// ignore the error.
|
||||
// Clear the session, even on failure. Everything is A-OK.
|
||||
// See issue #616
|
||||
// log and ignore the error.
|
||||
self.logViewEvent('signout.error');
|
||||
})
|
||||
.fin(function () {
|
||||
self.logViewEvent('signout.success');
|
||||
self.user.clearSignedInAccount();
|
||||
self.clearSessionAndNavigateToSignIn();
|
||||
});
|
||||
}),
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
/* 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/.*/
|
||||
|
||||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var $ = require('jquery');
|
||||
var Cocktail = require('cocktail');
|
||||
var Devices = require('models/devices');
|
||||
var FormView = require('views/form');
|
||||
var Notifier = require('lib/channels/notifier');
|
||||
var preventDefaultThen = require('views/base').preventDefaultThen;
|
||||
var SettingsPanelMixin = require('views/mixins/settings-panel-mixin');
|
||||
var SignedOutNotificationMixin = require('views/mixins/signed-out-notification-mixin');
|
||||
var Template = require('stache!templates/settings/devices');
|
||||
var Url = require('lib/url');
|
||||
|
||||
var DEVICE_REMOVED_ANIMATION_MS = 150;
|
||||
var DEVICES_SUPPORT_URL = 'https://support.mozilla.org/kb/fxa-managing-devices';
|
||||
var FIREFOX_DOWNLOAD_LINK = 'https://www.mozilla.org/firefox/new/?utm_source=accounts.firefox.com&utm_medium=referral&utm_campaign=fxa-devices';
|
||||
var FORCE_DEVICE_LIST_VIEW = 'forceDeviceList';
|
||||
|
||||
var View = FormView.extend({
|
||||
template: Template,
|
||||
className: 'devices',
|
||||
viewName: 'settings.devices',
|
||||
|
||||
initialize: function (options) {
|
||||
this._able = options.able;
|
||||
this._devices = options.devices;
|
||||
|
||||
// An empty Devices instance is created to render the initial view.
|
||||
// Data is only fetched once the panel has been opened.
|
||||
if (! this._devices) {
|
||||
this._devices = new Devices([], {
|
||||
notifier: options.notifier
|
||||
});
|
||||
}
|
||||
|
||||
var devices = this._devices;
|
||||
devices.on('add', this._onDeviceAdded.bind(this));
|
||||
devices.on('remove', this._onDeviceRemoved.bind(this));
|
||||
|
||||
var notifier = options.notifier;
|
||||
notifier.on(Notifier.DEVICE_REFRESH, this._onRefreshDeviceList.bind(this));
|
||||
},
|
||||
|
||||
context: function () {
|
||||
return {
|
||||
devices: this._devices.toJSON(),
|
||||
devicesSupportUrl: DEVICES_SUPPORT_URL,
|
||||
isPanelEnabled: this._isPanelEnabled(),
|
||||
isPanelOpen: this.isPanelOpen(),
|
||||
linkAndroid: FIREFOX_DOWNLOAD_LINK,
|
||||
linkIOS: FIREFOX_DOWNLOAD_LINK,
|
||||
linkLinux: FIREFOX_DOWNLOAD_LINK,
|
||||
linkOSX: FIREFOX_DOWNLOAD_LINK,
|
||||
linkWindows: FIREFOX_DOWNLOAD_LINK
|
||||
};
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .device-disconnect': preventDefaultThen('_onDisconnectDevice'),
|
||||
'click .devices-refresh': preventDefaultThen('_onRefreshDeviceList')
|
||||
},
|
||||
|
||||
_isPanelEnabled: function () {
|
||||
return this._able.choose('deviceListVisible', {
|
||||
forceDeviceList: Url.searchParam(FORCE_DEVICE_LIST_VIEW, this.window.location.search)
|
||||
});
|
||||
},
|
||||
|
||||
_onDeviceAdded: function () {
|
||||
this.render();
|
||||
},
|
||||
|
||||
_onDeviceRemoved: function (device) {
|
||||
var id = device.get('id');
|
||||
var self = this;
|
||||
$('#' + id).slideUp(DEVICE_REMOVED_ANIMATION_MS, function () {
|
||||
// re-render in case the last device is removed and the
|
||||
// "no registered devices" message needs to be shown.
|
||||
self.render();
|
||||
});
|
||||
},
|
||||
|
||||
_onDisconnectDevice: function (event) {
|
||||
this.logViewEvent('disconnect');
|
||||
var deviceId = $(event.currentTarget).attr('data-id');
|
||||
this._destroyDevice(deviceId);
|
||||
},
|
||||
|
||||
_onRefreshDeviceList: function () {
|
||||
if (this.isPanelOpen()) {
|
||||
this.logViewEvent('refresh');
|
||||
// only refresh devices if panel is visible
|
||||
// if panel is hidden there is no point of fetching devices
|
||||
this._fetchDevices();
|
||||
}
|
||||
},
|
||||
|
||||
openPanel: function () {
|
||||
this.logViewEvent('open');
|
||||
this._fetchDevices();
|
||||
},
|
||||
|
||||
_fetchDevices: function () {
|
||||
var account = this.getSignedInAccount();
|
||||
|
||||
return this.user.fetchAccountDevices(account, this._devices);
|
||||
},
|
||||
|
||||
_destroyDevice: function (deviceId) {
|
||||
var self = this;
|
||||
var account = this.getSignedInAccount();
|
||||
var device = this._devices.get(deviceId);
|
||||
if (device) {
|
||||
this.user.destroyAccountDevice(account, device)
|
||||
.then(function () {
|
||||
if (device.get('isCurrentDevice')) {
|
||||
self.navigateToSignIn();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Cocktail.mixin(
|
||||
View,
|
||||
SettingsPanelMixin,
|
||||
SignedOutNotificationMixin
|
||||
);
|
||||
|
||||
module.exports = View;
|
||||
});
|
||||
|
|
@ -385,3 +385,43 @@ body.settings #main-content.card {
|
|||
width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.device-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.device {
|
||||
background-position: 0 4px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 30px 30px;
|
||||
|
||||
height: 40px;
|
||||
list-style: none;
|
||||
margin: 10px 0;
|
||||
padding-left: 40px;
|
||||
position: relative;
|
||||
|
||||
.settings-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 20%;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
}
|
||||
|
||||
.last-connected {
|
||||
font-size: 12px;
|
||||
color: #909ca8;
|
||||
}
|
||||
}
|
||||
|
||||
.device.desktop {
|
||||
background-image: url(/images/device-icon-pc.png);
|
||||
}
|
||||
|
||||
.device.mobile {
|
||||
background-image: url(/images/device-icon-mobile.png);
|
||||
}
|
||||
|
|
|
@ -854,6 +854,33 @@ define(function (require, exports, module) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deviceList', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(realClient, 'deviceList', function () {
|
||||
return p();
|
||||
});
|
||||
return client.deviceList('session token');
|
||||
});
|
||||
|
||||
it('calls `deviceList` of the realClient', function () {
|
||||
assert.isTrue(realClient.deviceList.calledWith('session token'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('deviceDestroy', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(realClient, 'deviceDestroy', function () {
|
||||
return p();
|
||||
});
|
||||
return client.deviceDestroy('session token', 'device id');
|
||||
});
|
||||
|
||||
it('calls `deviceDestroy` of the realClient', function () {
|
||||
assert.isTrue(
|
||||
realClient.deviceDestroy.calledWith('session token', 'device id'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ define(function (require, exports, module) {
|
|||
var AuthErrors = require('lib/auth-errors');
|
||||
var chai = require('chai');
|
||||
var Constants = require('lib/constants');
|
||||
var Device = require('models/device');
|
||||
var Devices = require('models/devices');
|
||||
var FxaClientWrapper = require('lib/fxa-client');
|
||||
var MarketingEmailClient = require('lib/marketing-email-client');
|
||||
var OAuthClient = require('lib/oauth-client');
|
||||
|
@ -976,5 +978,69 @@ define(function (require, exports, module) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchDevices', function () {
|
||||
var devices;
|
||||
|
||||
beforeEach(function () {
|
||||
account.set('sessionToken', SESSION_TOKEN);
|
||||
|
||||
devices = new Devices([], {
|
||||
notifier: {
|
||||
on: sinon.spy()
|
||||
}
|
||||
});
|
||||
|
||||
sinon.stub(fxaClient, 'deviceList', function () {
|
||||
return p([
|
||||
{
|
||||
id: 'device-1',
|
||||
isCurrentDevice: false,
|
||||
name: 'alpha'
|
||||
},
|
||||
{
|
||||
id: 'device-2',
|
||||
isCurrentDevice: true,
|
||||
name: 'beta'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
return account.fetchDevices(devices);
|
||||
});
|
||||
|
||||
it('fetches the device list from the back end', function () {
|
||||
assert.isTrue(fxaClient.deviceList.calledWith(SESSION_TOKEN));
|
||||
});
|
||||
|
||||
it('populates the `devices` collection', function () {
|
||||
assert.equal(devices.length, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroyDevice', function () {
|
||||
var device;
|
||||
|
||||
beforeEach(function () {
|
||||
account.set('sessionToken', SESSION_TOKEN);
|
||||
|
||||
device = new Device({
|
||||
id: 'device-1',
|
||||
isCurrentDevice: true,
|
||||
name: 'alpha'
|
||||
});
|
||||
|
||||
sinon.stub(fxaClient, 'deviceDestroy', function () {
|
||||
return p();
|
||||
});
|
||||
|
||||
return account.destroyDevice(device);
|
||||
});
|
||||
|
||||
it('tells the backend to destroy the device', function () {
|
||||
assert.isTrue(
|
||||
fxaClient.deviceDestroy.calledWith(SESSION_TOKEN, 'device-1'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/* 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/. */
|
||||
|
||||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var assert = require('chai').assert;
|
||||
var Device = require('models/device');
|
||||
var sinon = require('sinon');
|
||||
|
||||
describe('models/device', function () {
|
||||
var device;
|
||||
|
||||
beforeEach(function () {
|
||||
device = new Device();
|
||||
});
|
||||
|
||||
describe('destroy', function () {
|
||||
beforeEach(function () {
|
||||
sinon.spy(device, 'trigger');
|
||||
|
||||
device.destroy();
|
||||
});
|
||||
|
||||
it('triggers a `destroy` message', function () {
|
||||
assert.isTrue(device.trigger.calledWith('destroy'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
/* 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/. */
|
||||
|
||||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var assert = require('chai').assert;
|
||||
var Devices = require('models/devices');
|
||||
var Notifier = require('lib/channels/notifier');
|
||||
|
||||
describe('models/devices', function () {
|
||||
var devices;
|
||||
var notifier;
|
||||
|
||||
beforeEach(function () {
|
||||
notifier = new Notifier();
|
||||
|
||||
devices = new Devices([], {
|
||||
notifier: notifier
|
||||
});
|
||||
});
|
||||
|
||||
describe('device list ordering', function () {
|
||||
beforeEach(function () {
|
||||
devices.set([
|
||||
{
|
||||
isCurrentDevice: false,
|
||||
name: 'zeta'
|
||||
},
|
||||
{
|
||||
isCurrentDevice: true,
|
||||
name: 'upsilon'
|
||||
},
|
||||
{
|
||||
isCurrentDevice: false,
|
||||
name: 'xi'
|
||||
},
|
||||
{
|
||||
isCurrentDevice: false,
|
||||
name: 'tau'
|
||||
},
|
||||
{
|
||||
isCurrentDevice: false,
|
||||
name: 'tau'
|
||||
},
|
||||
{
|
||||
isCurrentDevice: false,
|
||||
name: 'theta'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('places the `current` device first', function () {
|
||||
assert.equal(devices.at(0).get('name'), 'upsilon');
|
||||
});
|
||||
|
||||
it('sorts the rest alphabetically', function () {
|
||||
assert.equal(devices.at(1).get('name'), 'tau');
|
||||
assert.equal(devices.at(2).get('name'), 'tau');
|
||||
assert.equal(devices.at(3).get('name'), 'theta');
|
||||
assert.equal(devices.at(4).get('name'), 'xi');
|
||||
assert.equal(devices.at(5).get('name'), 'zeta');
|
||||
});
|
||||
});
|
||||
|
||||
describe('device name change', function () {
|
||||
beforeEach(function () {
|
||||
devices.set([
|
||||
{
|
||||
id: 'device-1',
|
||||
isCurrentDevice: false,
|
||||
name: 'zeta'
|
||||
},
|
||||
{
|
||||
id: 'device-2',
|
||||
isCurrentDevice: true,
|
||||
name: 'upsilon'
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -8,6 +8,8 @@ define(function (require, exports, module) {
|
|||
var AuthErrors = require('lib/auth-errors');
|
||||
var chai = require('chai');
|
||||
var Constants = require('lib/constants');
|
||||
var Device = require('models/device');
|
||||
var Devices = require('models/devices');
|
||||
var FxaClient = require('lib/fxa-client');
|
||||
var Notifier = require('lib/channels/notifier');
|
||||
var p = require('lib/promise');
|
||||
|
@ -117,7 +119,7 @@ define(function (require, exports, module) {
|
|||
assert.equal(user.getAccountByUid('uid').get('uid'), 'uid');
|
||||
assert.equal(notifier.triggerRemote.callCount, 1);
|
||||
var args = notifier.triggerRemote.args[0];
|
||||
assert.lengthOf(args, 1);
|
||||
assert.lengthOf(args, 2);
|
||||
assert.equal(args[0], notifier.EVENTS.SIGNED_OUT);
|
||||
});
|
||||
});
|
||||
|
@ -522,5 +524,87 @@ define(function (require, exports, module) {
|
|||
assert.equal(resumeTokenInfo.uniqueUserId, UUID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAccountDevices', function () {
|
||||
var account;
|
||||
var devices;
|
||||
|
||||
beforeEach(function () {
|
||||
account = user.initAccount({});
|
||||
sinon.stub(account, 'fetchDevices', function () {
|
||||
return p();
|
||||
});
|
||||
|
||||
devices = new Devices([], {
|
||||
notifier: {
|
||||
on: sinon.spy()
|
||||
}
|
||||
});
|
||||
|
||||
return user.fetchAccountDevices(account, devices);
|
||||
});
|
||||
|
||||
it('delegates to the account to fetch devices', function () {
|
||||
assert.isTrue(account.fetchDevices.calledWith(devices));
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroyAccountDevice', function () {
|
||||
var account;
|
||||
var device;
|
||||
|
||||
beforeEach(function () {
|
||||
account = user.initAccount({
|
||||
email: 'a@a.com',
|
||||
sessionToken: 'session token',
|
||||
uid: 'the uid'
|
||||
});
|
||||
|
||||
sinon.stub(account, 'destroyDevice', function () {
|
||||
return p();
|
||||
});
|
||||
|
||||
sinon.stub(account, 'fetch', function () {
|
||||
return p();
|
||||
});
|
||||
|
||||
|
||||
sinon.spy(user, 'clearSignedInAccount');
|
||||
|
||||
device = new Device({
|
||||
id: 'device-1',
|
||||
name: 'alpha',
|
||||
sessionToken: 'session token'
|
||||
});
|
||||
|
||||
return user.destroyAccountDevice(account, device);
|
||||
});
|
||||
|
||||
it('delegates to the account to destroy the device', function () {
|
||||
assert.isTrue(account.destroyDevice.calledWith(device));
|
||||
});
|
||||
|
||||
describe('with a remote device', function () {
|
||||
it('does not sign out the current user', function () {
|
||||
assert.isFalse(user.clearSignedInAccount.called);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('with the current account\'s current device', function () {
|
||||
beforeEach(function () {
|
||||
device.set('isCurrentDevice', true);
|
||||
|
||||
return user.setSignedInAccount(account)
|
||||
.then(function () {
|
||||
return user.destroyAccountDevice(account, device);
|
||||
});
|
||||
});
|
||||
|
||||
it('signs out the current account', function () {
|
||||
assert.isTrue(user.clearSignedInAccount.called);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -96,6 +96,15 @@ define(function (require, exports, module) {
|
|||
assert.isTrue($('body').hasClass('layout'));
|
||||
});
|
||||
|
||||
it('triggers the `rendered` message when complete', function () {
|
||||
var deferred = p.defer();
|
||||
|
||||
view.on('rendered', deferred.resolve.bind(deferred));
|
||||
view.render();
|
||||
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
it('updates the page title with the embedded h1 and h2 tags', function () {
|
||||
view.template = function () {
|
||||
return '<header><h1>Main title</h1><h2>Sub title</h2></header>';
|
||||
|
|
|
@ -19,7 +19,7 @@ define(function (require, exports, module) {
|
|||
|
||||
describe('views/mixins/signed-out-notification-mixin', function () {
|
||||
it('exports correct interface', function () {
|
||||
assert.lengthOf(Object.keys(SignedOutNotificationMixin), 2);
|
||||
assert.lengthOf(Object.keys(SignedOutNotificationMixin), 3);
|
||||
assert.isObject(SignedOutNotificationMixin.notifications);
|
||||
assert.isFunction(SignedOutNotificationMixin.clearSessionAndNavigateToSignIn);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
/* 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/. */
|
||||
|
||||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var $ = require('jquery');
|
||||
var _ = require ('underscore');
|
||||
var Able = require('lib/able');
|
||||
var assert = require('chai').assert;
|
||||
var BaseView = require('views/base');
|
||||
var Devices = require('models/devices');
|
||||
var Notifier = require('lib/channels/notifier');
|
||||
var p = require('lib/promise');
|
||||
var sinon = require('sinon');
|
||||
var User = require('models/user');
|
||||
var View = require('views/settings/devices');
|
||||
|
||||
describe('views/settings/devices', function () {
|
||||
var devices;
|
||||
var notifier;
|
||||
var parentView;
|
||||
var user;
|
||||
var view;
|
||||
var able;
|
||||
|
||||
function initView () {
|
||||
view = new View({
|
||||
able: able,
|
||||
devices: devices,
|
||||
notifier: notifier,
|
||||
parentView: parentView,
|
||||
user: user
|
||||
});
|
||||
|
||||
return view.render();
|
||||
}
|
||||
|
||||
function setupReRenderTest(testAction) {
|
||||
return initView()
|
||||
.then(function () {
|
||||
var deferred = p.defer();
|
||||
|
||||
view.on('rendered', deferred.resolve.bind(deferred));
|
||||
|
||||
if (_.isFunction(testAction)) {
|
||||
testAction();
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
able = new Able();
|
||||
sinon.stub(able, 'choose', function () {
|
||||
return true;
|
||||
});
|
||||
notifier = new Notifier();
|
||||
parentView = new BaseView();
|
||||
user = new User();
|
||||
|
||||
devices = new Devices([
|
||||
{
|
||||
id: 'device-1',
|
||||
isCurrentDevice: false,
|
||||
name: 'alpha'
|
||||
},
|
||||
{
|
||||
id: 'device-2',
|
||||
isCurrentDevice: true,
|
||||
name: 'beta'
|
||||
}
|
||||
], {
|
||||
notifier: notifier
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
view.remove();
|
||||
view.destroy();
|
||||
|
||||
view = null;
|
||||
});
|
||||
|
||||
describe('constructor', function () {
|
||||
beforeEach(function () {
|
||||
view = new View({
|
||||
notifier: notifier,
|
||||
parentView: parentView,
|
||||
user: user
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a `Devices` instance if one not passed in', function () {
|
||||
assert.ok(view._devices);
|
||||
});
|
||||
});
|
||||
|
||||
describe('render', function () {
|
||||
beforeEach(function () {
|
||||
sinon.spy(devices, 'fetch');
|
||||
|
||||
return initView();
|
||||
});
|
||||
|
||||
it('does not fetch the device list immediately to avoid startup XHR requests', function () {
|
||||
assert.isFalse(devices.fetch.called);
|
||||
});
|
||||
|
||||
it('renders devices already in the collection', function () {
|
||||
assert.ok(view.$('li.device').length, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('device added to collection', function () {
|
||||
beforeEach(function () {
|
||||
return setupReRenderTest(function () {
|
||||
devices.add({
|
||||
id: 'device-3',
|
||||
name: 'delta'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('adds new device to list', function () {
|
||||
assert.lengthOf(view.$('li.device'), 3);
|
||||
assert.include(view.$('#device-3 .device-name').text().trim(), 'delta');
|
||||
});
|
||||
});
|
||||
|
||||
describe('device removed from collection', function () {
|
||||
beforeEach(function () {
|
||||
return setupReRenderTest(function () {
|
||||
// DOM needs written so that device remove animation completes
|
||||
$('#container').html(view.el);
|
||||
devices.get('device-1').destroy();
|
||||
});
|
||||
});
|
||||
|
||||
it('removes device from list', function () {
|
||||
assert.lengthOf(view.$('li.device'), 1);
|
||||
assert.lengthOf(view.$('#device-2'), 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openPanel', function () {
|
||||
beforeEach(function () {
|
||||
return initView()
|
||||
.then(function () {
|
||||
sinon.stub(view, '_fetchDevices', function () {
|
||||
});
|
||||
return view.openPanel();
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches the device list', function () {
|
||||
assert.isTrue(view._fetchDevices.called);
|
||||
});
|
||||
});
|
||||
|
||||
describe('click to disconnect device', function () {
|
||||
beforeEach(function () {
|
||||
return initView()
|
||||
.then(function () {
|
||||
// click events require the view to be in the DOM
|
||||
$('#container').html(view.el);
|
||||
|
||||
sinon.stub(view, '_destroyDevice', function () {
|
||||
return p();
|
||||
});
|
||||
|
||||
$('#device-2 .device-disconnect').click();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls `_destroyDevice` with the deviceId', function () {
|
||||
assert.isTrue(view._destroyDevice.calledWith('device-2'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('refresh list behaviour', function () {
|
||||
beforeEach(function () {
|
||||
return initView()
|
||||
.then(function () {
|
||||
// click events require the view to be in the DOM
|
||||
$('#container').html(view.el);
|
||||
|
||||
sinon.stub(view, '_fetchDevices', function () {
|
||||
return p();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
it('calls `_fetchDevices` using a button', function () {
|
||||
sinon.stub(view, 'isPanelOpen', function () {
|
||||
return true;
|
||||
});
|
||||
|
||||
$('.devices-refresh').click();
|
||||
assert.isTrue(view._fetchDevices.called);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('_fetchDevices', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(user, 'fetchAccountDevices', function () {
|
||||
return p();
|
||||
});
|
||||
|
||||
return initView()
|
||||
.then(function () {
|
||||
return view._fetchDevices();
|
||||
});
|
||||
});
|
||||
|
||||
it('delegates to the user to fetch the device list', function () {
|
||||
var account = view.getSignedInAccount();
|
||||
assert.isTrue(user.fetchAccountDevices.calledWith(account, devices));
|
||||
});
|
||||
});
|
||||
|
||||
describe('_destroyDevice', function () {
|
||||
var deviceToDestroy;
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(user, 'destroyAccountDevice', function () {
|
||||
return p();
|
||||
});
|
||||
|
||||
return initView()
|
||||
.then(function () {
|
||||
deviceToDestroy = devices.get('device-1');
|
||||
return view._destroyDevice('device-1');
|
||||
});
|
||||
});
|
||||
|
||||
it('delegates to the user to destroy the device', function () {
|
||||
var account = view.getSignedInAccount();
|
||||
assert.isTrue(
|
||||
user.destroyAccountDevice.calledWith(account, deviceToDestroy));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -83,6 +83,7 @@ function (Translator, Session) {
|
|||
'../tests/spec/views/settings/change_password',
|
||||
'../tests/spec/views/settings/communication_preferences',
|
||||
'../tests/spec/views/settings/delete_account',
|
||||
'../tests/spec/views/settings/devices',
|
||||
'../tests/spec/views/settings/display_name',
|
||||
'../tests/spec/views/settings/gravatar_permissions',
|
||||
'../tests/spec/views/sub_panels',
|
||||
|
@ -134,6 +135,8 @@ function (Translator, Session) {
|
|||
'../tests/spec/models/unique-user-id',
|
||||
'../tests/spec/models/user',
|
||||
'../tests/spec/models/account',
|
||||
'../tests/spec/models/device',
|
||||
'../tests/spec/models/devices',
|
||||
'../tests/spec/models/profile-image',
|
||||
'../tests/spec/models/email-resend',
|
||||
'../tests/spec/models/form-prefill',
|
||||
|
|
|
@ -112,6 +112,7 @@ module.exports = function (config, i18n) {
|
|||
'/settings/change_password',
|
||||
'/settings/communication_preferences',
|
||||
'/settings/delete_account',
|
||||
'/settings/devices',
|
||||
'/settings/display_name',
|
||||
'/signin',
|
||||
'/signin_complete',
|
||||
|
|
|
@ -31,6 +31,7 @@ define([
|
|||
'./functional/robots_txt',
|
||||
'./functional/settings',
|
||||
'./functional/settings_common',
|
||||
'./functional/settings_devices',
|
||||
'./functional/sync_settings',
|
||||
'./functional/change_password',
|
||||
'./functional/force_auth',
|
||||
|
|
|
@ -53,6 +53,7 @@ define([
|
|||
'settings/change_password',
|
||||
'settings/communication_preferences',
|
||||
'settings/delete_account',
|
||||
'settings/devices',
|
||||
'settings/display_name',
|
||||
'signin',
|
||||
'signin_complete',
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/* 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/. */
|
||||
|
||||
define([
|
||||
'intern',
|
||||
'intern/chai!assert',
|
||||
'intern!object',
|
||||
'intern/node_modules/dojo/node!xmlhttprequest',
|
||||
'app/bower_components/fxa-js-client/fxa-client',
|
||||
'tests/lib/helpers',
|
||||
'tests/functional/lib/helpers'
|
||||
], function (intern, assert, registerSuite, nodeXMLHttpRequest,
|
||||
FxaClient, TestHelpers, FunctionalHelpers) {
|
||||
|
||||
var config = intern.config;
|
||||
var AUTH_SERVER_ROOT = config.fxaAuthRoot;
|
||||
var SIGNIN_URL = config.fxaContentRoot + 'signin';
|
||||
var SIGNIN_URL_DEVICE_LIST = SIGNIN_URL + '?forceDeviceList=1';
|
||||
|
||||
var ANIMATION_TIME = 1000;
|
||||
var FIRST_PASSWORD = 'password';
|
||||
var BROWSER_DEVICE_NAME = 'Browser Session Device';
|
||||
var BROWSER_DEVICE_TYPE = 'desktop';
|
||||
var TEST_DEVICE_NAME = 'Test Runner Session Device';
|
||||
var TEST_DEVICE_TYPE = 'mobile';
|
||||
var email;
|
||||
var client;
|
||||
var accountData;
|
||||
|
||||
registerSuite({
|
||||
name: 'settings devices',
|
||||
|
||||
beforeEach: function () {
|
||||
email = TestHelpers.createEmail();
|
||||
client = new FxaClient(AUTH_SERVER_ROOT, {
|
||||
xhr: nodeXMLHttpRequest.XMLHttpRequest
|
||||
});
|
||||
var self = this;
|
||||
return client.signUp(email, FIRST_PASSWORD, {preVerified: true})
|
||||
.then(function (result) {
|
||||
accountData = result;
|
||||
return FunctionalHelpers.clearBrowserState(self);
|
||||
});
|
||||
},
|
||||
|
||||
afterEach: function () {
|
||||
return FunctionalHelpers.clearBrowserState(this);
|
||||
},
|
||||
|
||||
'device panel is not visible without query param': function () {
|
||||
var self = this;
|
||||
return FunctionalHelpers.openPage(this, SIGNIN_URL, '#fxa-signin-header')
|
||||
.then(function () {
|
||||
return FunctionalHelpers.fillOutSignIn(self, email, FIRST_PASSWORD);
|
||||
})
|
||||
|
||||
.findByCssSelector('#fxa-settings-header')
|
||||
.end()
|
||||
|
||||
.then(FunctionalHelpers.noSuchElement(this, '#devices'));
|
||||
},
|
||||
|
||||
'device panel works with query param, same device': function () {
|
||||
var self = this;
|
||||
return FunctionalHelpers.openPage(this, SIGNIN_URL_DEVICE_LIST, '#fxa-signin-header')
|
||||
.then(function () {
|
||||
return FunctionalHelpers.fillOutSignIn(self, email, FIRST_PASSWORD);
|
||||
})
|
||||
|
||||
.findByCssSelector('#fxa-settings-header')
|
||||
.end()
|
||||
|
||||
.findByCssSelector('#devices .settings-unit-stub button')
|
||||
.click()
|
||||
.end()
|
||||
|
||||
.findByCssSelector('.devices-refresh')
|
||||
.click()
|
||||
.end()
|
||||
|
||||
// add a device from the test runner
|
||||
.then(function () {
|
||||
return client.deviceRegister(
|
||||
accountData.sessionToken,
|
||||
TEST_DEVICE_NAME,
|
||||
TEST_DEVICE_TYPE
|
||||
);
|
||||
})
|
||||
|
||||
.findByCssSelector('.devices-refresh')
|
||||
.click()
|
||||
.end()
|
||||
|
||||
.findByCssSelector('.device-name')
|
||||
.getVisibleText()
|
||||
.then(function (val) {
|
||||
assert.equal(val, TEST_DEVICE_NAME, 'device name is correct');
|
||||
})
|
||||
.end()
|
||||
|
||||
// add a device using a session token from the browser
|
||||
.execute(function (uid) {
|
||||
var accounts = JSON.parse(localStorage.getItem('__fxa_storage.accounts'));
|
||||
|
||||
return accounts[uid];
|
||||
}, [ accountData.uid ])
|
||||
.then(function (browserAccount) {
|
||||
return client.deviceRegister(
|
||||
browserAccount.sessionToken,
|
||||
BROWSER_DEVICE_NAME,
|
||||
BROWSER_DEVICE_TYPE
|
||||
);
|
||||
})
|
||||
|
||||
.findByCssSelector('.devices-refresh')
|
||||
.click()
|
||||
.end()
|
||||
|
||||
// wait for 2 devices
|
||||
.findByCssSelector('.device:nth-child(2)')
|
||||
.end()
|
||||
|
||||
// browser device should be sorted first
|
||||
.findByCssSelector('.device:nth-child(1) .device-name')
|
||||
.getVisibleText()
|
||||
.then(function (val) {
|
||||
assert.equal(val, BROWSER_DEVICE_NAME + ' (current)', 'device name is correct');
|
||||
})
|
||||
.end()
|
||||
|
||||
// clicking disconnect on the second device should update the list
|
||||
.findByCssSelector('.device:nth-child(2) .device-disconnect')
|
||||
.click()
|
||||
.end()
|
||||
|
||||
.sleep(ANIMATION_TIME)
|
||||
|
||||
.findByCssSelector('.devices-refresh')
|
||||
.click()
|
||||
.end()
|
||||
|
||||
.then(FunctionalHelpers.noSuchElement(this, '.device:nth-child(2)'))
|
||||
|
||||
// clicking disconnect on the current device should sign you out
|
||||
.findByCssSelector('.device:nth-child(1) .device-disconnect')
|
||||
.click()
|
||||
.end()
|
||||
|
||||
.findByCssSelector('#fxa-signin-header')
|
||||
.end();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -31,6 +31,7 @@ define([
|
|||
'tests/functional/robots_txt',
|
||||
'tests/functional/settings',
|
||||
'tests/functional/settings_common',
|
||||
'tests/functional/settings_devices',
|
||||
'tests/functional/sync_settings',
|
||||
'tests/functional/change_password',
|
||||
'tests/functional/force_auth',
|
||||
|
|
|
@ -59,6 +59,7 @@ define([
|
|||
'/settings/avatar/gravatar_permissions': { statusCode: 200 },
|
||||
'/settings/change_password': { statusCode: 200 },
|
||||
'/settings/delete_account': { statusCode: 200 },
|
||||
'/settings/devices': { statusCode: 200 },
|
||||
'/settings/display_name': { statusCode: 200 },
|
||||
'/signin': { statusCode: 200 },
|
||||
'/signin_complete': { statusCode: 200 },
|
||||
|
|
Загрузка…
Ссылка в новой задаче