summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrei Mihailov <and.mikhaylov@gmail.com>2017-02-16 12:18:52 +0000
committerGitHub <noreply@github.com>2017-02-16 12:18:52 +0000
commit4aaf7e0ddbbdf9a5e03fdbb20ecabe4866de96c3 (patch)
tree8f7dfc5545c484472d3dc5138cddf6a810fa0558
parent5ba4a72e546747167e7ae815724ee2dd4b2ef8fb (diff)
downloadmullvadvpn-4aaf7e0ddbbdf9a5e03fdbb20ecabe4866de96c3.tar.xz
mullvadvpn-4aaf7e0ddbbdf9a5e03fdbb20ecabe4866de96c3.zip
Feature/menubar popup window (#1)
Add menubar and tests
-rw-r--r--app/actions/connect.js2
-rw-r--r--app/app.js31
-rw-r--r--app/assets/css/style.css2
-rw-r--r--app/assets/images/app-triangle-error.svg5
-rw-r--r--app/assets/images/app-triangle-success.svg5
-rw-r--r--app/assets/images/app-triangle.svg5
-rw-r--r--app/assets/images/trayIconTemplate.png (renamed from app/assets/images/trayicon.png)bin950 -> 950 bytes
-rw-r--r--app/assets/images/trayIconTemplate@2x.png (renamed from app/assets/images/trayicon@2x.png)bin1316 -> 1316 bytes
-rw-r--r--app/components/Connect.js9
-rw-r--r--app/components/HeaderBar.css29
-rw-r--r--app/components/HeaderBar.js2
-rw-r--r--app/components/Layout.css2
-rw-r--r--app/components/Layout.js4
-rw-r--r--app/components/Tray.js39
-rw-r--r--app/containers/ConnectPage.js11
-rw-r--r--app/containers/Tray.js14
-rw-r--r--app/lib/components/TrayMenu.js120
-rw-r--r--app/main.js180
-rw-r--r--app/store.js5
-rw-r--r--electron-builder.yml2
-rw-r--r--package.json2
-rw-r--r--test/actions/user.spec.js31
-rw-r--r--test/lib/enum.spec.js36
23 files changed, 243 insertions, 293 deletions
diff --git a/app/actions/connect.js b/app/actions/connect.js
index b1c6ea436a..ff8b4c5632 100644
--- a/app/actions/connect.js
+++ b/app/actions/connect.js
@@ -1 +1 @@
-export default {}
+export default {};
diff --git a/app/app.js b/app/app.js
index 6ac3ce492f..8789608887 100644
--- a/app/app.js
+++ b/app/app.js
@@ -3,21 +3,27 @@ import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Router, hashHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
-import { remote, webFrame } from 'electron';
-import path from 'path';
+import { webFrame } from 'electron';
import routes from './routes';
import configureStore from './store';
-import Tray from './containers/Tray';
import Backend from './lib/backend';
const backend = new Backend();
-const iconPath = path.join(__dirname, 'assets/images/trayicon.png');
-const tray = new remote.Tray(iconPath);
-
const initialState = {};
const store = configureStore(initialState);
-const routerHistory = syncHistoryWithStore(hashHistory, store);
+
+// desperately trying to fix https://github.com/reactjs/react-router-redux/issues/534
+hashHistory.replace('/');
+
+// see https://github.com/reactjs/react-router-redux/issues/534
+const recentLocation = (store.getState().routing || {}).locationBeforeTransitions;
+const routerHistory = syncHistoryWithStore(hashHistory, store, { adjustUrlOnReplay: true });
+
+if(recentLocation && recentLocation.pathname) {
+ routerHistory.replace(recentLocation.pathname);
+}
+
const rootElement = document.querySelector(document.currentScript.getAttribute('data-container'));
// disable smart pinch.
@@ -32,13 +38,8 @@ const createElement = (Component, props) => {
};
ReactDOM.render(
- <div>
- <Provider store={ store }>
- <Router history={ routerHistory } routes={ routes } createElement={ createElement } />
- </Provider>
- <Provider store={ store }>
- <Tray handle={ tray } backend={ backend } />
- </Provider>
- </div>,
+ <Provider store={ store }>
+ <Router history={ routerHistory } routes={ routes } createElement={ createElement } />
+ </Provider>,
rootElement
);
diff --git a/app/assets/css/style.css b/app/assets/css/style.css
index fa883fee98..3b8706f63e 100644
--- a/app/assets/css/style.css
+++ b/app/assets/css/style.css
@@ -2,6 +2,6 @@
@import 'fonts.css';
@import 'global.css';
@import '../../components/Login.css';
-@import '../../components/LoggedIn.css';
+@import '../../components/Connect.css';
@import '../../components/HeaderBar.css';
@import '../../components/Layout.css';
diff --git a/app/assets/images/app-triangle-error.svg b/app/assets/images/app-triangle-error.svg
new file mode 100644
index 0000000000..2914d7e389
--- /dev/null
+++ b/app/assets/images/app-triangle-error.svg
@@ -0,0 +1,5 @@
+<svg width="30px" height="13px" viewBox="0 0 30 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>app-triangle-extended</title>
+ <desc>Mullvad VPN app</desc>
+ <path fill="#D0021B" d="M0,12 L30,12 L30,13 L0,13 L0,12 Z M0,12 C7.24137931,12 12.9310345,1.0135008e-16 15,0 C17.0689655,0 23.7931034,12 30,12 L0,12 Z" id="app-triangle-extended"></path>
+</svg> \ No newline at end of file
diff --git a/app/assets/images/app-triangle-success.svg b/app/assets/images/app-triangle-success.svg
new file mode 100644
index 0000000000..711e99afd7
--- /dev/null
+++ b/app/assets/images/app-triangle-success.svg
@@ -0,0 +1,5 @@
+<svg width="30px" height="13px" viewBox="0 0 30 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>app-triangle-extended</title>
+ <desc>Mullvad VPN app</desc>
+ <path fill="#44AD4D" d="M0,12 L30,12 L30,13 L0,13 L0,12 Z M0,12 C7.24137931,12 12.9310345,1.0135008e-16 15,0 C17.0689655,0 23.7931034,12 30,12 L0,12 Z" id="app-triangle-extended"></path>
+</svg> \ No newline at end of file
diff --git a/app/assets/images/app-triangle.svg b/app/assets/images/app-triangle.svg
new file mode 100644
index 0000000000..e2f7ca044b
--- /dev/null
+++ b/app/assets/images/app-triangle.svg
@@ -0,0 +1,5 @@
+<svg width="30px" height="13px" viewBox="0 0 30 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>app-triangle-extended</title>
+ <desc>Mullvad VPN app</desc>
+ <path fill="#294D73" d="M0,12 L30,12 L30,13 L0,13 L0,12 Z M0,12 C7.24137931,12 12.9310345,1.0135008e-16 15,0 C17.0689655,0 23.7931034,12 30,12 L0,12 Z" id="app-triangle-extended"></path>
+</svg> \ No newline at end of file
diff --git a/app/assets/images/trayicon.png b/app/assets/images/trayIconTemplate.png
index 3888745a8f..3888745a8f 100644
--- a/app/assets/images/trayicon.png
+++ b/app/assets/images/trayIconTemplate.png
Binary files differ
diff --git a/app/assets/images/trayicon@2x.png b/app/assets/images/trayIconTemplate@2x.png
index 3b5f364903..3b5f364903 100644
--- a/app/assets/images/trayicon@2x.png
+++ b/app/assets/images/trayIconTemplate@2x.png
Binary files differ
diff --git a/app/components/Connect.js b/app/components/Connect.js
index dc32594067..27aaedd4ee 100644
--- a/app/components/Connect.js
+++ b/app/components/Connect.js
@@ -1,14 +1,19 @@
-import React, { Component } from 'react';
+import React, { Component, PropTypes } from 'react';
import { Layout, Container, Header } from './Layout';
export default class Connect extends Component {
+
+ static propTypes = {
+ logout: PropTypes.func.isRequired
+ }
+
render() {
return (
<Layout>
<Header />
<Container>
<div className="connect">
-
+ <button style={{ width: '100px', display: 'block', margin: '10px auto' }} onClick={ this.props.logout }>Log out</button>
</div>
</Container>
</Layout>
diff --git a/app/components/HeaderBar.css b/app/components/HeaderBar.css
index 4d9d326eff..140e64df55 100644
--- a/app/components/HeaderBar.css
+++ b/app/components/HeaderBar.css
@@ -1,15 +1,40 @@
.headerbar {
+ margin-top: 12px;
padding: 12px;
+ background-color: #294D73;
+ border-radius: 8px 8px 0 0;
+ position: relative;
}
-.header--style-error {
+.headerbar:before {
+ display: block;
+ content: '';
+ width: 30px;
+ height: 13px;
+ margin: -12px 0 0 -15px;
+ position: absolute;
+ left: 50%;
+ top: 0%;
+ background-image: url(../assets/images/app-triangle.svg);
+ background-repeat: no-repeat;
+}
+
+.headerbar--style-error {
background-color: #D0021B;
}
-.header--style-success {
+.headerbar--style-error:before {
+ background-image: url(../assets/images/app-triangle-error.svg);
+}
+
+.headerbar--style-success {
background-color: #44AD4D;
}
+.headerbar--style-success:before {
+ background-image: url(../assets/images/app-triangle-success.svg);
+}
+
.headerbar__title {
font-family: DINPro;
font-size: 24px;
diff --git a/app/components/HeaderBar.js b/app/components/HeaderBar.js
index d13c5c3462..ce5d039f1f 100644
--- a/app/components/HeaderBar.js
+++ b/app/components/HeaderBar.js
@@ -15,7 +15,7 @@ export default class HeaderBar extends Component {
let containerClass = ['headerbar'];
if(HeaderBar.Style.isValid(style)) {
- containerClass.push(`header--style-${style}`);
+ containerClass.push(`headerbar--style-${style}`);
}
return (
diff --git a/app/components/Layout.css b/app/components/Layout.css
index 8ef242d7f5..5d1aa92445 100644
--- a/app/components/Layout.css
+++ b/app/components/Layout.css
@@ -2,7 +2,6 @@
display: flex;
flex-direction: column;
height: 100vh;
- background: #294D73;
}
.layout__header {
@@ -11,4 +10,5 @@
.layout__container {
flex: 1 1 100%;
+ background: #294D73;
} \ No newline at end of file
diff --git a/app/components/Layout.js b/app/components/Layout.js
index b0cc0a8420..50f52e7dca 100644
--- a/app/components/Layout.js
+++ b/app/components/Layout.js
@@ -10,7 +10,7 @@ export class Header extends Component {
<div className="layout__header">
<HeaderBar {...this.props} />
</div>
- )
+ );
}
}
@@ -24,7 +24,7 @@ export class Container extends Component {
<div className="layout__container">
{this.props.children}
</div>
- )
+ );
}
}
diff --git a/app/components/Tray.js b/app/components/Tray.js
deleted file mode 100644
index 00dc679981..0000000000
--- a/app/components/Tray.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import { TrayMenu, TrayItem } from '../lib/components/TrayMenu';
-import { shell } from 'electron';
-
-import { LoginState } from '../constants';
-
-export default class Tray extends Component {
-
- static propTypes = {
- handle: PropTypes.object.isRequired,
- backend: PropTypes.object.isRequired
- }
-
- logout() {
- this.props.logout(this.props.backend);
- }
-
- openPrivacyPolicy() {
- shell.openExternal('https://mullvad.net/#privacy');
- }
-
- openHomepage() {
- shell.openExternal('https://mullvad.net');
- }
-
- render() {
- const loggedIn = this.props.user.status === LoginState.ok;
-
- return (
- <TrayMenu tray={ this.props.handle }>
- <TrayItem label="Log out" click={ ::this.logout } visible={ loggedIn } />
- <TrayItem type="separator" visible={ loggedIn } />
- <TrayItem label="Privacy Policy" click={ ::this.openPrivacyPolicy } />
- <TrayItem label="Visit homepage" click={ ::this.openHomepage } />
- </TrayMenu>
- );
- }
-
-} \ No newline at end of file
diff --git a/app/containers/ConnectPage.js b/app/containers/ConnectPage.js
index 9aad1f81f5..9802e8b2d8 100644
--- a/app/containers/ConnectPage.js
+++ b/app/containers/ConnectPage.js
@@ -1,12 +1,19 @@
import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
import Connect from '../components/Connect';
+import userActions from '../actions/user';
const mapStateToProps = (state) => {
return state;
};
-const mapDispatchToProps = (dispatch) => { // eslint-disable-line no-unused-vars
- return {};
+const mapDispatchToProps = (dispatch, props) => { // eslint-disable-line no-unused-vars
+ const user = bindActionCreators(userActions, dispatch);
+ return {
+ logout: () => {
+ return user.logout(props.backend);
+ }
+ };
};
export default connect(mapStateToProps, mapDispatchToProps)(Connect);
diff --git a/app/containers/Tray.js b/app/containers/Tray.js
deleted file mode 100644
index 3916263d0d..0000000000
--- a/app/containers/Tray.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
-import Tray from '../components/Tray';
-import userActions from '../actions/user';
-
-const mapStateToProps = (state) => {
- return state;
-};
-
-const mapDispatchToProps = (dispatch) => {
- return bindActionCreators(userActions, dispatch);
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(Tray);
diff --git a/app/lib/components/TrayMenu.js b/app/lib/components/TrayMenu.js
deleted file mode 100644
index 5f14ca71f9..0000000000
--- a/app/lib/components/TrayMenu.js
+++ /dev/null
@@ -1,120 +0,0 @@
-/**
- * Declarative Tray implementation for React + Electron
- */
-
-import React, { Component, PropTypes } from 'react';
-import { remote } from 'electron';
-
-const { Menu, MenuItem } = remote;
-
-/**
- * Tray menu component
- *
- * Example:
- *
- * const tray = new remote.Tray('/path/to/icon');
- *
- * return (
- * <TrayMenu tray={tray}>
- * <TrayItem label="Visit homepage" />
- * </TrayMenu>
- * )
- */
-export class TrayMenu extends Component {
-
- static childContextTypes = {
- menu: PropTypes.object.isRequired
- };
-
- static propTypes = {
- tray: PropTypes.object.isRequired,
- children: PropTypes.arrayOf(PropTypes.node).isRequired
- };
-
- _contextMenu = null;
-
- getChildContext() {
- return { menu: this._contextMenu };
- }
-
- componentDidMount() {
- this.props.tray.setContextMenu(this._contextMenu);
- }
-
- componentDidUpdate() {
- this.props.tray.setContextMenu(this._contextMenu);
- }
-
- render() {
- // create new menu during each rendering
- // see: https://github.com/electron/electron/issues/8598
- this._contextMenu = new Menu();
-
- return (
- <div>{this.props.children}</div>
- );
- }
-
-}
-
-/**
- * Submenu component
- *
- * Example:
- *
- * <TrayMenu tray={this.props.handle}>
- * <TraySubmenu label="Resources">
- * <TrayItem label="Homepage" />
- * </TraySubmenu>
- * </TrayMenu>
- *
- */
-export class TraySubmenu extends Component {
-
- static contextTypes = {
- menu: PropTypes.object.isRequired
- };
-
- static childContextTypes = {
- menu: PropTypes.object.isRequired
- };
-
- static propTypes = {
- children: PropTypes.arrayOf(PropTypes.node).isRequired
- };
-
- _contextMenu = null;
-
- getChildContext() {
- return { menu: this._contextMenu };
- }
-
- render() {
- // create new menu during each rendering
- // see: https://github.com/electron/electron/issues/8598
- this._contextMenu = new Menu();
-
- this.context.menu.append(new MenuItem({ ...this.props, submenu: this._contextMenu }));
-
- return (
- <div>{this.props.children}</div>
- );
- }
-
-}
-
-/**
- * Item component
- */
-export class TrayItem extends Component {
-
- static contextTypes = {
- menu: PropTypes.object.isRequired
- };
-
- render() {
- this.context.menu.append(new MenuItem(this.props));
- return null;
- }
-
-}
diff --git a/app/main.js b/app/main.js
index caea31ab57..81e5d39e4c 100644
--- a/app/main.js
+++ b/app/main.js
@@ -1,11 +1,13 @@
import path from 'path';
-import url from 'url';
-import { app, crashReporter, BrowserWindow, Menu } from 'electron';
+import { app, crashReporter, BrowserWindow, ipcMain, Tray, Menu } from 'electron';
const isDevelopment = (process.env.NODE_ENV === 'development');
-let mainWindow = null;
-let forceQuit = false;
+let window = null;
+let tray = null;
+
+// hide dock icon
+app.dock.hide();
const installExtensions = async () => {
const installer = require('electron-devtools-installer');
@@ -14,7 +16,7 @@ const installExtensions = async () => {
'REDUX_DEVTOOLS'
];
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
- for (const name of extensions) {
+ for(const name of extensions) {
try {
await installer.default(installer[name], forceDownload);
} catch (e) {
@@ -23,86 +25,122 @@ const installExtensions = async () => {
}
};
-crashReporter.start({
- productName: 'YourName',
- companyName: 'YourCompany',
- submitURL: 'https://your-domain.com/url-to-submit',
- uploadToServer: false
-});
+const installDevTools = async () => {
+ await installExtensions();
-app.on('window-all-closed', () => {
- // On OS X it is common for applications and their menu bar
- // to stay active until the user quits explicitly with Cmd + Q
- if (process.platform !== 'darwin') {
- app.quit();
- }
-});
+ // show devtools when ctrl clicked
+ tray.on('click', function () {
+ if(!window) { return; }
-app.on('ready', async () => {
- if (isDevelopment) {
- await installExtensions();
- }
+ if(window.isDevToolsOpened()) {
+ window.devToolsWebContents.focus();
+ } else {
+ window.openDevTools({ mode: 'detach' });
+ }
+ });
+
+ // add inspect element on right click menu
+ window.webContents.on('context-menu', (e, props) => {
+ Menu.buildFromTemplate([{
+ label: 'Inspect element',
+ click() {
+ window.openDevTools({ mode: 'detach' });
+ window.inspectElement(props.x, props.y);
+ }
+ }]).popup(window);
+ });
+};
+
+const getWindowPosition = () => {
+ const windowBounds = window.getBounds();
+ const trayBounds = tray.getBounds();
+
+ // center window horizontally below the tray icon
+ const x = Math.round(trayBounds.x + (trayBounds.width / 2) - (windowBounds.width / 2));
- mainWindow = new BrowserWindow({
+ // position window vertically below the tray icon
+ const y = Math.round(trayBounds.y + trayBounds.height);
+
+ return { x, y };
+};
+
+const createWindow = () => {
+ window = new BrowserWindow({
width: 320,
- height: 568 + 20, // 20pt window chrome
- show: false,
- backgroundColor: '#000000',
+ height: 568,
+ frame: false,
resizable: false,
maximizable: false,
- fullscreenable: false
+ fullscreenable: false,
+ transparent: true,
+ show: false,
+ webPreferences: {
+ // prevents renderer process code from not running when window is hidden
+ backgroundThrottling: false
+ }
});
- mainWindow.loadURL(url.format({
- pathname: path.join(__dirname, 'index.html'),
- protocol: 'file:',
- slashes: true
- }));
+ window.loadURL('file://' + path.join(__dirname, 'index.html'));
- // show window once on first load
- mainWindow.webContents.once('did-finish-load', () => {
- mainWindow.show();
+ // hide the window when it loses focus
+ window.on('blur', () => {
+ if(!window.webContents.isDevToolsOpened()) {
+ window.hide();
+ }
});
- mainWindow.webContents.on('did-finish-load', () => {
- // Handle window logic properly on macOS:
- // 1. App should not terminate if window has been closed
- // 2. Click on icon in dock should re-open the window
- // 3. ⌘+Q should close the window and quit the app
- if (process.platform === 'darwin') {
- mainWindow.on('close', function (e) {
- if (!forceQuit) {
- e.preventDefault();
- mainWindow.hide();
- }
- });
+ window.on('show', () => {
+ tray.setHighlightMode('always');
+ });
- app.on('activate', () => {
- mainWindow.show();
- });
-
- app.on('before-quit', () => {
- forceQuit = true;
- });
- } else {
- mainWindow.on('closed', () => {
- mainWindow = null;
- });
- }
+ window.on('hide', () => {
+ tray.setHighlightMode('never');
});
- if (isDevelopment) {
- // auto-open dev tools
- mainWindow.webContents.openDevTools();
+};
+
+const toggleWindow = () => {
+ if (window.isVisible()) {
+ window.hide();
+ } else {
+ showWindow();
+ }
+};
+
+const showWindow = () => {
+ const position = getWindowPosition();
+ window.setPosition(position.x, position.y, false);
+ window.show();
+ window.focus();
+};
+
+ipcMain.on('show-window', () => {
+ showWindow();
+});
+
+const createTray = () => {
+ tray = new Tray(path.join(__dirname, 'assets/images/trayIconTemplate.png'));
+ tray.on('right-click', toggleWindow);
+ tray.on('double-click', toggleWindow);
+ tray.on('click', toggleWindow);
+};
+
+crashReporter.start({
+ productName: 'YourName',
+ companyName: 'YourCompany',
+ submitURL: 'https://your-domain.com/url-to-submit',
+ uploadToServer: false
+});
+
+app.on('window-all-closed', () => {
+ app.quit();
+});
+
+app.on('ready', () => {
+ createTray();
+ createWindow();
- // add inspect element on right click menu
- mainWindow.webContents.on('context-menu', (e, props) => {
- Menu.buildFromTemplate([{
- label: 'Inspect element',
- click() {
- mainWindow.inspectElement(props.x, props.y);
- }
- }]).popup(mainWindow);
- });
+ if(isDevelopment) {
+ installDevTools();
}
});
diff --git a/app/store.js b/app/store.js
index 350650631b..55a3df85f8 100644
--- a/app/store.js
+++ b/app/store.js
@@ -1,6 +1,6 @@
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import { hashHistory } from 'react-router';
-import { routerMiddleware, routerReducer as routing, push } from 'react-router-redux';
+import { routerMiddleware, routerReducer as routing, push, replace } from 'react-router-redux';
import persistState from 'redux-localstorage';
import thunk from 'redux-thunk';
@@ -11,7 +11,8 @@ const router = routerMiddleware(hashHistory);
const actionCreators = {
...userActions,
- push
+ pushRoute: (route) => push(route),
+ replaceRoute: (route) => replace(route),
};
const reducers = {
diff --git a/electron-builder.yml b/electron-builder.yml
index 04f8620817..a9646c0878 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -27,6 +27,8 @@ dmg:
mac:
target: dmg
category: public.app-category.tools
+ extendInfo:
+ LSUIElement: true
win:
target: nsis
diff --git a/package.json b/package.json
index 612103bf91..f8a6bebb39 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
"chai": "^3.4.1",
"chai-spies": "^0.7.1",
"electron": "^1.5.0",
- "electron-builder": "^12.3.1",
+ "electron-builder": "^13.8.2",
"electron-devtools-installer": "^2.1.0",
"eslint": "^3.14.1",
"eslint-plugin-react": "^6.9.0",
diff --git a/test/actions/user.spec.js b/test/actions/user.spec.js
index 259e862a6d..9bd93d4010 100644
--- a/test/actions/user.spec.js
+++ b/test/actions/user.spec.js
@@ -27,7 +27,7 @@ describe('actions', () => {
const account = '1234';
const getState = () => ({});
let callCounter = 0;
- const dispatch = chai.spy((action) => {
+ const dispatch = chai.spy(() => {
callCounter += 1;
if(callCounter == 2) {
@@ -59,7 +59,7 @@ describe('actions', () => {
const account = '1234';
const getState = () => ({});
let callCounter = 0;
- const dispatch = chai.spy((action) => {
+ const dispatch = chai.spy(() => {
callCounter += 1;
if(callCounter == 2) {
@@ -86,5 +86,32 @@ describe('actions', () => {
action(dispatch, getState);
});
+ it('should log out', (done) => {
+ let callCount = 0;
+ const getState = () => ({ account: '1234', status: LoginState.ok });
+ const dispatch = chai.spy( (u) => {
+ callCount += 1;
+
+ if(callCount === 1) {
+ expect(dispatch).to.have.been.called.with({
+ type: actions.loginChange.toString(),
+ payload: {
+ account: '',
+ status: LoginState.none,
+ error: null
+ }
+ });
+
+ done();
+ }
+ });
+ const backend = {
+ logout: () => Promise.resolve()
+ };
+ const action = actions.logout(backend);
+
+ action(dispatch, getState);
+ });
+
});
});
diff --git a/test/lib/enum.spec.js b/test/lib/enum.spec.js
index 0f5d6ee4d1..20de191d08 100644
--- a/test/lib/enum.spec.js
+++ b/test/lib/enum.spec.js
@@ -1,24 +1,26 @@
import { expect } from 'chai';
import Enum from '../../app/lib/enum';
-describe('enum', () => {
-
- it('should be able to compare values', () => {
- const e = Enum('NORTH', 'SOUTH', 'WEST', 'EAST');
- expect(e.NORTH).to.be.equal('NORTH');
- });
+describe('lib', () => {
- it('should not be able to modify enum', () => {
- let e = Enum('NORTH', 'SOUTH', 'WEST', 'EAST');
- expect(() => e.ANYWHERE = 'ANYWHERE').to.throw();
- });
+ describe('enum', () => {
+ it('should be able to compare values', () => {
+ const e = Enum('NORTH', 'SOUTH', 'WEST', 'EAST');
+ expect(e.NORTH).to.be.equal('NORTH');
+ });
- it('should be able to validate enum keys', () => {
- let e = Enum('NORTH', 'SOUTH', 'WEST', 'EAST');
- expect(e.isValid('SOUTH')).to.be.true;
- expect(e.isValid('ANYWHERE')).to.be.false;
- expect(e.isValid()).to.be.false;
- expect(e.isValid(null)).to.be.false;
- })
+ it('should not be able to modify enum', () => {
+ let e = Enum('NORTH', 'SOUTH', 'WEST', 'EAST');
+ expect(() => e.ANYWHERE = 'ANYWHERE').to.throw();
+ });
+
+ it('should be able to validate enum keys', () => {
+ let e = Enum('NORTH', 'SOUTH', 'WEST', 'EAST');
+ expect(e.isValid('SOUTH')).to.be.true;
+ expect(e.isValid('ANYWHERE')).to.be.false;
+ expect(e.isValid()).to.be.false;
+ expect(e.isValid(null)).to.be.false;
+ });
+ });
});