summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@codeispoetry.ru>2017-03-16 21:16:31 +0000
committerAndrej Mihajlov <and@codeispoetry.ru>2017-03-16 21:16:31 +0000
commit627c285b8409a8872208f54e8b9f3c3e3f4bd34d (patch)
treefcd9b116d038ca1b12de34d7a80184208c1e3046
parent0fca9f68baba1ef13cfa76b8316bf93f37d6f741 (diff)
downloadmullvadvpn-627c285b8409a8872208f54e8b9f3c3e3f4bd34d.tar.xz
mullvadvpn-627c285b8409a8872208f54e8b9f3c3e3f4bd34d.zip
Refactor code
-rw-r--r--app/app.js12
-rw-r--r--app/enums.js9
-rw-r--r--app/lib/tray-animation.js271
-rw-r--r--app/lib/tray-animator.js176
-rw-r--r--app/lib/tray-icon-manager.js99
-rw-r--r--app/lib/tray-icon-provider.js100
-rw-r--r--app/main.js63
-rw-r--r--test/tray-animator.spec.js3
8 files changed, 499 insertions, 234 deletions
diff --git a/app/app.js b/app/app.js
index 114b5e276e..468deca4df 100644
--- a/app/app.js
+++ b/app/app.js
@@ -12,7 +12,7 @@ import connectActions from './actions/connect';
import Backend from './lib/backend';
import mapBackendEventsToReduxActions from './lib/backend-redux-actions';
import mapBackendEventsToRouter from './lib/backend-routing';
-import { LoginState, ConnectionState } from './enums';
+import { LoginState, ConnectionState, TrayIconType } from './enums';
const initialState = {};
const memoryHistory = createMemoryHistory();
@@ -45,15 +45,15 @@ if(recentLocation && recentLocation.pathname) {
// Tray icon
const updateTrayIcon = () => {
- const getName = (s) => {
+ const getIconType = (s) => {
switch(s) {
- case ConnectionState.connected: return 'secured';
- case ConnectionState.connecting: return 'securing';
- default: return 'unsecured';
+ case ConnectionState.connected: return TrayIconType.secured;
+ case ConnectionState.connecting: return TrayIconType.securing;
+ default: return TrayIconType.unsecured;
}
};
const { connect } = store.getState();
- ipcRenderer.send('changeTrayIcon', getName(connect.status));
+ ipcRenderer.send('changeTrayIcon', getIconType(connect.status));
};
// patch backend
diff --git a/app/enums.js b/app/enums.js
index bc14ba7258..399c4602b4 100644
--- a/app/enums.js
+++ b/app/enums.js
@@ -19,3 +19,12 @@ export const LoginState = new Enum('none', 'connecting', 'failed', 'ok');
* @property {string} failed Failed to connect
*/
export const ConnectionState = new Enum('disconnected', 'connecting', 'connected', 'failed');
+
+/**
+ * Tray icon type
+ * @type {TrayIconType}
+ * @property {string} unsecured - Initial state (unlocked)
+ * @property {string} securing - Securing network (spinner)
+ * @property {string} secured - Connection is secured (locked)
+ */
+export const TrayIconType = new Enum('unsecured', 'securing', 'secured');
diff --git a/app/lib/tray-animation.js b/app/lib/tray-animation.js
new file mode 100644
index 0000000000..82496033ab
--- /dev/null
+++ b/app/lib/tray-animation.js
@@ -0,0 +1,271 @@
+import assert from 'assert';
+import fs from 'fs';
+import path from 'path';
+import { nativeImage } from 'electron';
+
+/**
+ * Tray animation descriptor
+ *
+ * @export
+ * @class TrayAnimation
+ * @property {number} speed - speed per frame
+ * @property {bool} repeat - whether to repeat animation
+ * @property {bool} reverse - play in reverse
+ * @property {bool} alternate - whether to alternate sequence when reached the end of animation
+ * @property {string[]} source - image source
+ * @property {electron.NativeImage} nativeImages - a sequence of native images
+ * @property {bool} isFinished - whether animation sequence is finished (repeating animation never finish)
+ */
+export class TrayAnimation {
+
+ /**
+ * Set animation pace per frame in ms
+ *
+ * @memberOf TrayAnimation
+ */
+ set speed(v) { this._speed = parseInt(v); }
+
+ /**
+ * Get animation pace per frame in ms
+ *
+ * @readonly
+ *
+ * @memberOf TrayAnimation
+ */
+ get speed() { return this._speed; }
+
+ /**
+ * Set animation repetition
+ * @memberOf TrayAnimation
+ */
+ set repeat(v) { this._repeat = !!v; }
+
+ /**
+ * Get animation repetition
+ *
+ * @readonly
+ *
+ * @memberOf TrayAnimation
+ */
+ get repeat() { return this._repeat; }
+
+ /**
+ * Set animation reversal
+ * @memberOf TrayAnimation
+ */
+ set reverse(v) { this._reverse = !!v; }
+
+ /**
+ * Get animation reversal
+ *
+ * @readonly
+ *
+ * @memberOf TrayAnimation
+ */
+ get reverse() { return this._repeat; }
+
+ /**
+ * Set animation alternation
+ *
+ * @memberOf TrayAnimation
+ */
+ set alternate(v) { this._alternate = !!v; }
+
+ /**
+ * Get animation alternation
+ *
+ * @readonly
+ *
+ * @memberOf TrayAnimation
+ */
+ get alternate() { return this._alternate; }
+
+ /**
+ * Source array of images
+ *
+ * @readonly
+ *
+ * @memberOf TrayAnimation
+ */
+ get source() { return this._source.slice(); }
+
+ /**
+ * Array of NativeImage instances loaded based on source input
+ *
+ * @readonly
+ *
+ * @memberOf TrayAnimation
+ */
+ get nativeImages() { return this._nativeImages.slice(); }
+
+ /**
+ * Flag that tells whether animation finished
+ *
+ * @readonly
+ *
+ * @memberOf TrayAnimation
+ */
+ get isFinished() { return this._isFinished; }
+
+ /**
+ * Create animation using file sequence
+ *
+ * @static
+ * @param {string} filePattern - file name pattern where {s} is replaced with index
+ * @param {number[]} range - sequence range [start, end]
+ *
+ * @memberOf TrayAnimation
+ * @return {TrayAnimation}
+ */
+ static fromFileSequence(filePattern, range) {
+ assert(range.length === 2 && range[0] < range[1]);
+
+ let images = [];
+ for(let i = range[0]; i <= range[1]; i++) {
+ images.push(filePattern.replace('{s}', i));
+ }
+
+ return new TrayAnimation(images);
+ }
+
+ /**
+ * Creates an instance of TrayAnimation.
+ * @param {string[]} images
+ *
+ * @memberOf TrayAnimation
+ */
+ constructor(images) {
+ assert(images.length > 0);
+
+ this._source = images.slice();
+ this._nativeImages = images.map((pathOrNativeImage) => {
+ if(typeof(pathOrNativeImage) === 'string') {
+ return nativeImage.createFromPath(pathOrNativeImage);
+ } else if(typeof(pathOrNativeImage) === 'NativeImage') {
+ return pathOrNativeImage;
+ }
+ return nativeImage.createEmpty();
+ });
+
+ this._speed = 200; // ms
+ this._repeat = false;
+ this._reverse = false;
+ this._alternate = false;
+
+ this._numFrames = images.length;
+ this._currentFrame = 0;
+ this._isFinished = false;
+ }
+
+ /**
+ * Get current sprite
+ *
+ * @readonly
+ *
+ * @memberOf TrayAnimation
+ */
+ get currentImage() {
+ return this._nativeImages[this._currentFrame];
+ }
+
+ /**
+ * Prepare initial state for animation before running it.
+ * @memberOf TrayAnimation
+ */
+ prepare() {
+ this._currentFrame = this._firstFrame(this._reverse);
+ }
+
+ /**
+ * Advance animation to the start
+ * This method respects animation reversal
+ *
+ * @memberOf TrayAnimation
+ */
+ advanceToStart() {
+ this._currentFrame = this._firstFrame(this._reverse);
+ }
+
+ /**
+ * Advance animation to the end
+ * This method respects animation reversal
+ *
+ * @memberOf TrayAnimation
+ */
+ advanceToEnd() {
+ this._currentFrame = this._lastFrame(this._reverse);
+ }
+
+ /**
+ * Advance animation frame
+ * @memberOf TrayAnimation
+ */
+ advanceFrame() {
+ // do not advance frame when animation is finished
+ if(this._isFinished) { return; }
+
+ // advance frame
+ let nextFrame = this._nextFrame(this._currentFrame, this._reverse);
+
+ // did reach end?
+ if(nextFrame < 0 || nextFrame >= this._numFrames) {
+ // mark animation as finished if it's not marked as repeating
+ if(!this._repeat) {
+ this._isFinished = true;
+ return;
+ }
+
+ // change animation direction if marked for alternation
+ if(this._alternate) {
+ this._reverse = !this._reverse;
+
+ // clamp range
+ nextFrame = Math.min(Math.max(0, nextFrame), this._numFrames - 1);
+
+ // skip corner frame when alternating by advancing frame once again
+ nextFrame = this._nextFrame(nextFrame, this._reverse);
+ } else {
+ nextFrame = this._firstFrame(this._reverse);
+ }
+ }
+ this._currentFrame = nextFrame;
+ }
+
+ /**
+ * Calculate next frame
+ * @private
+ * @param {number} cur - current frame
+ * @param {bool} isReverse - reverse sequence direction?
+ * @returns {number}
+ *
+ * @memberOf TrayAnimation
+ */
+ _nextFrame(cur, isReverse) {
+ return cur + (isReverse ? -1 : 1);
+ }
+
+ /**
+ * Get first frame of animation
+ *
+ * @param {bool} isReverse reverse animation?
+ * @returns {number}
+ *
+ * @memberOf TrayAnimation
+ */
+ _firstFrame(isReverse) {
+ return isReverse ? this._numFrames - 1 : 0;
+ }
+
+ /**
+ * Get last frame of animation
+ *
+ * @param {bool} isReverse reverse animation?
+ * @returns {number}
+ *
+ * @memberOf TrayAnimation
+ */
+ _lastFrame(isReverse) {
+ return isReverse ? 0 : this._numFrames - 1;
+ }
+
+} \ No newline at end of file
diff --git a/app/lib/tray-animator.js b/app/lib/tray-animator.js
index c4c00628c5..601362d96d 100644
--- a/app/lib/tray-animator.js
+++ b/app/lib/tray-animator.js
@@ -4,182 +4,6 @@ import path from 'path';
import { nativeImage } from 'electron';
/**
- * Tray animation descriptor
- *
- * @export
- * @class TrayAnimation
- * @property {number} speed - speed per frame
- * @property {bool} repeat - whether to repeat animation
- * @property {bool} reverse - play in reverse
- * @property {bool} alternate - whether to alternate sequence when reached the end of animation
- * @property {string[]} source - image source
- * @property {electron.NativeImage} nativeImages - a sequence of native images
- * @property {bool} isFinished - whether animation sequence is finished (repeating animation never finish)
- */
-export class TrayAnimation {
-
- set speed(v) { this._speed = parseInt(v); }
- get speed() { return this._speed; }
-
- set repeat(v) { this._repeat = !!v; }
- get repeat() { return this._repeat; }
-
- set reverse(v) { this._reverse = !!v; }
- get reverse() { return this._repeat; }
-
- set alternate(v) { this._alternate = !!v; }
- get alternate() { return this._alternate; }
-
- get source() { return this._source.slice(); }
- get nativeImages() { return this._nativeImages.slice(); }
-
- get isFinished() { return this._isFinished; }
-
- /**
- * Create animation using file sequence
- *
- * @static
- * @param {string} filePattern - file name pattern where {s} is replaced with index
- * @param {number[]} range - sequence range [start, end]
- *
- * @memberOf TrayAnimation
- * @return {TrayAnimation}
- */
- static fromFileSequence(filePattern, range) {
- assert(range.length === 2 && range[0] < range[1]);
-
- let images = [];
- for(let i = range[0]; i <= range[1]; i++) {
- images.push(filePattern.replace('{s}', i));
- }
-
- return new TrayAnimation(images);
- }
-
- /**
- * Creates an instance of TrayAnimation.
- * @param {string[]} images
- *
- * @memberOf TrayAnimation
- */
- constructor(images) {
- assert(images.length > 0);
-
- this._source = images.slice();
- this._nativeImages = images.map((pathOrNativeImage) => {
- if(typeof(pathOrNativeImage) === 'string') {
- return nativeImage.createFromPath(pathOrNativeImage);
- } else if(typeof(pathOrNativeImage) === 'NativeImage') {
- return pathOrNativeImage;
- }
- return nativeImage.createEmpty();
- });
-
- this._speed = 200; // ms
- this._repeat = false;
- this._reverse = false;
- this._alternate = false;
-
- this._numFrames = images.length;
- this._currentFrame = 0;
- this._isFinished = false;
- }
-
- get currentImage() {
- return this._nativeImages[this._currentFrame];
- }
-
- /**
- * Prepare initial state for animation before running it.
- * @memberOf TrayAnimation
- */
- prepare() {
- this._currentFrame = this._firstFrame(this._reverse);
- }
-
- advanceToStart() {
- this._currentFrame = this._firstFrame(this._reverse);
- }
-
- advanceToEnd() {
- this._currentFrame = this._lastFrame(this._reverse);
- }
-
- /**
- * Advance animation frame
- * @memberOf TrayAnimation
- */
- advanceFrame() {
- // do not advance frame when animation is finished
- if(this._isFinished) { return; }
-
- // advance frame
- let nextFrame = this._nextFrame(this._currentFrame, this._reverse);
-
- // did reach end?
- if(nextFrame < 0 || nextFrame >= this._numFrames) {
- // mark animation as finished if it's not marked as repeating
- if(!this._repeat) {
- this._isFinished = true;
- return;
- }
-
- // change animation direction if marked for alternation
- if(this._alternate) {
- this._reverse = !this._reverse;
-
- // clamp range
- nextFrame = Math.min(Math.max(0, nextFrame), this._numFrames - 1);
-
- // skip corner frame when alternating by advancing frame once again
- nextFrame = this._nextFrame(nextFrame, this._reverse);
- } else {
- nextFrame = this._firstFrame(this._reverse);
- }
- }
- this._currentFrame = nextFrame;
- }
-
- /**
- * Calculate next frame
- * @private
- * @param {number} cur - current frame
- * @param {bool} isReverse - reverse sequence direction?
- * @returns {number}
- *
- * @memberOf TrayAnimation
- */
- _nextFrame(cur, isReverse) {
- return cur + (isReverse ? -1 : 1);
- }
-
- /**
- * Get first frame of animation
- *
- * @param {bool} isReverse reverse animation?
- * @returns {number}
- *
- * @memberOf TrayAnimation
- */
- _firstFrame(isReverse) {
- return isReverse ? this._numFrames - 1 : 0;
- }
-
- /**
- * Get last frame of animation
- *
- * @param {bool} isReverse reverse animation?
- * @returns {number}
- *
- * @memberOf TrayAnimation
- */
- _lastFrame(isReverse) {
- return isReverse ? 0 : this._numFrames - 1;
- }
-
-}
-
-/**
* Tray icon animator
* @class TrayAnimator
*/
diff --git a/app/lib/tray-icon-manager.js b/app/lib/tray-icon-manager.js
new file mode 100644
index 0000000000..e72c348add
--- /dev/null
+++ b/app/lib/tray-icon-manager.js
@@ -0,0 +1,99 @@
+import assert from 'assert';
+import { TrayAnimator } from './tray-animator';
+import { TrayIconType } from '../enums';
+
+/**
+ * Tray icon manager
+ *
+ * @export
+ * @class TrayIconManager
+ */
+export default class TrayIconManager {
+
+ /**
+ * Creates an instance of TrayIconManager.
+ * @param {electron.Tray} tray
+ * @param {TrayIconProvider} iconProvider
+ *
+ * @memberOf TrayIconManager
+ */
+ constructor(tray, iconProvider) {
+ assert(tray);
+ assert(iconProvider);
+
+ this._tray = tray;
+ this._iconProvider = iconProvider;
+ this._animator = null;
+ this._iconType = null;
+ }
+
+ /**
+ * Destroy manager
+ * @memberOf TrayIconManager
+ */
+ destroy() {
+ if(this._animator) {
+ this._animator.stop();
+ this._animator = null;
+ }
+ this._iconType = null;
+ }
+
+ /**
+ * Get current icon type
+ * @memberOf TrayIconManager
+ */
+ get iconType() {
+ return this._iconType;
+ }
+
+ /**
+ * Set current icon type
+ * @memberOf TrayIconManager
+ */
+ set iconType(type) {
+ let animator;
+ assert(TrayIconType.isValid(type));
+
+ // no-op if same animator requested
+ if(this._iconType === type) { return; }
+
+ // destroy existing animator
+ if(this._animator) {
+ this._animator.stop();
+ this._animator = null;
+ }
+
+ // do not animate if setting icon for the first time
+ const skipAnimation = this._iconType === null;
+
+ switch(type) {
+ case TrayIconType.secured:
+ animator = new TrayAnimator(this._tray, this._iconProvider.lockAnimation());
+ if(skipAnimation) {
+ animator.advanceToEnd();
+ } else {
+ animator.start();
+ }
+ break;
+
+ case TrayIconType.unsecured:
+ animator = new TrayAnimator(this._tray, this._iconProvider.unlockAnimation());
+ if(skipAnimation) {
+ animator.advanceToStart();
+ } else {
+ animator.start();
+ }
+ break;
+
+ case TrayIconType.securing:
+ animator = new TrayAnimator(this._tray, this._iconProvider.spinnerAnimation());
+ animator.start();
+ break;
+ }
+
+ this._animator = animator;
+ this._iconType = type;
+ }
+
+} \ No newline at end of file
diff --git a/app/lib/tray-icon-provider.js b/app/lib/tray-icon-provider.js
new file mode 100644
index 0000000000..cf859852eb
--- /dev/null
+++ b/app/lib/tray-icon-provider.js
@@ -0,0 +1,100 @@
+import path from 'path';
+import { systemPreferences } from 'electron';
+import { TrayAnimation } from './tray-animation';
+
+const menubarIcons = {
+ base: path.join(path.resolve(__dirname, '..'), 'assets/images/menubar icons'),
+ spinner: {
+ light: 'light ui/spinner/spinner-{s}-light.png',
+ dark: 'dark ui/spinner/spinner-{s}-dark.png'
+ },
+ lock: {
+ light: 'light ui/lock/lock-{s}-light.png',
+ dark: 'dark ui/lock/lock-{s}-dark.png'
+ }
+};
+
+/**
+ * Tray icon provider
+ *
+ * @export
+ * @class TrayIconProvider
+ */
+export default class TrayIconProvider {
+
+ /**
+ * Get lock animation
+ *
+ * @param {boolean} [isReverse=false] whether animation should be reversed
+ * @returns TrayIconAnimator
+ *
+ * @memberOf TrayIconProvider
+ */
+ lockAnimation(isReverse = false) {
+ let animation = TrayAnimation.fromFileSequence(this._lockPath, [1, 9]);
+ animation.speed = 100;
+ animation.reverse = isReverse;
+
+ return animation;
+ }
+
+ /**
+ * Get unlock animation
+ *
+ * @returns TrayIconAnimator
+ *
+ * @memberOf TrayIconProvider
+ */
+ unlockAnimation() {
+ return this.lockAnimation(true);
+ }
+
+ /**
+ * Get spinner animation
+ *
+ * @returns TrayIconAnimator
+ *
+ * @memberOf TrayIconProvider
+ */
+ spinnerAnimation() {
+ let animation = TrayAnimation.fromFileSequence(this._spinnerPath, [1, 9]);
+ animation.speed = 100;
+ animation.repeat = true;
+
+ return animation;
+ }
+
+ /**
+ * Pattern to spinner animation assets based on macOS menubar theme
+ *
+ * @readonly
+ *
+ * @memberOf TrayIconProvider
+ */
+ get _spinnerPath() {
+ return path.join(menubarIcons.base, menubarIcons.spinner[this._colorTheme]);
+ }
+
+ /**
+ * Pattern to lock/unlock animation assets based on macOS menubar theme
+ *
+ * @readonly
+ *
+ * @memberOf TrayIconProvider
+ */
+ get _lockPath() {
+ return path.join(menubarIcons.base, menubarIcons.lock[this._colorTheme]);
+ }
+
+ /**
+ * Current theme name based on macOS menubar theme.
+ *
+ * @readonly
+ *
+ * @memberOf TrayIconProvider
+ */
+ get _colorTheme() {
+ return systemPreferences.isDarkMode() ? 'dark' : 'light';
+ }
+
+} \ No newline at end of file
diff --git a/app/main.js b/app/main.js
index 582b14c68d..56cf8b6967 100644
--- a/app/main.js
+++ b/app/main.js
@@ -1,6 +1,7 @@
import path from 'path';
-import { app, crashReporter, BrowserWindow, ipcMain, Tray, Menu } from 'electron';
-import { TrayAnimation, TrayAnimator } from './lib/tray-animator';
+import { app, crashReporter, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron';
+import TrayIconManager from './lib/tray-icon-manager';
+import TrayIconProvider from './lib/tray-icon-provider';
// Override appData path to avoid collisions with old client
// New userData path, i.e on macOS: ~/Library/Application Support/mullvad.vpn
@@ -13,6 +14,7 @@ const isDevelopment = (process.env.NODE_ENV === 'development');
let window = null;
let tray = null;
let macEventMonitor = null;
+let trayIconManager = null;
const startTrayEventMonitor = (win) => {
if(process.platform === 'darwin') {
@@ -30,52 +32,8 @@ const stopTrayEventMonitor = () => {
}
};
-const menubarIcons = {
- base: path.join(__dirname, 'assets/images/menubar icons'),
- spinner: {
- light: 'light ui/spinner/spinner-{s}-light.png',
- dark: 'dark ui/spinner/spinner-{s}-dark.png'
- },
- lock: {
- light: 'light ui/lock/lock-{s}-light.png',
- dark: 'dark ui/lock/lock-{s}-dark.png'
- }
-};
-
-const spinnerPath = path.join(menubarIcons.base, menubarIcons.spinner.light);
-const lockPath = path.join(menubarIcons.base, menubarIcons.lock.light);
-
-let trayAnimator = null;
-
-ipcMain.on('changeTrayIcon', (event, name) => {
- if(!tray) { return; }
-
- trayAnimator && trayAnimator.stop();
- trayAnimator = null;
-
- console.log('changeTrayIcon: ' + name);
-
- if(name === 'securing') {
- let animation = TrayAnimation.fromFileSequence(spinnerPath, [1, 9]);
- animation.speed = 100;
- animation.repeat = true;
-
- trayAnimator = new TrayAnimator(tray, animation);
- trayAnimator.start();
- } else if(name === 'secured') {
- let animation = TrayAnimation.fromFileSequence(lockPath, [1, 9]);
- animation.speed = 100;
-
- trayAnimator = new TrayAnimator(tray, animation);
- trayAnimator.start();
- } else if(name === 'unsecured') {
- let animation = TrayAnimation.fromFileSequence(lockPath, [1, 9]);
- animation.speed = 100;
- animation.reverse = true;
-
- trayAnimator = new TrayAnimator(tray, animation);
- trayAnimator.start();
- }
+ipcMain.on('changeTrayIcon', (event, type) => {
+ trayIconManager.iconType = type;
});
// hide dock icon
@@ -171,12 +129,10 @@ const createWindow = () => {
});
window.on('show', () => {
- tray.setHighlightMode('always');
startTrayEventMonitor(window);
});
window.on('hide', () => {
- tray.setHighlightMode('never');
stopTrayEventMonitor();
});
@@ -198,10 +154,15 @@ const showWindow = () => {
};
const createTray = () => {
- tray = new Tray(path.join(__dirname, 'assets/images/tray-icon-default.png'));
+ tray = new Tray(nativeImage.createEmpty());
tray.on('right-click', toggleWindow);
tray.on('double-click', toggleWindow);
tray.on('click', toggleWindow);
+
+ // never highlight menu
+ tray.setHighlightMode('never');
+
+ trayIconManager = new TrayIconManager(tray, new TrayIconProvider());
};
crashReporter.start({
diff --git a/test/tray-animator.spec.js b/test/tray-animator.spec.js
index 275215413d..c6c1769160 100644
--- a/test/tray-animator.spec.js
+++ b/test/tray-animator.spec.js
@@ -1,5 +1,6 @@
import { expect } from 'chai';
-import { TrayAnimator, TrayAnimation } from '../app/lib/tray-animator';
+import { TrayAnimator, } from '../app/lib/tray-animator';
+import { TrayAnimation } from '../app/lib/tray-animation';
import { nativeImage } from 'electron';
describe('lib/tray-animator', function() {