diff --git a/features/accountManagement.feature b/features/accountManagement.feature deleted file mode 100644 index dfb3c5005..000000000 --- a/features/accountManagement.feature +++ /dev/null @@ -1,49 +0,0 @@ -Feature: Account management - Scenario: should allow to save account locally, after page reload it should require passphrase to do the first transaction, and remember the passphrase for next transactions - Given I'm logged in as "genesis" - When I click "save account" in main menu - And I click "save account button" - And I wait 1 seconds - And I should see text "Account saved" in "toast" element - And I refresh the page - And I wait 2 seconds - Then I should be logged in - And I click "send button" - And I should see empty "passphrase" field - And I fill in "1" to "amount" field - And I fill in "537318935439898807L" to "recipient" field - And I fill in passphrase of "genesis" to "passphrase" field - And I click "submit button" - And I click "ok button" - And I wait 1 seconds - And I click "send button" - And I fill in "2" to "amount" field - And I fill in "537318935439898807L" to "recipient" field - And I click "submit button" - And I should see alert dialog with title "Success" and text "Your transaction of 2 LSK to 537318935439898807L was accepted and will be processed in a few seconds." - - Scenario: should allow to forget locally saved account - Given I'm logged in as "any account" - When I click "save account" in main menu - And I click "save account button" - And I refresh the page - And I wait 2 seconds - And I click "forget account" in main menu - And I wait 1 seconds - Then I should see text "Account was successfully forgotten." in "toast" element - And I refresh the page - And I should be on login page - - @integration - Scenario: should allow to exit save account dialog with "cancel button" - Given I'm logged in as "any account" - When I click "save account" in main menu - And I click "cancel button" - Then I should see no "modal dialog" - - @integration - Scenario: should allow to exit save account dialog with "x button" - Given I'm logged in as "any account" - When I click "save account" in main menu - And I click "x button" - Then I should see no "modal dialog" diff --git a/features/savedAccounts.feature b/features/savedAccounts.feature new file mode 100644 index 000000000..742940316 --- /dev/null +++ b/features/savedAccounts.feature @@ -0,0 +1,82 @@ +Feature: Saved Accounts + Scenario: should allow to save account locally, after page reload it should require passphrase to do the first transaction, and remember the passphrase for next transactions + Given I'm logged in as "genesis" + When I click "saved accounts" in main menu + And I click "add active account button" + And I click "x button" + And I wait 1 seconds + And I should see text "Account saved" in "toast" element + And I refresh the page + And I wait 2 seconds + Then I should be logged in + And I click "send button" + And I should see empty "passphrase" field + And I fill in "1" to "amount" field + And I fill in "537318935439898807L" to "recipient" field + And I fill in passphrase of "genesis" to "passphrase" field + And I click "submit button" + And I click "ok button" + And I wait 1 seconds + And I click "send button" + And I fill in "2" to "amount" field + And I fill in "537318935439898807L" to "recipient" field + And I click "submit button" + And I should see alert dialog with title "Success" and text "Your transaction of 2 LSK to 537318935439898807L was accepted and will be processed in a few seconds." + + Scenario: should allow to save second account + Given I'm logged in as "genesis" + When I click "saved accounts" in main menu + And I click "add active account button" + And I click "x button" + And I wait 1 seconds + And I click "logout button" + And I'm logged in as "empty account" + And I click "saved accounts" in main menu + And I click "add active account button" + Then I should see "saved accounts table" table with 2 lines + And I refresh the page + And I wait 2 seconds + And I should be logged in as "empty account" account + + Scenario: should allow to forget second account + Given I'm logged in as "genesis" + When I click "saved accounts" in main menu + And I click "add active account button" + And I click "x button" + And I wait 1 seconds + And I click "logout button" + And I'm logged in as "delegate" + And I click "saved accounts" in main menu + And I click "add active account button" + And I should see "saved accounts table" table with 2 lines + And I click "forget button" + Then I should see "saved accounts table" table with 1 lines + + @pending + Scenario: should allow to switch account + Given I'm logged in as "genesis" + When I click "saved accounts" in main menu + And I click "add active account button" + And I click "x button" + And I wait 1 seconds + And I click "logout button" + And I'm logged in as "delegate" + And I click "saved accounts" in main menu + And I click "add active account button" + And I click "switch button" + And I wait 1 seconds + And I should be logged in as "genesis" account + + Scenario: should login to last active saved account + Given I'm logged in as "genesis" + When I click "saved accounts" in main menu + And I click "add active account button" + And I click "x button" + And I wait 1 seconds + And I click "logout button" + And I'm logged in as "empty account" + And I click "saved accounts" in main menu + And I click "add active account button" + And I click "x button" + And I refresh the page + And I should be logged in as "empty account" account diff --git a/features/step_definitions/generic.step.js b/features/step_definitions/generic.step.js index 6c976a2c7..a3606a328 100644 --- a/features/step_definitions/generic.step.js +++ b/features/step_definitions/generic.step.js @@ -99,6 +99,13 @@ defineSupportCode(({ Given, When, Then, setDefaultTimeout }) => { .and.notify(callback); }); + Then('I should see "{elementName}" table with {lineCount} lines', (elementName, lineCount, callback) => { + browser.sleep(500); + expect(element.all(by.css(`table.${elementName.replace(/ /g, '-')} tbody tr`)).count()).to.eventually.equal(parseInt(lineCount, 10)) + .and.notify(callback); + }); + + Then('I should see no "{elementName}"', (elementName, callback) => { const selector = `.${elementName.replace(/ /g, '-')}`; waitForElemRemoved(selector).then(() => { @@ -139,7 +146,6 @@ defineSupportCode(({ Given, When, Then, setDefaultTimeout }) => { }); Given('I\'m logged in as "{accountName}"', { timeout: 2 * defaultTimeout }, (accountName, callback) => { - browser.get(browser.params.baseURL); const passphrase = browser.params.useTestnetPassphrase ? browser.params.testnetPassphrase : accounts[accountName].passphrase; @@ -200,6 +206,14 @@ defineSupportCode(({ Given, When, Then, setDefaultTimeout }) => { browser.executeScript('window.scrollBy(0, 10000);'); }); + Then('I should be logged in', (callback) => { + waitForElemAndCheckItsText('.logout-button', 'LOGOUT', callback); + }); + + Then('I should be logged in as "{accountName}" account', (accountName, callback) => { + waitForElemAndCheckItsText('.full.address', accounts[accountName].address, callback); + }); + When('I click "{itemSelector}" in main menu', (itemSelector, callback) => { waitForElemAndClickIt('.main-menu-icon-button'); browser.sleep(1000); diff --git a/features/step_definitions/hooks.js b/features/step_definitions/hooks.js index 60a4b3892..8ade23514 100644 --- a/features/step_definitions/hooks.js +++ b/features/step_definitions/hooks.js @@ -41,6 +41,7 @@ defineSupportCode(({ Before, After }) => { localStorage.clear(); localStorage.setItem('address', browser.params.liskCoreURL); localStorage.setItem('network', networks[browser.params.network].code); + browser.get(browser.params.baseURL); callback(); }); diff --git a/features/step_definitions/login.step.js b/features/step_definitions/login.step.js index 31ed605f7..236e15e10 100644 --- a/features/step_definitions/login.step.js +++ b/features/step_definitions/login.step.js @@ -1,14 +1,9 @@ /* eslint-disable import/no-extraneous-dependencies */ const { defineSupportCode } = require('cucumber'); -const { waitForElemAndCheckItsText } = require('../support/util.js'); -defineSupportCode(({ Given, Then }) => { +defineSupportCode(({ Given }) => { Given('I\'m on login page', (callback) => { browser.get(browser.params.baseURL).then(callback); }); - - Then('I should be logged in', (callback) => { - waitForElemAndCheckItsText('.logout-button', 'LOGOUT', callback); - }); }); diff --git a/i18n/locales/en/common.json b/i18n/locales/en/common.json index f2035c3a6..abdb99fe1 100644 --- a/i18n/locales/en/common.json +++ b/i18n/locales/en/common.json @@ -3,6 +3,7 @@ "About": "About", "Account saved": "Account saved", "Account was successfully forgotten.": "Account was successfully forgotten.", + "Add active account": "Add active account", "Add vote to": "Add vote to", "Address": "Address", "Address copied to clipboard": "Address copied to clipboard", @@ -19,6 +20,7 @@ "Blockchain Application Registration": "Blockchain Application Registration", "Cancel": "Cancel", "Click to send all funds": "Click to send all funds", + "Close": "Close", "Confirm": "Confirm", "Connection re-established": "Connection re-established", "Copy": "Copy", @@ -42,7 +44,7 @@ "Fee": "Fee", "Fee: {{amount}} LSK": "Fee: {{amount}} LSK", "Fee: {{fee}} LSK": "Fee: {{fee}} LSK", - "Forget this account": "Forget this account", + "Forget": "Forget", "Forging": "Forging", "From / To": "From / To", "Help": "Help", @@ -73,6 +75,7 @@ "Move your mouse to generate random bytes": "Move your mouse to generate random bytes", "Multisignature Creation": "Multisignature Creation", "Name": "Name", + "Network": "Network", "New Account": "New Account", "Next": "Next", "No delegates found": "No delegates found", @@ -112,7 +115,6 @@ "Register as delegate": "Register as delegate", "Register second passphrase": "Register second passphrase", "Reload": "Reload", - "Remember this account": "Remember this account", "Remove vote from": "Remove vote from", "Repeat the transaction": "Repeat the transaction", "Report Issue...": "Report Issue...", @@ -120,8 +122,8 @@ "Result": "Result", "Result copied to clipboard": "Result copied to clipboard", "Reward": "Reward", - "Save account": "Save account", "Save your passphrase in a safe place": "Save your passphrase in a safe place", + "Saved accounts": "Saved accounts", "Search": "Search", "Search by username": "Search by username", "Second Passphrase": "Second Passphrase", @@ -143,6 +145,7 @@ "Signing a message with this tool indicates ownership of a privateKey (secret) and provides a level of proof that you are the owner of the key. Its important to bear in mind that this is not a 100% proof as computer systems can be compromised, but is still an effective tool for proving ownership of a particular publicKey/address pair.": "Signing a message with this tool indicates ownership of a privateKey (secret) and provides a level of proof that you are the owner of the key. Its important to bear in mind that this is not a 100% proof as computer systems can be compromised, but is still an effective tool for proving ownership of a particular publicKey/address pair.", "Submit": "Submit", "Success": "Success", + "Switch": "Switch", "Testnet": "Testnet", "The URL was invalid": "The URL was invalid", "There are no transactions, yet.": "There are no transactions, yet.", diff --git a/src/actions/savedAccounts.js b/src/actions/savedAccounts.js index 2c9ba928b..862bb3b78 100644 --- a/src/actions/savedAccounts.js +++ b/src/actions/savedAccounts.js @@ -1,14 +1,21 @@ import actionTypes from '../constants/actions'; -import { getSavedAccount, setSavedAccount, removeSavedAccount } from '../utils/saveAccount'; +import { + getSavedAccounts, + setSavedAccount, + removeSavedAccount, + setLastActiveAccount, + getLastActiveAccount, +} from '../utils/savedAccounts'; /** * An action to dispatch accountSaved * */ -export const accountSaved = (data) => { - setSavedAccount(data); +export const accountSaved = (account) => { + setSavedAccount(account); + setLastActiveAccount(account); return { - data, + data: account, type: actionTypes.accountSaved, }; }; @@ -16,14 +23,25 @@ export const accountSaved = (data) => { /** * An action to dispatch accountRemoved */ -export const accountRemoved = (publicKey) => { - removeSavedAccount(publicKey); +export const accountRemoved = (account) => { + removeSavedAccount(account); return { - data: publicKey, + data: account, type: actionTypes.accountRemoved, }; }; +/** + * An action to dispatch accountSwitched + */ +export const accountSwitched = (account) => { + setLastActiveAccount(account); + return { + data: account, + type: actionTypes.accountSwitched, + }; +}; + /** * The action to initiate savedAccounts store with the retrieved accounts * @@ -31,11 +49,10 @@ export const accountRemoved = (publicKey) => { * eventually it should receive an array and return that to reducer * */ -export const accountsRetrieved = () => { - const accounts = getSavedAccount(); - const data = accounts !== undefined ? accounts : []; - return { - data, - type: actionTypes.accountsRetrieved, - }; -}; +export const accountsRetrieved = () => ({ + data: { + accounts: getSavedAccounts(), + lastActive: getLastActiveAccount(), + }, + type: actionTypes.accountsRetrieved, +}); diff --git a/src/actions/savedAccounts.test.js b/src/actions/savedAccounts.test.js index c2fc3ff7d..06191fa23 100644 --- a/src/actions/savedAccounts.test.js +++ b/src/actions/savedAccounts.test.js @@ -1,9 +1,10 @@ import { expect } from 'chai'; import sinon from 'sinon'; import actionTypes from '../constants/actions'; -import * as saveAccountUtils from '../utils/saveAccount'; +import * as savedAccountsUtils from '../utils/savedAccounts'; import { accountSaved, + accountSwitched, accountRemoved, accountsRetrieved, } from './savedAccounts'; @@ -18,12 +19,19 @@ describe('actions: savedAccount', () => { describe('accountsRetrieved', () => { it('should create an action to retrieved the saved accounts list', () => { - sinon.stub(saveAccountUtils, 'getSavedAccount').returns([data]); + const getSavedAccountsStub = sinon.stub(savedAccountsUtils, 'getSavedAccounts').returns([data]); + const getLastActiveAccountStub = sinon.stub(savedAccountsUtils, 'getLastActiveAccount').returns(data); const expectedAction = { - data: [data], + data: { + accounts: [data], + lastActive: data, + }, type: actionTypes.accountsRetrieved, }; expect(accountsRetrieved()).to.be.deep.equal(expectedAction); + + getSavedAccountsStub.restore(); + getLastActiveAccountStub.restore(); }); }); @@ -37,6 +45,16 @@ describe('actions: savedAccount', () => { }); }); + describe('accountSwitched', () => { + it('should create an action to save account', () => { + const expectedAction = { + data, + type: actionTypes.accountSwitched, + }; + expect(accountSwitched(data)).to.be.deep.equal(expectedAction); + }); + }); + describe('accountRemoved', () => { it('should create an action to remove account', () => { const expectedAction = { diff --git a/src/components/dialog/dialogs.js b/src/components/dialog/dialogs.js index 95f1ea0fd..5d5d1412d 100644 --- a/src/components/dialog/dialogs.js +++ b/src/components/dialog/dialogs.js @@ -2,7 +2,7 @@ import i18next from 'i18next'; import ReceiveDialog from '../receiveDialog'; import Register from '../register'; import RegisterDelegate from '../registerDelegate'; -import SaveAccount from '../saveAccount'; +import SavedAccounts from '../savedAccounts'; import SecondPassphrase from '../secondPassphrase'; import Send from '../send'; import Settings from '../settings'; @@ -45,9 +45,9 @@ export default () => ({ title: i18next.t('New Account'), component: Register, }, - 'save-account': { - title: i18next.t('Remember this account'), - component: SaveAccount, + 'saved-accounts': { + title: i18next.t('Saved accounts'), + component: SavedAccounts, }, settings: { title: i18next.t('Settings'), diff --git a/src/components/header/header.js b/src/components/header/header.js index 2765976c9..b82455285 100644 --- a/src/components/header/header.js +++ b/src/components/header/header.js @@ -4,7 +4,6 @@ import React from 'react'; import grid from 'flexboxgrid/dist/flexboxgrid.css'; import PrivateWrapper from '../privateWrapper'; -import SaveAccountButton from '../saveAccountButton'; import logo from '../../assets/images/LISK-nano.png'; import offlineStyle from '../offlineWrapper/offlineWrapper.css'; import styles from './header.css'; @@ -53,7 +52,10 @@ const Header = props => ( to='decrypt-message'>{props.t('Decrypt message')} - + + {props.t('Saved accounts')} + {props.t('Settings')} diff --git a/src/components/header/header.test.js b/src/components/header/header.test.js index 2550a535e..f5c0c6772 100644 --- a/src/components/header/header.test.js +++ b/src/components/header/header.test.js @@ -45,8 +45,8 @@ describe('Header', () => { expect(wrapper.find(Button)).to.have.length(1); }); - it('renders 9 RelativeLink components', () => { - expect(wrapper.find(RelativeLink)).to.have.length(9); + it('renders 10 RelativeLink components', () => { + expect(wrapper.find(RelativeLink)).to.have.length(10); }); it('should have an image with source of "logo"', () => { diff --git a/src/components/login/login.js b/src/components/login/login.js index afd76fe8d..2dceb386e 100644 --- a/src/components/login/login.js +++ b/src/components/login/login.js @@ -127,8 +127,8 @@ class Login extends React.Component { autoLogin() { const { savedAccounts } = this.props; - if (savedAccounts && savedAccounts.length > 0 && !this.props.account.afterLogout) { - this.account = savedAccounts[0]; + if (savedAccounts && savedAccounts.lastActive && !this.props.account.afterLogout) { + this.account = savedAccounts.lastActive; const network = Object.assign({}, getNetwork(this.account.network)); if (this.account.network === networks.customNode.code) { network.address = this.account.address; diff --git a/src/components/saveAccount/index.js b/src/components/saveAccount/index.js deleted file mode 100644 index 07cba1897..000000000 --- a/src/components/saveAccount/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import { connect } from 'react-redux'; -import { translate } from 'react-i18next'; -import { accountSaved } from '../../actions/savedAccounts'; -import SaveAccount from './saveAccount'; - -const mapStateToProps = state => ({ - address: state.peers.data.options.address, - publicKey: state.account.publicKey, - network: state.peers.data.options.name, -}); - -const mapDispatchToProps = dispatch => ({ - accountSaved: data => dispatch(accountSaved(data)), -}); - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(translate()(SaveAccount)); diff --git a/src/components/saveAccount/index.test.js b/src/components/saveAccount/index.test.js deleted file mode 100644 index 656892f33..000000000 --- a/src/components/saveAccount/index.test.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { expect } from 'chai'; -import { mount } from 'enzyme'; -import configureMockStore from 'redux-mock-store'; -import sinon from 'sinon'; -import PropTypes from 'prop-types'; -import i18n from '../../i18n'; -import SaveAccountHOC from './index'; -import * as savedAccounts from '../../actions/savedAccounts'; - - -describe('SaveAccountHOC', () => { - let wrapper; - - const account = { - isDelegate: false, - publicKey: 'sample_key', - username: 'lisk-nano', - }; - const peers = { data: { - options: { - address: 'http://localhost:4000', - network: 'Custom node', - }, - } }; - const store = configureMockStore([])({ - peers, - account, - activePeerSet: () => {}, - }); - - beforeEach(() => { - wrapper = mount( {}} t={(key => key)} />, { - context: { store, i18n }, - childContextTypes: { - store: PropTypes.object.isRequired, - i18n: PropTypes.object.isRequired, - }, - }); - }); - - it('should render SaveAccount', () => { - expect(wrapper.find('SaveAccount')).to.have.lengthOf(1); - }); - - it('should bind accountSaved action to SaveAccount props.accountSaved', () => { - const actionsSpy = sinon.spy(savedAccounts, 'accountSaved'); - wrapper.find('SaveAccount .save-account-button button').simulate('click'); - expect(actionsSpy).to.be.calledWith(); - actionsSpy.restore(); - }); -}); - diff --git a/src/components/saveAccount/saveAccount.js b/src/components/saveAccount/saveAccount.js deleted file mode 100644 index da86a6540..000000000 --- a/src/components/saveAccount/saveAccount.js +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import InfoParagraph from '../infoParagraph'; -import ActionBar from '../actionBar'; -import networks from '../../constants/networks'; - -const SaveAccount = ({ - network, - address, - publicKey, - closeDialog, - accountSaved, - t, -}) => { - const save = () => { - // eslint-disable-next-line arrow-body-style - const index = Object.keys(networks).map((item, i) => { - return (networks[item].name === network) ? i : null; - }).find(item => item !== null); - accountSaved({ - network: index, - address, - publicKey, - }); - closeDialog(); - }; - - return ( -
- - {t('This will save public key of your account on this device, so next time it will launch without the need to log in. However, you will be prompted to enter the passphrase once you want to do any transaction.')} - - -
- ); -}; - -export default SaveAccount; diff --git a/src/components/saveAccount/saveAccount.test.js b/src/components/saveAccount/saveAccount.test.js deleted file mode 100644 index d2d6b41cd..000000000 --- a/src/components/saveAccount/saveAccount.test.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { expect } from 'chai'; -import { mount } from 'enzyme'; -import { spy } from 'sinon'; -import configureStore from 'redux-mock-store'; -import PropTypes from 'prop-types'; -import i18n from '../../i18n'; -import SaveAccount from './saveAccount'; - -const fakeStore = configureStore(); - -describe('SaveAccount', () => { - let wrapper; - let closeDialogSpy; - let accountSavedSpy; - - const props = { - account: { - publicKey: 'fab9d261ea050b9e326d7e11587eccc343a20e64e29d8781b50fd06683cacc88', - }, - closeDialog: () => {}, - accountSaved: () => {}, - t: key => key, - }; - - beforeEach(() => { - closeDialogSpy = spy(props, 'closeDialog'); - accountSavedSpy = spy(props, 'accountSaved'); - const store = fakeStore({ - account: { - balance: 100e8, - }, - }); - wrapper = mount(, { - context: { store, i18n }, - childContextTypes: { - store: PropTypes.object.isRequired, - i18n: PropTypes.object.isRequired, - }, - }); - }); - - afterEach(() => { - closeDialogSpy.restore(); - accountSavedSpy.restore(); - }); - - it.skip('should render ActionBar', () => { - expect(wrapper.find('ActionBar')).to.have.lengthOf(1); - }); - - it('should call props.closeDialog and props.accountSaved on "save button" click', () => { - wrapper.find('button.save-account-button').simulate('click'); - const componentProps = wrapper.find(SaveAccount).props(); - expect(componentProps.closeDialog).to.have.been.calledWith(); - expect(componentProps.accountSaved).to.have.been.calledWith(); - }); -}); - diff --git a/src/components/saveAccountButton/index.js b/src/components/saveAccountButton/index.js deleted file mode 100644 index 2d3aec1a4..000000000 --- a/src/components/saveAccountButton/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import { connect } from 'react-redux'; -import { translate } from 'react-i18next'; - -import { accountRemoved } from '../../actions/savedAccounts'; -import SaveAccountButton from './saveAccountButton'; - -const mapStateToProps = state => ({ - account: state.account, - savedAccounts: state.savedAccounts, -}); - -const mapDispatchToProps = dispatch => ({ - accountRemoved: data => dispatch(accountRemoved(data)), -}); - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(translate()(SaveAccountButton)); diff --git a/src/components/saveAccountButton/index.test.js b/src/components/saveAccountButton/index.test.js deleted file mode 100644 index b3422cc06..000000000 --- a/src/components/saveAccountButton/index.test.js +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { expect } from 'chai'; -import { mount } from 'enzyme'; -import configureMockStore from 'redux-mock-store'; -import { BrowserRouter as Router } from 'react-router-dom'; -import PropTypes from 'prop-types'; -import i18n from '../../i18n'; -import SaveAccountButtonHOC from './index'; -import SaveAccountButton from './saveAccountButton'; - -describe('SaveAccountButtonHOC', () => { - let props; - let wrapper; - - const account = { - isDelegate: false, - publicKey: 'sample_key', - username: 'lisk-nano', - }; - const savedAccounts = []; - const store = configureMockStore([])({ - savedAccounts, - account, - activePeerSet: () => {}, - }); - - beforeEach(() => { - wrapper = mount(, { - context: { store, i18n }, - childContextTypes: { - store: PropTypes.object.isRequired, - i18n: PropTypes.object.isRequired, - }, - }); - props = wrapper.find(SaveAccountButton).props(); - }); - - it('should render the SaveAccountButton with props.successToast and props.setActiveDialog', () => { - expect(wrapper.find(SaveAccountButton).exists()).to.equal(true); - expect(props.account).to.equal(account); - expect(props.savedAccounts).to.equal(savedAccounts); - expect(typeof props.accountRemoved).to.equal('function'); - }); -}); - diff --git a/src/components/saveAccountButton/saveAccountButton.js b/src/components/saveAccountButton/saveAccountButton.js deleted file mode 100644 index 1ef4098b7..000000000 --- a/src/components/saveAccountButton/saveAccountButton.js +++ /dev/null @@ -1,18 +0,0 @@ -import { MenuItem } from 'react-toolbox/lib/menu'; -import React from 'react'; -import RelativeLink from '../relativeLink'; - -const SaveAccountButton = ({ account, savedAccounts, accountRemoved, t, theme }) => - (savedAccounts.length > 0 ? - : - - - {t('Save account')} - - - ); - -export default SaveAccountButton; diff --git a/src/components/saveAccountButton/saveAccountButton.test.js b/src/components/saveAccountButton/saveAccountButton.test.js deleted file mode 100644 index 5e7239123..000000000 --- a/src/components/saveAccountButton/saveAccountButton.test.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import { expect } from 'chai'; -import { mount } from 'enzyme'; -import sinon from 'sinon'; -import PropTypes from 'prop-types'; -import configureMockStore from 'redux-mock-store'; -import { BrowserRouter as Router } from 'react-router-dom'; -import SaveAccountButton from './saveAccountButton'; - -describe('SaveAccountButton', () => { - const account = { publicKey: 'sampleKey' }; - const emptySavedAccounts = []; - const savedAccounts = [account]; - const props = { - theme: { - menuLink: 'some class', - menuItem: 'some other class', - }, - account, - accountRemoved: sinon.spy(), - t: key => key, - }; - - - const store = configureMockStore([])({ - account, - activePeerSet: () => {}, - }); - const options = { - context: { store }, - childContextTypes: { - store: PropTypes.object.isRequired, - }, - }; - - it('fires accountRemoved action if an account is already saved', () => { - const wrapper = mount( - , options); - wrapper.find('MenuItem').simulate('click'); - expect(props.accountRemoved).to.have.been.calledWith(); - }); - - it('Allows to open SaveAccount modal if account not yet saved', () => { - const wrapper = mount( - , options); - expect(wrapper.find('RelativeLink').exists()).to.equal(true); - }); -}); diff --git a/src/components/savedAccounts/index.js b/src/components/savedAccounts/index.js new file mode 100644 index 000000000..65f348e6d --- /dev/null +++ b/src/components/savedAccounts/index.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import { translate } from 'react-i18next'; +import { accountSaved, accountRemoved, accountSwitched } from '../../actions/savedAccounts'; +import SavedAccounts from './savedAccounts'; + +const mapStateToProps = state => ({ + publicKey: state.account.publicKey, + networkOptions: state.peers.options, + savedAccounts: state.savedAccounts.accounts, +}); + +const mapDispatchToProps = dispatch => ({ + accountSaved: data => dispatch(accountSaved(data)), + accountRemoved: data => dispatch(accountRemoved(data)), + accountSwitched: data => dispatch(accountSwitched(data)), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(translate()(SavedAccounts)); diff --git a/src/components/savedAccounts/index.test.js b/src/components/savedAccounts/index.test.js new file mode 100644 index 000000000..e02f41682 --- /dev/null +++ b/src/components/savedAccounts/index.test.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import configureMockStore from 'redux-mock-store'; +import sinon from 'sinon'; +import PropTypes from 'prop-types'; +import i18n from '../../i18n'; +import SavedAccountsHOC from './index'; +import * as savedAccounts from '../../actions/savedAccounts'; + +describe('SavedAccountsHOC', () => { + let wrapper; + + const account = { + isDelegate: false, + publicKey: 'sample_key', + username: 'lisk-nano', + }; + const options = { + address: 'http://localhost:4000', + network: 'Custom node', + }; + const peers = { data: { options }, options }; + const store = configureMockStore([])({ + peers, + account, + activePeerSet: () => {}, + savedAccounts: { accounts: [] }, + }); + + beforeEach(() => { + wrapper = mount( {}} t={(key => key)} />, { + context: { store, i18n }, + childContextTypes: { + store: PropTypes.object.isRequired, + i18n: PropTypes.object.isRequired, + }, + }); + }); + + it('should render SavedAccounts', () => { + expect(wrapper.find('SavedAccounts')).to.have.lengthOf(1); + }); + + it('should bind accountSaved action to SavedAccounts props.accountSaved', () => { + const actionsSpy = sinon.spy(savedAccounts, 'accountSaved'); + wrapper.find('SavedAccounts').props().accountSaved({}); + expect(actionsSpy).to.be.calledWith(); + actionsSpy.restore(); + }); + + it('should bind accountRemoved action to SavedAccounts props.accountRemoved', () => { + const actionsSpy = sinon.spy(savedAccounts, 'accountRemoved'); + wrapper.find('SavedAccounts').props().accountRemoved({}); + expect(actionsSpy).to.be.calledWith(); + actionsSpy.restore(); + }); + + it('should bind accountSwitched action to SavedAccounts props.accountSwitched', () => { + const actionsSpy = sinon.spy(savedAccounts, 'accountSwitched'); + wrapper.find('SavedAccounts').props().accountSwitched({}); + expect(actionsSpy).to.be.calledWith(); + actionsSpy.restore(); + }); +}); + diff --git a/src/components/savedAccounts/savedAccounts.css b/src/components/savedAccounts/savedAccounts.css new file mode 100644 index 000000000..68c2a3cd4 --- /dev/null +++ b/src/components/savedAccounts/savedAccounts.css @@ -0,0 +1,13 @@ +.tableWrapper { + margin: -24px -24px 24px -24px; +} + +.iconCell { + width: 24px; + text-align: right; +} + +.isActive { + background-color: #d6f0ff !important; + font-weight: bold; +} diff --git a/src/components/savedAccounts/savedAccounts.js b/src/components/savedAccounts/savedAccounts.js new file mode 100644 index 000000000..397649521 --- /dev/null +++ b/src/components/savedAccounts/savedAccounts.js @@ -0,0 +1,90 @@ +import { Table, TableHead, TableRow, TableCell } from 'react-toolbox/lib/table'; +import React from 'react'; +import { IconButton } from 'react-toolbox/lib/button'; +import ActionBar from '../actionBar'; +import InfoParagraph from '../infoParagraph'; +import networks from '../../constants/networks'; +import getNetwork from '../../utils/getNetwork'; +import { extractAddress } from '../../utils/api/account'; + +import styles from './savedAccounts.css'; + +const SavedAccounts = ({ + networkOptions, + publicKey, + closeDialog, + accountSaved, + accountRemoved, + accountSwitched, + savedAccounts, + t, +}) => { + const save = () => { + accountSaved({ + network: networkOptions.code, + address: networkOptions.address, + publicKey, + }); + }; + + const isActive = account => ( + account.publicKey === publicKey && + account.network === networkOptions.code); + + return ( +
+ { savedAccounts.length === 0 ? + + {t('This will save public key of your account on this device, so next time it will launch without the need to log in. However, you will be prompted to enter the passphrase once you want to do any transaction.')} + : +
+ + + {t('Switch')} + {t('Address')} + {t('Network')} + {t('Forget')} + + {savedAccounts.map(account => ( + + + + + + {extractAddress(account.publicKey)} + + + {account.network === networks.customNode.code ? + account.address : + t(getNetwork(account.network).name)} + + + + + + ))} +
+
+ } + +
+ ); +}; + +export default SavedAccounts; diff --git a/src/components/savedAccounts/savedAccounts.test.js b/src/components/savedAccounts/savedAccounts.test.js new file mode 100644 index 000000000..30ee7968f --- /dev/null +++ b/src/components/savedAccounts/savedAccounts.test.js @@ -0,0 +1,92 @@ +import React from 'react'; +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import { spy } from 'sinon'; +import configureStore from 'redux-mock-store'; +import PropTypes from 'prop-types'; +import i18n from '../../i18n'; +import SavedAccounts from './savedAccounts'; + +const fakeStore = configureStore(); + +describe('SavedAccounts', () => { + let wrapper; + let closeDialogSpy; + let accountSavedSpy; + const publicKey = 'fab9d261ea050b9e326d7e11587eccc343a20e64e29d8781b50fd06683cacc88'; + const savedAccounts = [ + { + publicKey: 'hab9d261ea050b9e326d7e11587eccc343a20e64e29d8781b50fd06683cacc88', + network: 0, + }, + { + network: 2, + publicKey, + address: 'http://localhost:4000', + }, + { + network: 0, + publicKey, + }, + ]; + + const props = { + closeDialog: () => {}, + accountSaved: () => {}, + accountRemoved: () => {}, + accountSwitched: () => {}, + networkOptions: { + code: 0, + }, + publicKey, + savedAccounts: [], + t: key => key, + }; + + beforeEach(() => { + closeDialogSpy = spy(props, 'closeDialog'); + accountSavedSpy = spy(props, 'accountSaved'); + const store = fakeStore({ + account: { + balance: 100e8, + }, + }); + wrapper = mount(, { + context: { store, i18n }, + childContextTypes: { + store: PropTypes.object.isRequired, + i18n: PropTypes.object.isRequired, + }, + }); + }); + + afterEach(() => { + closeDialogSpy.restore(); + accountSavedSpy.restore(); + }); + + it.skip('should render ActionBar', () => { + expect(wrapper.find('ActionBar')).to.have.lengthOf(1); + }); + + it('should call props.accountSaved on "save button" click', () => { + wrapper.find('button.add-active-account-button').simulate('click'); + const componentProps = wrapper.find(SavedAccounts).props(); + expect(componentProps.accountSaved).to.have.been.calledWith(); + }); + + it('should render InfoParagraph', () => { + wrapper.find('button.add-active-account-button').simulate('click'); + expect(wrapper.find('InfoParagraph')).to.have.lengthOf(1); + }); + + it('should render savedAccounts.length table rows', () => { + wrapper.find('button.add-active-account-button').simulate('click'); + wrapper.setProps({ + ...props, + savedAccounts, + }); + expect(wrapper.find('TableRow')).to.have.lengthOf(savedAccounts.length); + }); +}); + diff --git a/src/constants/actions.js b/src/constants/actions.js index 23f2dc9a9..f535562f8 100644 --- a/src/constants/actions.js +++ b/src/constants/actions.js @@ -32,6 +32,7 @@ const actionTypes = { accountsRetrieved: 'ACCOUNTS_RETRIEVED', accountSaved: 'ACCOUNT_SAVED', accountRemoved: 'ACCOUNT_REMOVED', + accountSwitched: 'ACCOUNT_SWITCHED', }; export default actionTypes; diff --git a/src/store/middlewares/savedAccounts.js b/src/store/middlewares/savedAccounts.js index 5144136d9..565372b0e 100644 --- a/src/store/middlewares/savedAccounts.js +++ b/src/store/middlewares/savedAccounts.js @@ -1,6 +1,9 @@ import i18next from 'i18next'; import actionTypes from '../../constants/actions'; import { successToastDisplayed } from '../../actions/toaster'; +import { accountLoggedOut } from '../../actions/account'; +import { activePeerSet } from '../../actions/peers'; +import getNetwork from '../../utils/getNetwork'; const savedAccountsMiddleware = store => next => (action) => { next(action); @@ -11,6 +14,13 @@ const savedAccountsMiddleware = store => next => (action) => { case actionTypes.accountRemoved: store.dispatch(successToastDisplayed({ label: i18next.t('Account was successfully forgotten.') })); break; + case actionTypes.accountSwitched: + store.dispatch(accountLoggedOut()); + store.dispatch(activePeerSet({ + publicKey: action.data.publicKey, + network: getNetwork(action.data.network), + })); + break; default: break; } diff --git a/src/store/middlewares/savedAccounts.test.js b/src/store/middlewares/savedAccounts.test.js new file mode 100644 index 000000000..51a0ca2e1 --- /dev/null +++ b/src/store/middlewares/savedAccounts.test.js @@ -0,0 +1,60 @@ +import { expect } from 'chai'; +import { spy, mock } from 'sinon'; + +import { accountLoggedOut } from '../../actions/account'; +import { successToastDisplayed } from '../../actions/toaster'; +import actionTypes from '../../constants/actions'; +import middleware from './savedAccounts'; + +describe('SavedAccounts middleware', () => { + let store; + let next; + + beforeEach(() => { + store = mock(); + store.dispatch = spy(); + store.getState = () => ({}); + + next = spy(); + }); + + it('should pass the action to next middleware', () => { + const randomAction = { + type: 'SOME_ACTION', + data: { something: true }, + }; + + middleware(store)(next)(randomAction); + expect(next).to.have.been.calledWith(randomAction); + }); + + it(`should dispatch successToastDisplayed action on ${actionTypes.accountSaved} action`, () => { + const action = { + type: actionTypes.accountSaved, + data: {}, + }; + middleware(store)(next)(action); + expect(store.dispatch).to.have.been.calledWith(successToastDisplayed({ label: 'Account saved' })); + }); + + it(`should dispatch successToastDisplayed action on ${actionTypes.accountRemoved} action`, () => { + const action = { + type: actionTypes.accountRemoved, + data: {}, + }; + middleware(store)(next)(action); + expect(store.dispatch).to.have.been.calledWith(successToastDisplayed({ label: 'Account was successfully forgotten.' })); + }); + + it(`should dispatch accountLoggedOut action on ${actionTypes.accountSwitched} action`, () => { + const action = { + type: actionTypes.accountSwitched, + data: { + publicKey: '', + network: 0, + }, + }; + middleware(store)(next)(action); + expect(store.dispatch).to.have.been.calledWith(accountLoggedOut()); + }); +}); diff --git a/src/store/reducers/peers.js b/src/store/reducers/peers.js index 108647fd8..10f45ec51 100644 --- a/src/store/reducers/peers.js +++ b/src/store/reducers/peers.js @@ -8,14 +8,18 @@ import actionTypes from '../../constants/actions'; * * @returns {Object} - Next state object */ -const peers = (state = { status: {} }, action) => { +const peers = (state = { status: {}, options: {} }, action) => { switch (action.type) { case actionTypes.activePeerSet: - return Object.assign({}, state, { data: action.data }); + return Object.assign({}, state, { + data: action.data, + // options are duplicated here because lisk-js later on removes it from the 'data' object + options: action.data.options, + }); case actionTypes.activePeerUpdate: return Object.assign({}, state, { status: action.data }); case actionTypes.accountLoggedOut: - return Object.assign({}, state, { data: {}, status: {} }); + return Object.assign({}, state, { data: {}, status: {}, options: {} }); default: return state; } diff --git a/src/store/reducers/peers.test.js b/src/store/reducers/peers.test.js index b32505d2d..a4ef9f759 100644 --- a/src/store/reducers/peers.test.js +++ b/src/store/reducers/peers.test.js @@ -17,7 +17,7 @@ describe('Reducer: peers(state, action)', () => { }, }; - const newState = { data: action.data }; + const newState = { data: action.data, options: action.data.options }; const changedState = peers(state, action); expect(changedState).to.deep.equal(newState); }); @@ -49,7 +49,7 @@ describe('Reducer: peers(state, action)', () => { type: actionTypes.accountLoggedOut, }; - const newState = { status: {}, data: {} }; + const newState = { status: {}, data: {}, options: {} }; const changedState = peers(state, action); expect(changedState).to.deep.equal(newState); }); diff --git a/src/store/reducers/savedAccounts.js b/src/store/reducers/savedAccounts.js index b5948a638..01f39a32e 100644 --- a/src/store/reducers/savedAccounts.js +++ b/src/store/reducers/savedAccounts.js @@ -5,17 +5,25 @@ import actionTypes from '../../constants/actions'; * @param {Array} state * @param {Object} action */ -const savedAccounts = (state = [], action) => { +const savedAccounts = (state = { accounts: [] }, action) => { switch (action.type) { case actionTypes.accountsRetrieved: return action.data; case actionTypes.accountSaved: - return [ + return { ...state, - action.data, - ]; + accounts: [ + ...state.accounts, + action.data, + ], + }; case actionTypes.accountRemoved: - return state.filter(account => account.publicKey !== action.data); + return { + ...state, + accounts: state.accounts.filter(account => + !(account.publicKey === action.data.publicKey && + account.network === action.data.network)), + }; default: return state; } diff --git a/src/store/reducers/savedAccounts.test.js b/src/store/reducers/savedAccounts.test.js index ea8de5afe..436394691 100644 --- a/src/store/reducers/savedAccounts.test.js +++ b/src/store/reducers/savedAccounts.test.js @@ -13,36 +13,38 @@ describe('Reducer: savedAccounts(state, action)', () => { network: 'Custom node', address: 'http://localhost:4000', }; + it('should return action.data if action.type = actionTypes.accountsRetrieved', () => { - const state = []; + const state = { accounts: [] }; const action = { type: actionTypes.accountsRetrieved, - data: [account, account2], + data: { + accounts: [account, account2], + lastActive: account2, + }, }; - const expectedSate = [account, account2]; const changedState = savedAccounts(state, action); - expect(changedState).to.deep.equal(expectedSate); + expect(changedState).to.deep.equal(action.data); }); it('should return action.data with address if action.type = actionTypes.accountSaved', () => { - const state = []; + const state = { accounts: [] }; const action = { type: actionTypes.accountSaved, data: account, }; const changedState = savedAccounts(state, action); - expect(changedState).to.deep.equal([action.data]); + expect(changedState).to.deep.equal({ accounts: [action.data] }); }); it('should return array without given account if action.type = actionTypes.accountRemoved', () => { - const publicKey = 'sample_key_1'; - const state = [account]; + const state = { accounts: [account, account2] }; const action = { type: actionTypes.accountRemoved, - data: publicKey, + data: account, }; const changedState = savedAccounts(state, action); - expect(changedState).to.deep.equal([]); + expect(changedState).to.deep.equal({ accounts: [account2] }); }); }); diff --git a/src/utils/saveAccount.js b/src/utils/saveAccount.js deleted file mode 100644 index b0149e465..000000000 --- a/src/utils/saveAccount.js +++ /dev/null @@ -1,24 +0,0 @@ -export const getSavedAccount = () => { - const savedAccounts = localStorage.getItem('accounts'); - let account; - if (savedAccounts) { - account = JSON.parse(savedAccounts); - } - - return account; -}; - -export const setSavedAccount = ({ publicKey, network, address }) => { - localStorage.setItem('accounts', JSON.stringify([{ - publicKey, - network, - address, - }])); -}; - -export const removeSavedAccount = (publicKey) => { - let accounts = localStorage.getItem('accounts'); - accounts = JSON.parse(accounts); - accounts = accounts.filter(account => account.publicKey !== publicKey); - localStorage.setItem('accounts', JSON.stringify(accounts)); -}; diff --git a/src/utils/savedAccounts.js b/src/utils/savedAccounts.js new file mode 100644 index 000000000..68e011022 --- /dev/null +++ b/src/utils/savedAccounts.js @@ -0,0 +1,58 @@ +import { validateUrl } from './login'; +import { extractAddress } from './api/account'; + +const isValidSavedAccount = ({ publicKey, network, address }) => { + try { + return extractAddress(publicKey) && + network >= 0 && network <= 2 && + (validateUrl(address).addressValidity === '' || network !== 2); + } catch (e) { + return false; + } +}; + +export const getSavedAccounts = () => { + try { + return JSON.parse(localStorage.getItem('accounts')).filter(isValidSavedAccount); + } catch (e) { + return []; + } +}; + +export const getLastActiveAccount = () => ( + getSavedAccounts()[localStorage.getItem('lastActiveAccountIndex') || 0] +); + +export const setLastActiveAccount = ({ publicKey, network, address }) => { + const lastActiveAccountIndex = getSavedAccounts().findIndex(account => ( + account.publicKey === publicKey && + account.network === network && + account.address === address + )); + if (lastActiveAccountIndex > -1) { + localStorage.setItem('lastActiveAccountIndex', lastActiveAccountIndex); + } + return lastActiveAccountIndex; +}; + +export const setSavedAccount = ({ publicKey, network, address }) => { + const savedAccounts = [ + ...getSavedAccounts(), + { + publicKey, + network, + address, + }, + ]; + localStorage.setItem('accounts', JSON.stringify(savedAccounts)); + return savedAccounts; +}; + +export const removeSavedAccount = ({ publicKey, network, address }) => { + const accounts = getSavedAccounts().filter(account => + !(account.publicKey === publicKey && + account.network === network && + account.address === address)); + localStorage.setItem('accounts', JSON.stringify(accounts)); + return accounts; +}; diff --git a/src/utils/savedAccounts.test.js b/src/utils/savedAccounts.test.js new file mode 100644 index 000000000..ab404fe9a --- /dev/null +++ b/src/utils/savedAccounts.test.js @@ -0,0 +1,111 @@ +import { expect } from 'chai'; +import { mock } from 'sinon'; +import { + getSavedAccounts, + getLastActiveAccount, + setLastActiveAccount, + setSavedAccount, + removeSavedAccount, +} from './savedAccounts'; + +describe('savedAccounts', () => { + let localStorageMock; + const publicKey = 'fab9d261ea050b9e326d7e11587eccc343a20e64e29d8781b50fd06683cacc88'; + const accounts = [ + { + publicKey: 'hab9d261ea050b9e326d7e11587eccc343a20e64e29d8781b50fd06683cacc88', + network: 0, + }, + { + publicKey, + network: 0, + }, + { + publicKey, + network: 2, + address: 'http://localhost:4000', + }, + ]; + + beforeEach(() => { + localStorageMock = mock(localStorage); + }); + + afterEach(() => { + localStorageMock.restore(); + }); + + describe('getSavedAccounts', () => { + it('returns [] if if localStorage.getItem(\'accounts\') returns undefined', () => { + localStorageMock.expects('getItem').withExactArgs('accounts').returns(undefined); + expect(getSavedAccounts()).to.deep.equal([]); + }); + + it('returns [] if if localStorage.getItem(\'accounts\') returns invalid JSON string', () => { + localStorageMock.expects('getItem').withExactArgs('accounts').returns('{]'); + expect(getSavedAccounts()).to.deep.equal([]); + }); + + + it('returns [] if if localStorage.getItem(\'accounts\') returns JSON encoded array with invalid data', () => { + const invalidAccounts = [ + { + publicKey: 'invalid', + }, + ]; + localStorageMock.expects('getItem').withExactArgs('accounts').returns(JSON.stringify(invalidAccounts)); + expect(getSavedAccounts()).to.deep.equal([]); + }); + + it('returns array parsed from json in localStorage.getItem(\'accounts\')', () => { + localStorageMock.expects('getItem').withExactArgs('accounts').returns(JSON.stringify(accounts)); + expect(getSavedAccounts()).to.deep.equal(accounts); + }); + }); + + describe('getLastActiveAccount', () => { + it('returns first account if localStorage.getItem(\'lastActiveAccountIndex\') returns undefined', () => { + localStorageMock.expects('getItem').withExactArgs('accounts').returns(JSON.stringify(accounts)); + localStorageMock.expects('getItem').withExactArgs('lastActiveAccountIndex').returns(undefined); + expect(getLastActiveAccount()).to.deep.equal(accounts[0]); + }); + + it('returns nth account if localStorage.getItem(\'lastActiveAccountIndex\') returns n', () => { + const n = 2; + localStorageMock.expects('getItem').withExactArgs('accounts').returns(JSON.stringify(accounts)); + localStorageMock.expects('getItem').withExactArgs('lastActiveAccountIndex').returns(n); + expect(getLastActiveAccount()).to.deep.equal(accounts[n]); + }); + }); + + describe('setLastActiveAccount', () => { + it('sets nothing in localStorage if passed account is not in localStorageMock.accounts and returns -1', () => { + localStorageMock.expects('getItem').withExactArgs('accounts').returns(JSON.stringify(accounts.slice(0, 1))); + expect(setLastActiveAccount(accounts[2])).to.equal(-1); + }); + + it('sets index of passed account in localStorage.acocunts into localStorage and returns n', () => { + const n = 2; + localStorageMock.expects('getItem').withExactArgs('accounts').returns(JSON.stringify(accounts)); + localStorageMock.expects('setItem').withExactArgs('lastActiveAccountIndex', n); + expect(setLastActiveAccount(accounts[n])).to.equal(n); + }); + }); + + describe('setSavedAccount', () => { + it('sets accounts in localStorage with appended passed account and also returns it', () => { + localStorageMock.expects('getItem').withExactArgs('accounts').returns(JSON.stringify(accounts.slice(0, 2))); + localStorageMock.expects('setItem').withExactArgs('accounts', JSON.stringify(accounts)); + expect(setSavedAccount(accounts[2])).to.deep.equal(accounts); + }); + }); + + describe('removeSavedAccount', () => { + it('sets accounts in localStorage with removed passed account and also returns it', () => { + localStorageMock.expects('getItem').withExactArgs('accounts').returns(JSON.stringify(accounts)); + localStorageMock.expects('setItem').withExactArgs('accounts', JSON.stringify(accounts.slice(0, 2))); + expect(removeSavedAccount(accounts[2])).to.deep.equal(accounts.slice(0, 2)); + }); + }); +}); +