diff --git a/js/src/views/ParityBar/accountStore.js b/js/src/views/ParityBar/accountStore.js
new file mode 100644
index 00000000000..b53f40dd284
--- /dev/null
+++ b/js/src/views/ParityBar/accountStore.js
@@ -0,0 +1,101 @@
+// Copyright 2015-2017 Parity Technologies (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see .
+
+import { action, observable, transaction } from 'mobx';
+
+export default class AccountStore {
+ @observable accounts = [];
+ @observable defaultAccount = null;
+ @observable isLoading = false;
+
+ constructor (api) {
+ this._api = api;
+
+ this.loadAccounts();
+ this.subscribeDefaultAccount();
+ }
+
+ @action setAccounts = (accounts) => {
+ this.accounts = accounts;
+ }
+
+ @action setDefaultAccount = (defaultAccount) => {
+ this.defaultAccount = defaultAccount;
+ }
+
+ @action setLoading = (isLoading) => {
+ this.isLoading = isLoading;
+ }
+
+ makeDefaultAccount = (address) => {
+ const accounts = [address].concat(
+ this.accounts
+ .filter((account) => account.address !== address)
+ .map((account) => account.address)
+ );
+
+ return this._api.parity
+ .setNewDappsWhitelist(accounts)
+ .catch((error) => {
+ console.warn('makeDefaultAccount', error);
+ });
+ }
+
+ loadAccounts () {
+ this.setLoading(true);
+
+ return Promise
+ .all([
+ this._api.parity.getNewDappsWhitelist(),
+ this._api.parity.allAccountsInfo()
+ ])
+ .then(([whitelist, accounts]) => {
+ transaction(() => {
+ this.setLoading(false);
+ this.setAccounts(
+ Object
+ .keys(accounts)
+ .filter((address) => {
+ const isAccount = accounts[address].uuid;
+ const isWhitelisted = !whitelist || whitelist.includes(address);
+
+ return isAccount && isWhitelisted;
+ })
+ .map((address) => {
+ const account = accounts[address];
+
+ account.address = address;
+ account.default = address === this.defaultAccount;
+
+ return account;
+ })
+ );
+ });
+ })
+ .catch((error) => {
+ this.setLoading(false);
+ console.warn('loadAccounts', error);
+ });
+ }
+
+ subscribeDefaultAccount () {
+ return this._api.subscribe('parity_defaultAccount', (error, defaultAccount) => {
+ if (!error) {
+ this.setDefaultAccount(defaultAccount);
+ }
+ });
+ }
+}
diff --git a/js/src/views/ParityBar/accountStore.spec.js b/js/src/views/ParityBar/accountStore.spec.js
new file mode 100644
index 00000000000..6dd21980630
--- /dev/null
+++ b/js/src/views/ParityBar/accountStore.spec.js
@@ -0,0 +1,104 @@
+// Copyright 2015-2017 Parity Technologies (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see .
+
+import sinon from 'sinon';
+
+import AccountStore from './accountStore';
+
+import { ACCOUNT_DEFAULT, ACCOUNT_FIRST, ACCOUNT_NEW, createApi } from './parityBar.test.js';
+
+let api;
+let store;
+
+function create () {
+ api = createApi();
+ store = new AccountStore(api);
+
+ return store;
+}
+
+describe('views/ParityBar/AccountStore', () => {
+ beforeEach(() => {
+ create();
+ });
+
+ describe('constructor', () => {
+ it('subscribes to defaultAccount', () => {
+ expect(api.subscribe).to.have.been.calledWith('parity_defaultAccount');
+ });
+ });
+
+ describe('@action', () => {
+ describe('setAccounts', () => {
+ it('sets the accounts', () => {
+ store.setAccounts('testing');
+ expect(store.accounts).to.equal('testing');
+ });
+ });
+
+ describe('setDefaultAccount', () => {
+ it('sets the default account', () => {
+ store.setDefaultAccount('testing');
+ expect(store.defaultAccount).to.equal('testing');
+ });
+ });
+
+ describe('setLoading', () => {
+ it('sets the loading status', () => {
+ store.setLoading('testing');
+ expect(store.isLoading).to.equal('testing');
+ });
+ });
+ });
+
+ describe('operations', () => {
+ describe('loadAccounts', () => {
+ beforeEach(() => {
+ sinon.spy(store, 'setAccounts');
+
+ return store.loadAccounts();
+ });
+
+ afterEach(() => {
+ store.setAccounts.restore();
+ });
+
+ it('calls into parity_getNewDappsWhitelist', () => {
+ expect(api.parity.getNewDappsWhitelist).to.have.been.called;
+ });
+
+ it('calls into parity_allAccountsInfo', () => {
+ expect(api.parity.allAccountsInfo).to.have.been.called;
+ });
+
+ it('sets the accounts', () => {
+ expect(store.setAccounts).to.have.been.called;
+ });
+ });
+
+ describe('makeDefaultAccount', () => {
+ beforeEach(() => {
+ return store.makeDefaultAccount(ACCOUNT_NEW);
+ });
+
+ it('calls into parity_setNewDappsWhitelist (with ordering)', () => {
+ expect(api.parity.setNewDappsWhitelist).to.have.been.calledWith([
+ ACCOUNT_NEW, ACCOUNT_FIRST, ACCOUNT_DEFAULT
+ ]);
+ });
+ });
+ });
+});
diff --git a/js/src/views/ParityBar/parityBar.css b/js/src/views/ParityBar/parityBar.css
index a955bd6870f..1d37cb17709 100644
--- a/js/src/views/ParityBar/parityBar.css
+++ b/js/src/views/ParityBar/parityBar.css
@@ -1,4 +1,4 @@
-/* Copyright 2015, 2016 Parity Technologies (UK) Ltd.
+/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
@@ -15,13 +15,42 @@
/* along with Parity. If not, see .
*/
-.bar, .expanded {
- position: fixed;
- bottom: 0;
- right: 0;
- font-size: 16px;
- font-family: 'Roboto', sans-serif;
- z-index: 10001;
+.account {
+ display: flex;
+ flex: 1;
+ position: relative;
+
+ .accountOverlay {
+ position: absolute;
+ right: 0.5em;
+ top: 0.5em;
+ }
+
+ .iconDisabled {
+ opacity: 0.15;
+ }
+
+ .selected,
+ .unselected {
+ margin: 0.125em 0;
+
+ &:focus {
+ outline: none;
+ }
+ }
+
+ .unselected {
+ background: rgba(0, 0, 0, 0.4) !important;
+ }
+
+ .selected {
+ background: rgba(255, 255, 255, 0.35) !important;
+ }
+}
+
+.container {
+ display: flex;
+ flex-direction: column;
}
.overlay {
@@ -32,6 +61,16 @@
left: 0;
background: rgba(255, 255, 255, 0.5);
z-index: 10000;
+ user-select: none;
+}
+
+.bar,
+.expanded {
+ position: fixed;
+ font-size: 16px;
+ font-family: 'Roboto', sans-serif;
+ z-index: 10001;
+ user-select: none;
}
.bar {
@@ -39,35 +78,57 @@
display: flex;
flex-wrap: wrap;
width: 100%;
+ top: 0;
+ left: 0;
+
+ &.moving {
+ bottom: 0;
+ right: 0;
+
+ &:hover {
+ cursor: move;
+ }
+ }
+}
+
+.parityBg {
+ position: fixed;
+
+ transition-property: left, top, right, bottom;
+ transition-duration: 0.25s;
+ transition-timing-function: ease;
+
+ &.moving {
+ transition-duration: 0.05s;
+ transition-timing-function: ease-in-out;
+ }
}
.expanded {
- right: 1em;
border-radius: 4px 4px 0 0;
display: flex;
flex-direction: column;
max-height: 50vh;
-}
-.expanded .content {
- flex: 1;
- overflow-y: auto;
- overflow-x: hidden;
- display: flex;
- background: rgba(0, 0, 0, 0.8);
- min-height: 16em;
+ .content {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ display: flex;
+ background: rgba(0, 0, 0, 0.8);
+ min-height: 16em;
+ }
}
.corner {
- position: absolute;
- bottom: 0;
- right: 1em;
border-radius: 4px 4px 0 0;
}
.cornercolor {
background: rgba(0, 0, 0, 0.5);
padding: 0.5em 1em;
+ display: flex;
+ align-items: center;
}
.link {
@@ -76,19 +137,21 @@
outline: none !important;
color: white !important;
display: inline-block;
-}
-.link img, .link svg {
- height: 24px !important;
- width: 24px !important;
- margin: 2px 0.5em 0 0;
+ img, svg {
+ height: 24px !important;
+ width: 24px !important;
+ margin: 2px 0.5em 0 0;
+ }
}
.link+.link {
margin-left: 1em;
}
-.button, .parityButton {
+.button,
+.iconButton,
+.parityButton {
overflow: visible !important;
}
@@ -101,6 +164,14 @@
fill: white !important;
}
+.iconButton {
+ min-width: 2em !important;
+
+ img {
+ margin: 6px 0.5em 0 0.5em;
+ }
+}
+
.label {
position: relative;
display: inline-block;
@@ -123,20 +194,21 @@
padding: 0.5em 1em;
background: rgba(0, 0, 0, 0.25);
margin-bottom: 0;
-}
-.header:after {
- clear: both;
+ &:after {
+ clear: both;
+ }
}
-.header button,
-.corner button {
- color: white !important;
-}
+.header,
+.corner {
+ button {
+ color: white !important;
+ }
-.header svg,
-.coner svg {
- fill: white !important;
+ svg {
+ fill: white !important;
+ }
}
.body {
@@ -150,17 +222,49 @@
.actions {
float: right;
margin-top: -2px;
-}
-.actions div {
- margin-left: 1em;
- display: inline-block;
- cursor: pointer;
+ div {
+ margin-left: 1em;
+ display: inline-block;
+ cursor: pointer;
+ }
}
-.parityIcon, .signerIcon {
+.parityIcon,
+.signerIcon {
width: 24px;
height: 24px;
vertical-align: middle;
margin-left: 12px;
}
+
+.moveIcon {
+ display: flex;
+ align-items: center;
+
+ &:hover {
+ cursor: move;
+ }
+}
+
+.dragButton {
+ width: 1em;
+ height: 1em;
+ margin-left: 0.5em;
+
+ background-color: white;
+ opacity: 0.25;
+ border-radius: 50%;
+
+ transition-property: opacity;
+ transition-duration: 0.1s;
+ transition-timing-function: ease-in-out;
+
+ &:hover {
+ opacity: 0.5;
+ }
+
+ &.moving {
+ opacity: 0.75;
+ }
+}
diff --git a/js/src/views/ParityBar/parityBar.js b/js/src/views/ParityBar/parityBar.js
index 0ecb57ae791..63036b9e44a 100644
--- a/js/src/views/ParityBar/parityBar.js
+++ b/js/src/views/ParityBar/parityBar.js
@@ -1,4 +1,4 @@
-// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
+// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@@ -14,19 +14,38 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
+import { throttle } from 'lodash';
+import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
+import ReactDOM from 'react-dom';
+import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { connect } from 'react-redux';
-import ActionFingerprint from 'material-ui/svg-icons/action/fingerprint';
-import ContentClear from 'material-ui/svg-icons/content/clear';
+import store from 'store';
-import { Badge, Button, ContainerTitle, ParityBackground } from '~/ui';
-import { Embedded as Signer } from '../Signer';
+import imagesEthcoreBlock from '~/../assets/images/parity-logo-white-no-text.svg';
+import { AccountCard, Badge, Button, ContainerTitle, IdentityIcon, ParityBackground, SectionList } from '~/ui';
+import { CancelIcon, FingerprintIcon } from '~/ui/Icons';
+import DappsStore from '~/views/Dapps/dappsStore';
+import { Embedded as Signer } from '~/views/Signer';
-import imagesEthcoreBlock from '!url-loader!../../../assets/images/parity-logo-white-no-text.svg';
+import AccountStore from './accountStore';
import styles from './parityBar.css';
+const LS_STORE_KEY = '_parity::parityBar';
+const DEFAULT_POSITION = { right: '1em', bottom: 0 };
+const DISPLAY_ACCOUNTS = 'accounts';
+const DISPLAY_SIGNER = 'signer';
+
+@observer
class ParityBar extends Component {
+ app = null;
+ measures = null;
+ moving = false;
+
+ static contextTypes = {
+ api: PropTypes.object.isRequired
+ };
static propTypes = {
dapp: PropTypes.bool,
@@ -35,7 +54,33 @@ class ParityBar extends Component {
};
state = {
- opened: false
+ displayType: DISPLAY_SIGNER,
+ moving: false,
+ opened: false,
+ position: DEFAULT_POSITION
+ };
+
+ constructor (props) {
+ super(props);
+
+ this.debouncedMouseMove = throttle(
+ this._onMouseMove,
+ 40,
+ { leading: true, trailing: true }
+ );
+ }
+
+ componentWillMount () {
+ const { api } = this.context;
+
+ this.accountStore = new AccountStore(api);
+
+ // Hook to the dapp loaded event to position the
+ // Parity Bar accordingly
+ DappsStore.get(api).on('loaded', (app) => {
+ this.app = app;
+ this.loadPosition();
+ });
}
componentWillReceiveProps (nextProps) {
@@ -47,14 +92,14 @@ class ParityBar extends Component {
}
if (count < newCount) {
- this.setOpened(true);
+ this.setOpened(true, DISPLAY_SIGNER);
} else if (newCount === 0 && count === 1) {
this.setOpened(false);
}
}
- setOpened (opened) {
- this.setState({ opened });
+ setOpened (opened, displayType = DISPLAY_SIGNER) {
+ this.setState({ displayType, opened });
if (!this.bar) {
return;
@@ -74,11 +119,71 @@ class ParityBar extends Component {
}
render () {
- const { opened } = this.state;
+ const { moving, opened, position } = this.state;
+
+ const containerClassNames = opened
+ ? [ styles.overlay ]
+ : [ styles.bar ];
+
+ if (!opened && moving) {
+ containerClassNames.push(styles.moving);
+ }
+
+ const parityBgClassNames = [
+ opened
+ ? styles.expanded
+ : styles.corner,
+ styles.parityBg
+ ];
+
+ if (moving) {
+ parityBgClassNames.push(styles.moving);
+ }
- return opened
- ? this.renderExpanded()
- : this.renderBar();
+ const parityBgStyle = {
+ ...position
+ };
+
+ // Open the Signer at one of the four corners
+ // of the screen
+ if (opened) {
+ // Set at top or bottom of the screen
+ if (position.top !== undefined) {
+ parityBgStyle.top = 0;
+ } else {
+ parityBgStyle.bottom = 0;
+ }
+
+ // Set at left or right of the screen
+ if (position.left !== undefined) {
+ parityBgStyle.left = '1em';
+ } else {
+ parityBgStyle.right = '1em';
+ }
+ }
+
+ return (
+
+
+ {
+ opened
+ ? this.renderExpanded()
+ : this.renderBar()
+ }
+
+
+ );
}
renderBar () {
@@ -88,36 +193,72 @@ class ParityBar extends Component {
return null;
}
- const parityIcon = (
-
+ return (
+
+
+ }
+ onClick={ this.toggleAccountsDisplay }
+ />
+ {
+ this.renderLink(
+
+ }
+ label={
+ this.renderLabel(
+
+ )
+ }
+ />
+ )
+ }
+ }
+ label={ this.renderSignerLabel() }
+ onClick={ this.toggleSignerDisplay }
+ />
+ { this.renderDrag() }
+
);
+ }
- const parityButton = (
-
- );
+ renderDrag () {
+ if (this.props.externalLink) {
+ return;
+ }
+
+ const dragButtonClasses = [ styles.dragButton ];
+
+ if (this.state.moving) {
+ dragButtonClasses.push(styles.moving);
+ }
return (
-
-
- { this.renderLink(parityButton) }
- }
- label={ this.renderSignerLabel() }
- onClick={ this.toggleDisplay }
- />
-
-
+
);
}
@@ -144,27 +285,81 @@ class ParityBar extends Component {
}
renderExpanded () {
+ const { displayType } = this.state;
+
return (
-
-
-
-
-
-
-
- }
- label='Close'
- onClick={ this.toggleDisplay } />
-
+
+
+
+
+ )
+ : (
+
+ )
+ }
+ />
-
-
+
+ }
+ label={
+
+ }
+ onClick={ this.toggleSignerDisplay }
+ />
-
+
+
+ {
+ displayType === DISPLAY_ACCOUNTS
+ ? (
+
+ )
+ : (
+
+ )
+ }
+
+
+ );
+ }
+
+ renderAccount = (account) => {
+ const onMakeDefault = () => {
+ this.toggleAccountsDisplay();
+ this.accountStore.makeDefaultAccount(account.address);
+ };
+
+ return (
+
);
}
@@ -189,17 +384,271 @@ class ParityBar extends Component {
+ value={ pending.length }
+ />
);
}
- return this.renderLabel('Signer', bubble);
+ return this.renderLabel(
+
,
+ bubble
+ );
+ }
+
+ getHorizontal (x) {
+ const { page, button, container } = this.measures;
+
+ const left = x - button.offset.left;
+ const centerX = left + container.width / 2;
+
+ // left part of the screen
+ if (centerX < page.width / 2) {
+ return { left: Math.max(0, left) };
+ }
+
+ const right = page.width - x - button.offset.right;
+
+ return { right: Math.max(0, right) };
}
- toggleDisplay = () => {
+ getVertical (y) {
+ const STICKY_SIZE = 75;
+ const { page, button, container } = this.measures;
+
+ const top = y - button.offset.top;
+ const centerY = top + container.height / 2;
+
+ // top part of the screen
+ if (centerY < page.height / 2) {
+ // Add Sticky edges
+ const stickyTop = top < STICKY_SIZE
+ ? 0
+ : top;
+
+ return { top: Math.max(0, stickyTop) };
+ }
+
+ const bottom = page.height - y - button.offset.bottom;
+ // Add Sticky edges
+ const stickyBottom = bottom < STICKY_SIZE
+ ? 0
+ : bottom;
+
+ return { bottom: Math.max(0, stickyBottom) };
+ }
+
+ getPosition (x, y) {
+ if (!this.moving || !this.measures) {
+ return {};
+ }
+
+ const horizontal = this.getHorizontal(x);
+ const vertical = this.getVertical(y);
+
+ const position = {
+ ...horizontal,
+ ...vertical
+ };
+
+ return position;
+ }
+
+ onMouseDown = (event) => {
+ const containerElt = ReactDOM.findDOMNode(this.refs.container);
+ const dragButtonElt = ReactDOM.findDOMNode(this.refs.dragButton);
+
+ if (!containerElt || !dragButtonElt) {
+ console.warn(containerElt ? 'drag button' : 'container', 'not found...');
+ return;
+ }
+
+ const bodyRect = document.body.getBoundingClientRect();
+ const containerRect = containerElt.getBoundingClientRect();
+ const buttonRect = dragButtonElt.getBoundingClientRect();
+
+ const buttonOffset = {
+ top: (buttonRect.top + buttonRect.height / 2) - containerRect.top,
+ left: (buttonRect.left + buttonRect.width / 2) - containerRect.left
+ };
+
+ buttonOffset.bottom = containerRect.height - buttonOffset.top;
+ buttonOffset.right = containerRect.width - buttonOffset.left;
+
+ const button = {
+ offset: buttonOffset,
+ height: buttonRect.height,
+ width: buttonRect.width
+ };
+
+ const container = {
+ height: containerRect.height,
+ width: containerRect.width
+ };
+
+ const page = {
+ height: bodyRect.height,
+ width: bodyRect.width
+ };
+
+ this.moving = true;
+ this.measures = {
+ button,
+ container,
+ page
+ };
+
+ this.setState({ moving: true });
+ }
+
+ onMouseEnter = (event) => {
+ if (!this.moving) {
+ return;
+ }
+
+ const { buttons } = event;
+
+ // If no left-click, stop move
+ if (buttons !== 1) {
+ this.onMouseUp(event);
+ }
+ }
+
+ onMouseLeave = (event) => {
+ if (!this.moving) {
+ return;
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ onMouseMove = (event) => {
+ const { pageX, pageY } = event;
+
+ // this._onMouseMove({ pageX, pageY });
+ this.debouncedMouseMove({ pageX, pageY });
+
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ _onMouseMove = (event) => {
+ if (!this.moving) {
+ return;
+ }
+
+ const { pageX, pageY } = event;
+ const position = this.getPosition(pageX, pageY);
+
+ this.setState({ position });
+ }
+
+ onMouseUp = (event) => {
+ if (!this.moving) {
+ return;
+ }
+
+ const { pageX, pageY } = event;
+ const position = this.getPosition(pageX, pageY);
+
+ // Stick to bottom or top
+ if (position.top !== undefined) {
+ position.top = 0;
+ } else {
+ position.bottom = 0;
+ }
+
+ // Stick to bottom or top
+ if (position.left !== undefined) {
+ position.left = '1em';
+ } else {
+ position.right = '1em';
+ }
+
+ this.moving = false;
+ this.setState({ moving: false, position });
+ this.savePosition(position);
+ }
+
+ toggleAccountsDisplay = () => {
const { opened } = this.state;
- this.setOpened(!opened);
+ this.setOpened(!opened, DISPLAY_ACCOUNTS);
+
+ if (!opened) {
+ this.accountStore.loadAccounts();
+ }
+ }
+
+ toggleSignerDisplay = () => {
+ const { opened } = this.state;
+
+ this.setOpened(!opened, DISPLAY_SIGNER);
+ }
+
+ get config () {
+ let config;
+
+ try {
+ config = JSON.parse(store.get(LS_STORE_KEY));
+ } catch (error) {
+ config = {};
+ }
+
+ return config;
+ }
+
+ loadPosition (props = this.props) {
+ const { app, config } = this;
+
+ if (!app) {
+ return this.setState({ position: DEFAULT_POSITION });
+ }
+
+ if (config[app.id]) {
+ return this.setState({ position: config[app.id] });
+ }
+
+ const position = this.stringToPosition(app.position);
+
+ this.setState({ position });
+ }
+
+ savePosition (position) {
+ const { app, config } = this;
+
+ config[app.id] = position;
+
+ store.set(LS_STORE_KEY, JSON.stringify(config));
+ }
+
+ stringToPosition (value) {
+ switch (value) {
+ case 'top-left':
+ return {
+ left: '1em',
+ top: 0
+ };
+
+ case 'top-right':
+ return {
+ right: '1em',
+ top: 0
+ };
+
+ case 'bottom-left':
+ return {
+ bottom: 0,
+ left: '1em'
+ };
+
+ case 'bottom-right':
+ default:
+ return DEFAULT_POSITION;
+ }
}
}
diff --git a/js/src/views/ParityBar/parityBar.spec.js b/js/src/views/ParityBar/parityBar.spec.js
new file mode 100644
index 00000000000..941c47c6508
--- /dev/null
+++ b/js/src/views/ParityBar/parityBar.spec.js
@@ -0,0 +1,167 @@
+// Copyright 2015-2017 Parity Technologies (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see
.
+
+import { shallow } from 'enzyme';
+import React from 'react';
+import sinon from 'sinon';
+
+import ParityBar from './';
+
+import { createApi } from './parityBar.test.js';
+
+let api;
+let component;
+let instance;
+let store;
+
+function createRedux (state = {}) {
+ store = {
+ dispatch: sinon.stub(),
+ subscribe: sinon.stub(),
+ getState: () => Object.assign({ signer: { pending: [] } }, state)
+ };
+
+ return store;
+}
+
+function render (props = {}, state = {}) {
+ api = createApi();
+ component = shallow(
+
,
+ {
+ context: {
+ store: createRedux(state)
+ }
+ }
+ ).find('ParityBar').shallow({ context: { api } });
+ instance = component.instance();
+
+ return component;
+}
+
+describe('views/ParityBar', () => {
+ beforeEach(() => {
+ render({ dapp: true });
+ });
+
+ it('renders defaults', () => {
+ expect(component).to.be.ok;
+ });
+
+ it('includes the ParityBackground', () => {
+ expect(component.find('Connect(ParityBackground)')).to.have.length(1);
+ });
+
+ describe('renderBar', () => {
+ let bar;
+
+ beforeEach(() => {
+ bar = shallow(instance.renderBar());
+ });
+
+ it('renders nothing when not overlaying a dapp', () => {
+ render({ dapp: false });
+ expect(instance.renderBar()).to.be.null;
+ });
+
+ it('renders when overlaying a dapp', () => {
+ expect(bar.find('div')).not.to.have.length(0);
+ });
+
+ it('renders the Account selector button', () => {
+ const icon = bar.find('Button').first().props().icon;
+
+ expect(icon.type.displayName).to.equal('Connect(IdentityIcon)');
+ });
+
+ it('renders the Parity button', () => {
+ const label = shallow(bar.find('Button').at(1).props().label);
+
+ expect(label.find('FormattedMessage').props().id).to.equal('parityBar.label.parity');
+ });
+
+ it('renders the Signer button', () => {
+ const label = shallow(bar.find('Button').last().props().label);
+
+ expect(label.find('FormattedMessage').props().id).to.equal('parityBar.label.signer');
+ });
+ });
+
+ describe('renderExpanded', () => {
+ let expanded;
+
+ beforeEach(() => {
+ expanded = shallow(instance.renderExpanded());
+ });
+
+ it('includes the Signer', () => {
+ expect(expanded.find('Connect(Embedded)')).to.have.length(1);
+ });
+ });
+
+ describe('renderLabel', () => {
+ it('renders the label name', () => {
+ expect(shallow(instance.renderLabel('testing', null)).text()).to.equal('testing');
+ });
+
+ it('renders name and bubble', () => {
+ expect(shallow(instance.renderLabel('testing', '(bubble)')).text()).to.equal('testing(bubble)');
+ });
+ });
+
+ describe('renderSignerLabel', () => {
+ let label;
+
+ beforeEach(() => {
+ label = shallow(instance.renderSignerLabel());
+ });
+
+ it('renders the signer label', () => {
+ expect(label.find('FormattedMessage').props().id).to.equal('parityBar.label.signer');
+ });
+
+ it('does not render a badge when no pending requests', () => {
+ expect(label.find('Badge')).to.have.length(0);
+ });
+
+ it('renders a badge when pending requests', () => {
+ render({}, { signer: { pending: ['123', '456'] } });
+ expect(shallow(instance.renderSignerLabel()).find('Badge').props().value).to.equal(2);
+ });
+ });
+
+ describe('opened state', () => {
+ beforeEach(() => {
+ sinon.spy(instance, 'renderBar');
+ sinon.spy(instance, 'renderExpanded');
+ });
+
+ afterEach(() => {
+ instance.renderBar.restore();
+ instance.renderExpanded.restore();
+ });
+
+ it('renders the bar on with opened === false', () => {
+ expect(component.find('Link[to="/apps"]')).to.have.length(1);
+ });
+
+ it('renders expanded with opened === true', () => {
+ expect(instance.renderExpanded).not.to.have.been.called;
+ instance.setState({ opened: true });
+ expect(instance.renderExpanded).to.have.been.called;
+ });
+ });
+});
diff --git a/js/src/views/ParityBar/parityBar.test.js b/js/src/views/ParityBar/parityBar.test.js
new file mode 100644
index 00000000000..2623e407480
--- /dev/null
+++ b/js/src/views/ParityBar/parityBar.test.js
@@ -0,0 +1,55 @@
+// Copyright 2015-2017 Parity Technologies (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see
.
+
+import sinon from 'sinon';
+
+const ACCOUNT_DEFAULT = '0x2345678901';
+const ACCOUNT_FIRST = '0x1234567890';
+const ACCOUNT_NEW = '0x0987654321';
+const ACCOUNTS = {
+ [ACCOUNT_FIRST]: { uuid: 123 },
+ [ACCOUNT_DEFAULT]: { uuid: 234 },
+ '0x3456789012': {},
+ [ACCOUNT_NEW]: { uuid: 456 }
+};
+
+function createApi () {
+ const api = {
+ subscribe: (params, callback) => {
+ callback(null, ACCOUNT_DEFAULT);
+
+ return Promise.resolve(1);
+ },
+ parity: {
+ defaultAccount: sinon.stub().resolves(ACCOUNT_DEFAULT),
+ allAccountsInfo: sinon.stub().resolves(ACCOUNTS),
+ getNewDappsWhitelist: sinon.stub().resolves(null),
+ setNewDappsWhitelist: sinon.stub().resolves(true)
+ }
+ };
+
+ sinon.spy(api, 'subscribe');
+
+ return api;
+}
+
+export {
+ ACCOUNT_DEFAULT,
+ ACCOUNT_FIRST,
+ ACCOUNT_NEW,
+ ACCOUNTS,
+ createApi
+};