diff --git a/.babelrc b/.babelrc deleted file mode 100644 index c13c5f62..00000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/.gitignore b/.gitignore index 24134bb3..12286f89 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ DerivedData/ !default.mode2v3 *.perspectivev3 !default.perspectivev3 -xcuserdata/ *.moved-aside *.xccheckout *.xcscmblueprint @@ -23,3 +22,5 @@ xcuserdata/ *.dSYM.zip *.dSYM installer/mac/build/ +installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin.xcodeproj/xcuserdata +installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin.xcodeproj/project.xcworkspace/xcuserdata \ No newline at end of file diff --git a/installer/README.txt b/installer/README.txt deleted file mode 100644 index a52f4fbe..00000000 --- a/installer/README.txt +++ /dev/null @@ -1 +0,0 @@ -The .aip file located here is produced by 'Advanced Installer' (http://www.advancedinstaller.com/). Running this file through advanced installer will generate a Microsoft Windows MSI file. \ No newline at end of file diff --git a/installer/mac/SymphonySettingsPlugin.bundle/Contents/Info.plist b/installer/mac/SymphonySettingsPlugin.bundle/Contents/Info.plist index 73a6d98a..c8bfce3c 100644 --- a/installer/mac/SymphonySettingsPlugin.bundle/Contents/Info.plist +++ b/installer/mac/SymphonySettingsPlugin.bundle/Contents/Info.plist @@ -3,7 +3,7 @@ BuildMachineOSBuild - 16E195 + 16F73 CFBundleDevelopmentRegion en CFBundleExecutable diff --git a/installer/mac/SymphonySettingsPlugin.bundle/Contents/MacOS/SymphonySettingsPlugin b/installer/mac/SymphonySettingsPlugin.bundle/Contents/MacOS/SymphonySettingsPlugin index 24055ded..0f7898cd 100755 Binary files a/installer/mac/SymphonySettingsPlugin.bundle/Contents/MacOS/SymphonySettingsPlugin and b/installer/mac/SymphonySettingsPlugin.bundle/Contents/MacOS/SymphonySettingsPlugin differ diff --git a/installer/mac/SymphonySettingsPlugin.bundle/Contents/Resources/Base.lproj/MyInstallerPane.nib b/installer/mac/SymphonySettingsPlugin.bundle/Contents/Resources/Base.lproj/MyInstallerPane.nib index 2f92f7c3..896e97e7 100644 Binary files a/installer/mac/SymphonySettingsPlugin.bundle/Contents/Resources/Base.lproj/MyInstallerPane.nib and b/installer/mac/SymphonySettingsPlugin.bundle/Contents/Resources/Base.lproj/MyInstallerPane.nib differ diff --git a/installer/mac/SymphonySettingsPlugin.bundle/Contents/_CodeSignature/CodeResources b/installer/mac/SymphonySettingsPlugin.bundle/Contents/_CodeSignature/CodeResources index 6bc20d9c..b9b540b5 100644 --- a/installer/mac/SymphonySettingsPlugin.bundle/Contents/_CodeSignature/CodeResources +++ b/installer/mac/SymphonySettingsPlugin.bundle/Contents/_CodeSignature/CodeResources @@ -6,7 +6,7 @@ Resources/Base.lproj/MyInstallerPane.nib - 3HG8UEw6WFkbwFZjfhIU/OzjLks= + G7x4W+5j1tDqlKgoKCr5helllTU= Resources/InstallerSections.plist @@ -37,7 +37,7 @@ hash2 - WevXKKGDjV20uPQvqf0GCvSehTCXdBwwHhIijv6ffPg= + 5C6LFVcm159sGwREu78C3xnjlAHRN9oglcYR3P6rmhM= Resources/InstallerSections.plist diff --git a/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin.xcodeproj/project.xcworkspace/xcuserdata/vishwas.xcuserdatad/UserInterfaceState.xcuserstate b/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin.xcodeproj/project.xcworkspace/xcuserdata/vishwas.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 9597fec9..00000000 Binary files a/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin.xcodeproj/project.xcworkspace/xcuserdata/vishwas.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ diff --git a/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin/Base.lproj/MyInstallerPane.xib b/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin/Base.lproj/MyInstallerPane.xib index 7cdee64b..f866b69c 100644 --- a/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin/Base.lproj/MyInstallerPane.xib +++ b/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin/Base.lproj/MyInstallerPane.xib @@ -1,5 +1,5 @@ - + @@ -19,6 +19,7 @@ + @@ -26,34 +27,34 @@ - + - + - + - + - + - + - + - + - - - diff --git a/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin/MyInstallerPane.h b/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin/MyInstallerPane.h index c7fbaf7b..a942401e 100644 --- a/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin/MyInstallerPane.h +++ b/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin/MyInstallerPane.h @@ -11,5 +11,6 @@ @property (weak) IBOutlet NSButton *minimizeOnCloseCheckBox; @property (weak) IBOutlet NSButton *autoLaunchCheckBox; +@property (weak) IBOutlet NSTextField *podUrlTextBox; @end diff --git a/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin/MyInstallerPane.m b/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin/MyInstallerPane.m index f2a1bc42..ae6a99f4 100644 --- a/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin/MyInstallerPane.m +++ b/installer/mac/SymphonySettingsPlugin/SymphonySettingsPlugin/MyInstallerPane.m @@ -14,13 +14,12 @@ return [[NSBundle bundleForClass:[self class]] localizedStringForKey:@"PaneTitle" value:nil table:nil]; } -- (IBAction)capturePodUrl:(NSTextField *)sender { +- (void)willExitPane:(InstallerSectionDirection)dir { // Set the default protocol to https NSString *protocol = @"https://"; - // Capture the pod url entered by the user - NSString *podUrl = sender.stringValue; + NSString *podUrl = [_podUrlTextBox stringValue]; // If the pod url is empty, by default, set it to my.symphony.com if ([podUrl length] == 0) { diff --git a/installer/mac/background.png b/installer/mac/background.png new file mode 100644 index 00000000..c3ce109d Binary files /dev/null and b/installer/mac/background.png differ diff --git a/installer/mac/intro.html b/installer/mac/intro.html new file mode 100644 index 00000000..2130b59a --- /dev/null +++ b/installer/mac/intro.html @@ -0,0 +1,11 @@ + + + + Welcome to the Symphony Installer + + +
+
+

   We'll walk you through the steps to install Symphony

+ + \ No newline at end of file diff --git a/installer/mac/postinstall.sh b/installer/mac/postinstall.sh index f9feb617..e2fb34ff 100755 --- a/installer/mac/postinstall.sh +++ b/installer/mac/postinstall.sh @@ -16,5 +16,27 @@ sed -i "" -E "s#\"url\" ?: ?\".*\"#\"url\"\: \"$pod_url\"#g" $newPath sed -i "" -E "s#\"minimizeOnClose\" ?: ?([Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee])#\"minimizeOnClose\":\ $minimize_on_close#g" $newPath sed -i "" -E "s#\"launchOnStartup\" ?: ?([Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee])#\"launchOnStartup\":\ $launch_on_startup#g" $newPath +## Add app to login items +if [ $launch_on_startup == true ]; then +cat > ~/Library/LaunchAgents/com.symphony.symphony-desktop.agent.plist << EOT + + + + + Label + com.symphony.symphony-desktop.agent + ProgramArguments + + $installPath/Symphony.app/Contents/MacOS/Symphony + + RunAtLoad + + + +EOT +else +launchctl unload ~/Library/LaunchAgents/com.symphony.symphony-desktop.agent.plist +fi + ## Remove the temp settings file created ## rm -f $tempFilePath diff --git a/installer/mac/symphony-mac-packager.pkgproj b/installer/mac/symphony-mac-packager.pkgproj index 73560144..76212447 100644 --- a/installer/mac/symphony-mac-packager.pkgproj +++ b/installer/mac/symphony-mac-packager.pkgproj @@ -494,12 +494,12 @@ BACKGROUND_PATH PATH - /Users/vishwas/Desktop/background.jpg + background.png PATH_TYPE - 0 + 3 CUSTOM - 0 + 1 SCALING 1
@@ -568,14 +568,6 @@ LIST_TITLE_KEY InstallerSectionTitle
- - ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS - ICPresentationViewDestinationSelectController - INSTALLER_PLUGIN - TargetSelect - LIST_TITLE_KEY - InstallerSectionTitle - ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS ICPresentationViewInstallerPluginController @@ -584,9 +576,17 @@ PATH SymphonySettingsPlugin.bundle PATH_TYPE - 1 + 3 + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewDestinationSelectController + INSTALLER_PLUGIN + TargetSelect + LIST_TITLE_KEY + InstallerSectionTitle + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS ICPresentationViewInstallationTypeController @@ -615,7 +615,19 @@ INTRODUCTION LOCALIZATIONS - + + + LANGUAGE + English + VALUE + + PATH + intro.html + PATH_TYPE + 3 + + + LICENSE diff --git a/installer/Assets/Banner.jpg b/installer/win/Assets/Banner.jpg similarity index 100% rename from installer/Assets/Banner.jpg rename to installer/win/Assets/Banner.jpg diff --git a/installer/Assets/Banner.png b/installer/win/Assets/Banner.png similarity index 100% rename from installer/Assets/Banner.png rename to installer/win/Assets/Banner.png diff --git a/installer/Assets/Tabloid.jpg b/installer/win/Assets/Tabloid.jpg similarity index 100% rename from installer/Assets/Tabloid.jpg rename to installer/win/Assets/Tabloid.jpg diff --git a/installer/Assets/Tabloid.png b/installer/win/Assets/Tabloid.png similarity index 100% rename from installer/Assets/Tabloid.png rename to installer/win/Assets/Tabloid.png diff --git a/installer/win/README.md b/installer/win/README.md new file mode 100644 index 00000000..3e44a941 --- /dev/null +++ b/installer/win/README.md @@ -0,0 +1,2 @@ +The .aip file located here is produced by 'Advanced Installer' (http://www.advancedinstaller.com/). +Running this file through advanced installer will generate a Microsoft Windows MSI file. diff --git a/installer/Symphony-x64.aip b/installer/win/Symphony-x64.aip similarity index 87% rename from installer/Symphony-x64.aip rename to installer/win/Symphony-x64.aip index 79a41e61..aec70128 100644 --- a/installer/Symphony-x64.aip +++ b/installer/win/Symphony-x64.aip @@ -73,81 +73,81 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -359,9 +359,10 @@ + - + @@ -383,6 +384,7 @@ + diff --git a/js/activityDetection/activityDetection.js b/js/activityDetection/activityDetection.js index e5dab4bc..7b9b8283 100644 --- a/js/activityDetection/activityDetection.js +++ b/js/activityDetection/activityDetection.js @@ -2,6 +2,8 @@ const systemIdleTime = require('@paulcbetts/system-idle-time'); const throttle = require('../utils/throttle'); +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); let maxIdleTime; let activityWindow; @@ -75,6 +77,7 @@ function sendActivity() { */ function send(data) { if (activityWindow && data) { + log.send(logLevels.INFO, 'activity occurred at time= ' + new Date().toUTCString()); activityWindow.send('activity', { systemIdleTime: data.systemIdleTime }); @@ -84,11 +87,14 @@ function send(data) { function setActivityWindow(period, win) { maxIdleTime = period; activityWindow = win; + // Initiate activity detection to monitor user activity status + initiateActivityDetection(); } module.exports = { send: send, setActivityWindow: setActivityWindow, activityDetection: activityDetection, + monitorUserActivity: monitorUserActivity, // Exporting this for unit test initiateActivityDetection: initiateActivityDetection }; diff --git a/js/badgeCount.js b/js/badgeCount.js index 0ee4264d..1f1cf62c 100644 --- a/js/badgeCount.js +++ b/js/badgeCount.js @@ -7,9 +7,12 @@ const nativeImage = electron.nativeImage; const { isMac } = require('./utils/misc.js'); const windowMgr = require('./windowMgr.js'); const maxCount = 1e8; +const log = require('./log.js'); +const logLevels = require('./enums/logLevels.js'); function show(count) { if (typeof count !== 'number') { + log.send(logLevels.WARN, 'badgeCount: invalid func arg, must be a number: ' + count); return; } diff --git a/js/config.js b/js/config.js index 89554d52..7c92c87d 100644 --- a/js/config.js +++ b/js/config.js @@ -164,4 +164,9 @@ function saveUserConfig(fieldName, newValue, oldConfig) { }); } -module.exports = { getConfigField, updateConfigField, configFileName }; +module.exports = { + getConfigField, + updateConfigField, + configFileName, + saveUserConfig // Exporting this for unit tests +}; diff --git a/js/desktopCapturer/getSources.js b/js/desktopCapturer/getSources.js index 79c6b164..ac7fc336 100644 --- a/js/desktopCapturer/getSources.js +++ b/js/desktopCapturer/getSources.js @@ -14,6 +14,7 @@ var { ipcRenderer } = require('electron'); + var nextId = 0; var includes = [].includes; diff --git a/js/dialogs/showCertError.js b/js/dialogs/showCertError.js index ce3874f6..3e2795a3 100644 --- a/js/dialogs/showCertError.js +++ b/js/dialogs/showCertError.js @@ -2,6 +2,9 @@ const electron = require('electron'); +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); + let ignoreAllCertErrors = false; /** @@ -21,6 +24,8 @@ electron.app.on('certificate-error', function(event, webContents, url, error, return; } + log.send(logLevels.WARNING, 'Certificate error: ' + error + ' for url: ' + url); + const browserWin = electron.BrowserWindow.fromWebContents(webContents); const buttonId = electron.dialog.showMessageBox(browserWin, { type: 'warning', diff --git a/js/dialogs/showLoadError.js b/js/dialogs/showLoadError.js index ec58e22a..be83d535 100644 --- a/js/dialogs/showLoadError.js +++ b/js/dialogs/showLoadError.js @@ -2,6 +2,9 @@ const electron = require('electron'); +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); + /** * Show dialog pinned to given window when loading error occurs * @param {BrowserWindow} win Window to host dialog @@ -34,6 +37,9 @@ function showLoadFailure(win, url, errorDesc, errorCode, retryCallback) { message: msg }, response); + log.send(logLevels.WARNING, 'Load failure msg: ' + errorDesc + + ' errorCode: ' + errorCode + ' for url:' + url); + // async handle of user input function response(buttonId) { // retry if hitting butotn index 0 (i.e., reload) diff --git a/js/log.js b/js/log.js index 26ec6784..a059f5fc 100644 --- a/js/log.js +++ b/js/log.js @@ -1,28 +1,87 @@ 'use strict'; -let logWindow; +const getCmdLineArg = require('./utils/getCmdLineArg.js') -/** - * Send log messages from main process to logger hosted by - * renderer process. Allows main process to use logger - * provided by JS. - * @param {enum} level enum from ./enums/LogLevel.js - * @param {string} details msg to be logged - */ -function send(level, details) { - if (logWindow && level && details) { - logWindow.send('log', { +const MAX_LOG_QUEUE_LENGTH = 100; + +class Logger { + constructor() { + // browser window that has registered a logger + this.logWindow = null; + + // holds log messages received before logger has been registered. + this.logQueue = []; + } + + /** + * Send log messages from main process to logger hosted by + * renderer process. Allows main process to use logger + * provided by JS. + * @param {enum} level enum from ./enums/LogLevel.js + * @param {string} details msg to be logged + */ + send(level, details) { + if (!level || !details) { + return; + } + + let logMsg = { level: level, - details: details - }); + details: details, + startTime: Date.now() + }; + + if (this.logWindow) { + this.logWindow.send('log', { + msgs: [ logMsg ] + }); + } else { + // store log msgs for later when (if) we get logger registered + this.logQueue.push(logMsg); + // don't store more than 100 msgs. keep most recent log msgs. + if (this.logQueue.length > MAX_LOG_QUEUE_LENGTH) { + this.logQueue.shift(); + } + } + } + + setLogWindow(win) { + this.logWindow = win; + + if (this.logWindow) { + var logMsg = {}; + + if (Array.isArray(this.logQueue)) { + logMsg.msgs = this.logQueue; + } + + // configure desired log level and send pending log msgs + let logLevel = getCmdLineArg(process.argv, '--logLevel='); + if (logLevel) { + let level = logLevel.split('=')[1]; + if (level) { + logMsg.logLevel = level; + } + } + + if (getCmdLineArg(process.argv, '--enableConsoleLogging')) { + logMsg.showInConsole = true; + } + + if (Object.keys(logMsg).length) { + this.logWindow.send('log', logMsg); + } + + this.logQueue = []; + } } } -function setLogWindow(win) { - logWindow = win; -} +var loggerInstance = new Logger(); +// Logger class is only exposed for testing purposes. module.exports = { - send: send, - setLogWindow: setLogWindow -}; + Logger: Logger, + send: loggerInstance.send.bind(loggerInstance), + setLogWindow: loggerInstance.setLogWindow.bind(loggerInstance) +} diff --git a/js/main.js b/js/main.js index 107ee0da..0beb1093 100644 --- a/js/main.js +++ b/js/main.js @@ -7,8 +7,9 @@ const squirrelStartup = require('electron-squirrel-startup'); const AutoLaunch = require('auto-launch'); const urlParser = require('url'); const { getConfigField } = require('./config.js'); -const { isDevEnv } = require('./utils/misc.js'); +const { isMac, isDevEnv } = require('./utils/misc.js'); const protocolHandler = require('./protocolHandler'); +const getCmdLineArg = require('./utils/getCmdLineArg.js') const crashReporter = require('./crashReporter'); @@ -41,8 +42,8 @@ const shouldQuit = app.makeSingleInstance((argv) => { processProtocolAction(argv); }); -// quit if another instance is already running -if (shouldQuit) { +// quit if another instance is already running, ignore for dev env +if (!isDevEnv && shouldQuit) { app.quit(); } @@ -88,27 +89,18 @@ function setupThenOpenMainWindow() { isAppAlreadyOpen = true; - let installMode = false; - // allows installer to launch app and set auto startup mode then // immediately quit. - process.argv.some((val) => { - - let flag = '--install'; - if (val === flag) { - installMode = true; - getConfigField('launchOnStartup') - .then(setStartup) - .then(app.quit) - .catch(app.quit); - } - - return false; - }); - - if (installMode === false) { - getUrlAndCreateMainWindow(); + let hasInstallFlag = getCmdLineArg(process.argv, '--install', true); + if (!isMac && hasInstallFlag) { + getConfigField('launchOnStartup') + .then(setStartup) + .then(app.quit) + .catch(app.quit); + return; } + + getUrlAndCreateMainWindow(); } function setStartup(lStartup){ @@ -129,14 +121,9 @@ function setStartup(lStartup){ function getUrlAndCreateMainWindow() { // for dev env allow passing url argument if (isDevEnv) { - let url; - process.argv.forEach((val) => { - if (val.startsWith('--url=')) { - url = val.substr(6); - } - }); + let url = getCmdLineArg(process.argv, '--url=') if (url) { - windowMgr.createMainWindow(url); + windowMgr.createMainWindow(url.substr(6)); return; } } @@ -160,6 +147,7 @@ function createWin(urlFromConfig) { slahes: true, pathname: parsedUrl.href }); + windowMgr.createMainWindow(url); } @@ -190,16 +178,7 @@ function processProtocolAction(argv) { return; } - let protocolUri; - - for (let i = 0; i < argv.length; i++) { - - if (argv[i].startsWith("symphony://")) { - protocolUri = argv[i]; - break; - } - - } + let protocolUri = getCmdLineArg(argv, 'symphony://'); if (protocolUri) { diff --git a/js/mainApiMgr.js b/js/mainApiMgr.js index 14811c89..42098a46 100644 --- a/js/mainApiMgr.js +++ b/js/mainApiMgr.js @@ -8,6 +8,7 @@ const electron = require('electron'); const windowMgr = require('./windowMgr.js'); const log = require('./log.js'); +const logLevels = require('./enums/logLevels'); const activityDetection = require('./activityDetection/activityDetection'); const badgeCount = require('./badgeCount.js'); const protocolHandler = require('./protocolHandler'); @@ -39,9 +40,7 @@ function isValidWindow(event) { } if (!result) { - /* eslint-disable no-console */ - console.log('invalid window try to perform action, ignoring action'); - /* eslint-enable no-console */ + log.send(logLevels.WARN, 'invalid window try to perform action, ignoring action'); } return result; diff --git a/js/menus/menuTemplate.js b/js/menus/menuTemplate.js index 2d8b1109..ccf5c770 100644 --- a/js/menus/menuTemplate.js +++ b/js/menus/menuTemplate.js @@ -4,6 +4,9 @@ const electron = require('electron'); const { getConfigField, updateConfigField } = require('../config.js'); const AutoLaunch = require('auto-launch'); const isMac = require('../utils/misc.js').isMac; +const childProcess = require('child_process'); +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); var minimizeOnClose = false; var launchOnStartup = false; @@ -14,6 +17,7 @@ var symphonyAutoLauncher = new AutoLaunch({ name: 'Symphony', path: process.execPath, }); +let launchAgentPath = '~/Library/LaunchAgents/com.symphony.symphony-desktop.agent.plist'; const template = [ { @@ -184,17 +188,43 @@ function getTemplate(app) { checked: launchOnStartup, click: function (item) { if (item.checked){ - symphonyAutoLauncher.enable() - .catch(function (err) { - let title = 'Error setting AutoLaunch configuration'; - electron.dialog.showErrorBox(title, title + ': ' + err); - }); + if (isMac){ + // TODO: Need to change this implementation to AutoLaunch once they fix this issue -> + // https://github.com/Teamwork/node-auto-launch/issues/28 + childProcess.exec(`launchctl load ${launchAgentPath}`, (err) => { + if (err){ + let title = 'Error setting AutoLaunch configuration'; + log.send(logLevels.ERROR, 'MenuTemplate: ' + title + ': process error ' + err); + electron.dialog.showErrorBox(title, 'Please try reinstalling the application'); + } + }); + } else { + symphonyAutoLauncher.enable() + .catch(function (err) { + let title = 'Error setting AutoLaunch configuration'; + log.send(logLevels.ERROR, 'MenuTemplate: ' + title + ': auto launch error ' + err); + electron.dialog.showErrorBox(title, title + ': ' + err); + }); + } } else { - symphonyAutoLauncher.disable() - .catch(function (err) { - let title = 'Error setting AutoLaunch configuration'; - electron.dialog.showErrorBox(title, title + ': ' + err); - }); + if (isMac){ + // TODO: Need to change this implementation to AutoLaunch once they fix this issue -> + // https://github.com/Teamwork/node-auto-launch/issues/28 + childProcess.exec(`launchctl unload ${launchAgentPath}`, (err) => { + if (err){ + let title = 'Error disabling AutoLaunch configuration'; + log.send(logLevels.ERROR, 'MenuTemplate: ' + title + ': process error ' + err); + electron.dialog.showErrorBox(title, 'Please try reinstalling the application'); + } + }); + } else { + symphonyAutoLauncher.disable() + .catch(function (err) { + let title = 'Error setting AutoLaunch configuration'; + log.send(logLevels.ERROR, 'MenuTemplate: ' + title + ': auto launch error ' + err); + electron.dialog.showErrorBox(title, title + ': ' + err); + }); + } } launchOnStartup = item.checked; updateConfigField('launchOnStartup', launchOnStartup); @@ -235,6 +265,7 @@ function setCheckboxValues(){ minimizeOnClose = mClose; }).catch(function (err){ let title = 'Error loading configuration'; + log.send(logLevels.ERROR, 'MenuTemplate: error getting config field minimizeOnClose, error: ' + err); electron.dialog.showErrorBox(title, title + ': ' + err); }); @@ -242,6 +273,7 @@ function setCheckboxValues(){ launchOnStartup = lStartup; }).catch(function (err){ let title = 'Error loading configuration'; + log.send(logLevels.ERROR, 'MenuTemplate: error getting config field launchOnStartup, error: ' + err); electron.dialog.showErrorBox(title, title + ': ' + err); }); } diff --git a/js/notify/AnimationQueue.js b/js/notify/AnimationQueue.js index 49e6bbdc..abfa5169 100644 --- a/js/notify/AnimationQueue.js +++ b/js/notify/AnimationQueue.js @@ -1,5 +1,8 @@ 'use strict'; +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); + // One animation at a time const AnimationQueue = function(options) { this.options = options; @@ -27,6 +30,8 @@ AnimationQueue.prototype.animate = function(object) { } }.bind(this)) .catch(function(err) { + log.send(logLevels.ERROR, 'animationQueue: encountered an error: ' + err + + ' with stack trace:' + err.stack); /* eslint-disable no-console */ console.error('animation queue encountered an error: ' + err + ' with stack trace:' + err.stack); diff --git a/js/notify/electron-notify-preload.js b/js/notify/electron-notify-preload.js index 2768b99e..dfa6bcf3 100644 --- a/js/notify/electron-notify-preload.js +++ b/js/notify/electron-notify-preload.js @@ -8,6 +8,8 @@ // const electron = require('electron'); const ipc = electron.ipcRenderer; +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); function setStyle(config) { // Style it @@ -53,7 +55,8 @@ function setContents(event, notificationObj) { audio.play() } } catch (e) { - log('electron-notify: ERROR could not find sound file: ' + notificationObj.sound.replace('file://', ''), e, e.stack); + log.send(logLevels.ERROR, 'electron-notify: ERROR could not find sound file: ' + + notificationObj.sound.replace('file://', ''), e, e.stack); } } @@ -140,9 +143,3 @@ function reset() { ipc.on('electron-notify-set-contents', setContents) ipc.on('electron-notify-load-config', loadConfig) ipc.on('electron-notify-reset', reset) - -function log() { - /* eslint-disable no-console */ - console.log.apply(console, arguments) - /* eslint-enable no-console */ -} diff --git a/js/notify/electron-notify.js b/js/notify/electron-notify.js index 757b5ad0..eabf5222 100644 --- a/js/notify/electron-notify.js +++ b/js/notify/electron-notify.js @@ -15,6 +15,8 @@ const electron = require('electron'); const app = electron.app; const BrowserWindow = electron.BrowserWindow; const ipc = electron.ipcMain; +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); // maximum number of notifications that can be queued, after limit is // reached then error func callback will be invoked. @@ -158,7 +160,7 @@ function getTemplatePath() { try { fs.statSync(templatePath).isFile(); } catch (err) { - log('electron-notify: Could not find template ("' + templatePath + '").'); + log.send(logLevels.ERROR, 'electron-notify: Could not find template ("' + templatePath + '").'); } config.templatePath = 'file://' + templatePath; return config.templatePath; @@ -166,7 +168,7 @@ function getTemplatePath() { function calcDimensions() { const vertSpaceBetweenNotf = 8; - + // Calc totalHeight & totalWidth config.totalHeight = config.height + vertSpaceBetweenNotf; config.totalWidth = config.width @@ -254,7 +256,7 @@ function notify(notification) { }) return notf.id } - log('electron-notify: ERROR notify() only accepts a single object with notification parameters.') + log.send(logLevels.ERROR, 'electron-notify: ERROR notify() only accepts a single object with notification parameters.'); return null; } @@ -272,6 +274,7 @@ function showNotification(notificationObj) { id: notificationObj.id, error: 'max notification queue size reached: ' + MAX_QUEUE_SIZE }); + log.send(logLevels.INFO, 'showNotification: max notification queue size reached: ' + MAX_QUEUE_SIZE); }, 0); } resolve(); @@ -646,13 +649,5 @@ function cleanUpInactiveWindow() { inactiveWindows = []; } -function log() { - if (config.logging === true) { - /* eslint-disable no-console */ - console.log.apply(console, arguments); - /* eslint-enable no-console */ - } -} - module.exports.notify = notify module.exports.reset = setupConfig diff --git a/js/notify/notifyImpl.js b/js/notify/notifyImpl.js index 6b6dac5b..995a1554 100644 --- a/js/notify/notifyImpl.js +++ b/js/notify/notifyImpl.js @@ -2,7 +2,8 @@ const EventEmitter = require('events'); const { notify } = require('./electron-notify.js'); - +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); /** * implementation for notifications interface, * wrapper around electron-notify. @@ -27,6 +28,8 @@ class Notify { * } */ constructor(title, options) { + log.send(logLevels.INFO, 'creating notification, text=' + options.body); + let emitter = new EventEmitter(); this.emitter = Queue(emitter); @@ -44,10 +47,13 @@ class Notify { onErrorFunc: onError.bind(this) }); + log.send(logLevels.INFO, 'created notification, id=' + this._id + ', text=' + options.body); + this._data = options.data || null; function onShow(arg) { if (arg.id === this._id) { + log.send(logLevels.INFO, 'showing notification, id=' + this._id); this.emitter.queue('show', { target: this }); @@ -57,6 +63,7 @@ class Notify { function onClick(arg) { if (arg.id === this._id) { + log.send(logLevels.INFO, 'clicking notification, id=' + this._id); this.emitter.queue('click', { target: this }); @@ -65,6 +72,7 @@ class Notify { function onClose(arg) { if (arg.id === this._id || arg.event === 'close-all') { + log.send(logLevels.INFO, 'closing notification, id=' + this._id); this.emitter.queue('close', { target: this }); @@ -76,6 +84,8 @@ class Notify { if (arg.id === this._id) { // don't raise error event if handler doesn't exist, node // will throw an exception + log.send(logLevels.ERROR, 'error for notification, id=' + this._id + + ' error=' + (arg && arg.error)); if (this.emitter.eventNames().includes('error')) { this.emitter.queue('error', arg.error || 'notification error'); } diff --git a/js/preload/preloadMain.js b/js/preload/preloadMain.js index e3565bef..6bb4ba7e 100644 --- a/js/preload/preloadMain.js +++ b/js/preload/preloadMain.js @@ -20,6 +20,8 @@ const apiName = apiEnums.apiName; const getMediaSources = require('../desktopCapturer/getSources'); const crashReporter = require('../crashReporter'); +const nodeURL = require('url'); + // hold ref so doesn't get GC'ed const local = { ipcRenderer: ipcRenderer @@ -52,6 +54,25 @@ function createAPI() { return; } + // bug in electron is preventing using event 'will-navigate' from working + // in sandboxed environment. https://github.com/electron/electron/issues/8841 + // so in the mean time using this code below to block clicking on A tags. + // A tags are allowed if they include href='_blank', this cause 'new-window' + // event to be received which is handled properly in windowMgr.js + window.addEventListener('beforeunload', function(event) { + var newUrl = document.activeElement && document.activeElement.href; + if (newUrl) { + var currHostName = window.location.hostname; + var parsedNewUrl = nodeURL.parse(newUrl); + var parsedNewUrlHostName = parsedNewUrl && parsedNewUrl.hostname; + if (currHostName !== parsedNewUrlHostName) { + /* eslint-disable no-param-reassign */ + event.returnValue = 'false'; + /* eslint-enable no-param-reassign */ + } + } + }); + // note: window.open from main window (if in the same domain) will get // api access. window.open in another domain will be opened in the default // browser (see: handler for event 'new-window' in windowMgr.js) @@ -205,7 +226,6 @@ function createAPI() { * for interface: see documentation in desktopCapturer/getSources.js */ getMediaSources: getMediaSources - }; // add support for both ssf and SYM_API name-space. @@ -215,8 +235,8 @@ function createAPI() { // listen for log message from main process local.ipcRenderer.on('log', (event, arg) => { - if (local.logger && arg && arg.level && arg.details) { - local.logger(arg.level, arg.details); + if (arg && local.logger) { + local.logger(arg.msgs || [], arg.logLevel, arg.showInConsole); } }); @@ -294,7 +314,6 @@ function createAPI() { * @type {String} arg - the protocol url */ local.ipcRenderer.on('protocol-action', (event, arg) => { - if (local.processProtocolAction && arg) { local.processProtocolAction(arg); } diff --git a/js/protocolHandler/index.js b/js/protocolHandler/index.js index 74a0640d..e4b73870 100644 --- a/js/protocolHandler/index.js +++ b/js/protocolHandler/index.js @@ -1,5 +1,8 @@ 'use strict'; +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); + let protocolWindow; let protocolUrl; @@ -8,7 +11,8 @@ let protocolUrl; * @param {String} uri - the uri opened in the format 'symphony://...' */ function processProtocolAction(uri) { - if (protocolWindow && uri && uri.startsWith("symphony://")) { + log.send(logLevels.INFO, 'protocol action, uri=' + uri); + if (protocolWindow && uri && uri.startsWith('symphony://')) { protocolWindow.send('protocol-action', uri); } } diff --git a/js/screenSnippet/ScreenSnippet.js b/js/screenSnippet/ScreenSnippet.js index af70e3bc..64163d9c 100644 --- a/js/screenSnippet/ScreenSnippet.js +++ b/js/screenSnippet/ScreenSnippet.js @@ -8,6 +8,8 @@ const os = require('os'); const path = require('path'); const { isMac, isDevEnv } = require('../utils/misc.js'); +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); // static ref to child process, only allow one screen snippet at time, so // hold ref to prev, so can kill before starting next snippet. @@ -34,6 +36,8 @@ class ScreenSnippet { return new Promise((resolve, reject) => { let captureUtil, captureUtilArgs; + log.send(logLevels.INFO, 'ScreenSnippet: starting screen capture'); + let tmpFilename = 'symphonyImage-' + Date.now() + '.jpg'; let tmpDir = os.tmpdir(); @@ -60,6 +64,8 @@ class ScreenSnippet { captureUtilArgs = [ outputFileName ]; } + log.send(logLevels.INFO, 'ScreenSnippet: starting screen capture util: ' + captureUtil + ' with args=' + captureUtilArgs); + // only allow one screen capture at a time. if (child) { child.kill(); @@ -118,13 +124,10 @@ class ScreenSnippet { fs.unlink(outputFileName, function(removeErr) { // note: node complains if calling async // func without callback. - /* eslint-disable no-console */ if (removeErr) { - console.error( - 'error removing temp snippet file: ' + + log.send(logLevels.ERROR, 'ScreenSnippet: error removing temp snippet file: ' + outputFileName + ', err:' + removeErr); } - /* eslint-enable no-console */ }); } }); diff --git a/js/utils/getCmdLineArg.js b/js/utils/getCmdLineArg.js new file mode 100644 index 00000000..b557dac7 --- /dev/null +++ b/js/utils/getCmdLineArg.js @@ -0,0 +1,29 @@ +'use strict'; + +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); + +/** + * Search given argv for argName using exact match or starts with. + * @param {Array} argv Array of strings + * @param {String} argName Arg name to search for. + * @param {bool} exactMatch If true then look for exact match otherwise + * try finding arg that starts with argName. + * @return {String} If found, returns the arg, otherwise null. + */ +function getCmdLineArg(argv, argName, exactMatch) { + if (!Array.isArray(argv)) { + log.send(logLevels.WARN, 'getCmdLineArg: TypeError invalid func arg, must be an array: '+ argv); + return null; + } + + for (let i = 0, len = argv.length; i < len; i++) { + if ((exactMatch && argv[i] === argName) || + (!exactMatch && argv[i].startsWith(argName))) { + return argv[i]; + } + } + + return null; +} +module.exports = getCmdLineArg diff --git a/js/utils/getRegistry.js b/js/utils/getRegistry.js index b5010466..fdd9781a 100644 --- a/js/utils/getRegistry.js +++ b/js/utils/getRegistry.js @@ -2,6 +2,8 @@ const symphonyRegistry = '\\Software\\Symphony\\Symphony\\'; const { isMac } = require('./misc.js'); +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); var Registry = require('winreg'); var symphonyRegistryHKCU = new Registry({ @@ -32,6 +34,7 @@ var getRegistry = function (name) { //Try to get registry on HKEY_CURRENT_USER symphonyRegistryHKCU.get( name, function( err1, reg1 ) { if (!err1 && reg1 !==null && reg1.value) { + log.send(logLevels.WARN, 'getRegistry: Cannot find ' + name + ' Registry. Using HKCU'); resolve(reg1.value); return; } @@ -39,6 +42,7 @@ var getRegistry = function (name) { //Try to get registry on HKEY_LOCAL_MACHINE symphonyRegistryHKLM.get( name, function( err2, reg2 ) { if ( !err2 && reg2!==null && reg2.value) { + log.send(logLevels.WARN, 'getRegistry: Cannot find ' + name + ' Registry. Using HKLM'); resolve(reg2.value); return; } diff --git a/js/utils/throttle.js b/js/utils/throttle.js index cd7a2a08..6c74e88b 100644 --- a/js/utils/throttle.js +++ b/js/utils/throttle.js @@ -6,6 +6,12 @@ * @param {function} func function to invoke */ function throttle(throttleTime, func) { + if (typeof throttleTime !== 'number' || throttleTime <= 0) { + throw Error('throttle: invalid throttleTime arg, must be a number: ' + throttleTime); + } + if (typeof func !== 'function') { + throw Error('throttle: invalid func arg, must be a function: ' + func); + } let timer, lastInvoke = 0; return function() { let args = arguments; diff --git a/js/windowMgr.js b/js/windowMgr.js index 6ac43e33..2ad6bad4 100644 --- a/js/windowMgr.js +++ b/js/windowMgr.js @@ -17,8 +17,6 @@ const log = require('./log.js'); const logLevels = require('./enums/logLevels.js'); const notify = require('./notify/electron-notify.js'); -const activityDetection = require('./activityDetection/activityDetection.js'); - const throttle = require('./utils/throttle.js'); const { getConfigField, updateConfigField } = require('./config.js'); @@ -83,6 +81,8 @@ function doCreateMainWindow(initialUrl, initialBounds) { let title = 'Error loading configuration'; electron.dialog.showErrorBox(title, title + ': ' + err); }); + + log.send(logLevels.INFO, 'creating main window url: ' + url); let newWinOpts = { title: 'Symphony', @@ -150,10 +150,8 @@ function doCreateMainWindow(initialUrl, initialBounds) { } else { // removes all existing notifications when main window reloads notify.reset(); - log.send(logLevels.INFO, 'main window loaded url: ' + url); + log.send(logLevels.INFO, 'loaded main window url: ' + url); - // Initiate activity detection to monitor user activity status - activityDetection.initiateActivityDetection(); } }); @@ -212,7 +210,14 @@ function doCreateMainWindow(initialUrl, initialBounds) { mainWindow.on('closed', destroyAllWindows); - // open external links in default browser - a tag, window.open + // bug in electron is preventing this from working in sandboxed evt... + // https://github.com/electron/electron/issues/8841 + mainWindow.webContents.on('will-navigate', function(event, willNavUrl) { + event.preventDefault(); + openUrlInDefaultBrower(willNavUrl); + }); + + // open external links in default browser - a tag with href='_blank' or window.open mainWindow.webContents.on('new-window', function (event, newWinUrl, frameName, disposition, newWinOptions) { let newWinParsedUrl = getParsedUrl(newWinUrl); @@ -221,12 +226,9 @@ function doCreateMainWindow(initialUrl, initialBounds) { let newWinHost = newWinParsedUrl && newWinParsedUrl.host; let mainWinHost = mainWinParsedUrl && mainWinParsedUrl.host; - // if host url doesn't match then open in external browser - if (newWinHost !== mainWinHost) { - event.preventDefault(); - electron.shell.openExternal(newWinUrl); - } else if (disposition === 'foreground-tab' || - disposition === 'new-window') { + // only allow window.open to succeed is if coming from same hsot, + // otherwise open in default browser. + if (disposition === 'new-window' && newWinHost === mainWinHost) { // handle: window.open if (!frameName) { @@ -234,6 +236,8 @@ function doCreateMainWindow(initialUrl, initialBounds) { return; } + log.send(logLevels.INFO, 'creating pop-out window url: ' + newWinParsedUrl); + let x = 0; let y = 0; @@ -283,6 +287,8 @@ function doCreateMainWindow(initialUrl, initialBounds) { let browserWin = BrowserWindow.fromWebContents(webContents); if (browserWin) { + log.send(logLevels.INFO, 'loaded pop-out window url: ' + newWinParsedUrl); + browserWin.winName = frameName; browserWin.once('closed', function () { @@ -300,6 +306,9 @@ function doCreateMainWindow(initialUrl, initialBounds) { browserWin.on('resize', throttledBoundsChange); } }); + } else { + event.preventDefault(); + openUrlInDefaultBrower(newWinUrl) } }); @@ -323,17 +332,19 @@ function getMainWindow() { } function getWindowSizeAndPosition(window) { - let newPos = window.getPosition(); - let newSize = window.getSize(); + if (window) { + let newPos = window.getPosition(); + let newSize = window.getSize(); - if (newPos && newPos.length === 2 && - newSize && newSize.length === 2) { - return { - x: newPos[0], - y: newPos[1], - width: newSize[0], - height: newSize[1], - }; + if (newPos && newPos.length === 2 && + newSize && newSize.length === 2) { + return { + x: newPos[0], + y: newPos[1], + width: newSize[0], + height: newSize[1], + }; + } } return null; @@ -403,6 +414,12 @@ function sendChildWinBoundsChange(window) { } } +function openUrlInDefaultBrower(urlToOpen) { + if (urlToOpen) { + electron.shell.openExternal(urlToOpen); + } +} + module.exports = { createMainWindow: createMainWindow, getMainWindow: getMainWindow, diff --git a/package.json b/package.json index f2b1607a..549d7f9e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dist-mac": "npm run prebuild && build --mac", "dist-win": "npm run prebuild && build --win --x64", "dist-win-x86": "npm run prebuild && build --win --ia32", + "unpacked-mac": "npm run prebuild && build --mac --dir", "unpacked-win": "npm run prebuild && build --win --x64 --dir", "unpacked-win-x86": "npm run prebuild && build --win --ia32 --dir", "prebuild": "npm run lint && npm run test && npm run browserify-preload", @@ -68,11 +69,9 @@ "url": "https://support.symphony.com" }, "devDependencies": { - "babel-jest": "^19.0.0", - "babel-preset-es2015": "^6.24.0", "browserify": "^14.1.0", "cross-env": "^3.2.4", - "electron": "1.6.7", + "electron": "1.6.11", "electron-builder": "^13.9.0", "electron-builder-squirrel-windows": "^12.3.0", "electron-packager": "^8.5.2", diff --git a/tests/ProtocolHandler.test.js b/tests/ProtocolHandler.test.js index 7c3e9f59..b993cd90 100644 --- a/tests/ProtocolHandler.test.js +++ b/tests/ProtocolHandler.test.js @@ -7,6 +7,7 @@ const electron = require('./__mocks__/electron'); describe('protocol handler', function () { const url = 'symphony://?userId=100001'; + const nonProtocolUrl = 'sy://abc=123'; const mainProcess = electron.ipcMain; const protocolWindow = electron.ipcRenderer; @@ -41,6 +42,17 @@ describe('protocol handler', function () { }); + it('protocol handler open url should be called', function(done) { + + const spy = jest.spyOn(mainProcess, 'send'); + mainProcess.send('open-url', nonProtocolUrl); + + expect(spy).toHaveBeenCalled(); + + done(); + + }); + it('check protocol action should be called', function (done) { const spy = jest.spyOn(protocolHandler, 'checkProtocolAction'); @@ -58,4 +70,38 @@ describe('protocol handler', function () { }); + it('check protocol action should be called when we have an incorrect protocol url', function (done) { + + const spy = jest.spyOn(protocolHandler, 'checkProtocolAction'); + const setSpy = jest.spyOn(protocolHandler, 'setProtocolUrl'); + + protocolHandler.setProtocolUrl(nonProtocolUrl); + expect(setSpy).toHaveBeenCalledWith(nonProtocolUrl); + + protocolHandler.checkProtocolAction(); + expect(spy).toHaveBeenCalled(); + + expect(protocolHandler.getProtocolUrl()).toBeUndefined(); + + done(); + + }); + + it('check protocol action should be called when the protocol url is undefined', function(done) { + + const spy = jest.spyOn(protocolHandler, 'checkProtocolAction'); + const setSpy = jest.spyOn(protocolHandler, 'setProtocolUrl'); + + protocolHandler.setProtocolUrl(undefined); + expect(setSpy).toHaveBeenCalledWith(undefined); + + protocolHandler.checkProtocolAction(); + expect(spy).toHaveBeenCalled(); + + expect(protocolHandler.getProtocolUrl()).toBeUndefined(); + + done(); + + }); + }); \ No newline at end of file diff --git a/tests/activityDetection.test.js b/tests/activityDetection.test.js index cc8fe1f4..578768c4 100644 --- a/tests/activityDetection.test.js +++ b/tests/activityDetection.test.js @@ -1,10 +1,30 @@ -// const activityDetection = require('../js/activityDetection/activityDetection.js'); -// const electron = require('./__mocks__/electron'); +const electron = require('./__mocks__/electron'); +const childProcess = require('child_process'); -xdescribe('Tests for Activity Detection', function() { +let activityDetection; - beforeAll(function () { - activityDetection.setActivityWindow(120000, electron.ipcRenderer); +describe('Tests for Activity Detection', function() { + + var originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000; + + beforeAll(function (done) { + childProcess.exec('npm rebuild --runtime=electron --target=1.5.0 --disturl=https://atom.io/download/atom-shell --build-from-source', function (err) { + activityDetection = require('../js/activityDetection/activityDetection.js'); + activityDetection.setActivityWindow(120000, electron.ipcRenderer); + done(); + }); + }); + + beforeEach(function () { + jest.clearAllMocks() + }); + + afterAll(function (done) { + childProcess.exec('npm run rebuild', function (err, stdout) { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + done(); + }); }); it('should get user activity where user is not idle', function() { @@ -37,4 +57,25 @@ xdescribe('Tests for Activity Detection', function() { }); + it('should monitor user activity', function () { + activityDetection.setActivityWindow(500000, electron.ipcRenderer); + const spy = jest.spyOn(activityDetection, 'monitorUserActivity'); + + expect(spy).not.toBeCalled(); + + activityDetection.monitorUserActivity(); + expect(spy).toHaveBeenCalled(); + + }); + + it('should not send activity event as data is undefined', function () { + const spy = jest.spyOn(activityDetection, 'send'); + + expect(spy).not.toBeCalled(); + + activityDetection.send(undefined); + expect(spy).toHaveBeenCalledWith(undefined); + + }); + }); diff --git a/tests/config.test.js b/tests/config.test.js index 17caa01c..e4a7026f 100644 --- a/tests/config.test.js +++ b/tests/config.test.js @@ -1,4 +1,4 @@ -const { getConfigField, updateConfigField, configFileName } = require('../js/config'); +const { getConfigField, updateConfigField, configFileName, saveUserConfig } = require('../js/config'); const fs = require('fs'); const path = require('path'); const os = require('os'); @@ -135,6 +135,40 @@ describe('getConfigField tests', function() { expect(url).toBe('something'); }); }); + + it('should fail when global config path is invalid', function() { + + var globalConfig = { + url: 'something-else' + }; + createTempGlobalConfig(globalConfig); + + let correctConfigDir = globalConfigDir; + globalConfigDir = '//'; + + return getConfigField('url').catch(function(err) { + globalConfigDir = correctConfigDir; + expect(err).toBeTruthy(); + }); + + }); + + it('should fail when user config path is invalid', function() { + + var userConfig = { + url: 'something' + }; + createTempUserConfig(userConfig); + + let correctConfigDir = userConfigDir; + userConfigDir = '//'; + + return getConfigField('url').catch(function(err) { + userConfigDir = correctConfigDir; + expect(err).toBeTruthy(); + }); + + }); }); describe('updateConfigField tests', function() { @@ -142,7 +176,7 @@ describe('getConfigField tests', function() { it('should succeed and overwrite existing field', function() { var userConfig = { url: 'something' - } + }; createTempUserConfig(userConfig); @@ -157,7 +191,7 @@ describe('getConfigField tests', function() { it('should succeed and add new field', function() { var userConfig = { url: 'something' - } + }; createTempUserConfig(userConfig); @@ -169,5 +203,87 @@ describe('getConfigField tests', function() { }); }); }); + + it('should fail to update if invalid field name', function() { + + var userConfig = { + url: 'something' + }; + + createTempUserConfig(userConfig); + + return updateConfigField('', 'hello word').catch(function (err) { + expect(err).toBe('can not save config, invalid input'); + }); + + }); + + it('should throw error if path is not defined', function() { + + userConfigDir = null; + + return updateConfigField('url2', 'hello world') + .catch(function (err) { + expect(err).toThrow(err); + }); + + }); + + it('should throw error if fieldName is not defined', function() { + + var userConfig = { + url: 'something' + }; + + createTempUserConfig(userConfig); + + return saveUserConfig(undefined, 'something', 'oldConfig') + .catch(function (reject) { + expect(reject).toBe('can not save config, invalid input'); + }); + }); + + it('should throw error if config is not defined', function() { + + var userConfig = { + url: 'something' + }; + + createTempUserConfig(userConfig); + + return saveUserConfig('url2', 'something', undefined) + .catch(function (reject) { + expect(reject).toBe('can not save config, invalid input'); + }); + }); + + it('should throw error if config file is not correct', function() { + + var userConfig = { + url: 'something' + }; + createTempUserConfig(userConfig); + + let correctConfigDir = userConfigDir; + userConfigDir = '//'; + + return saveUserConfig('url2', 'hello world', 'url') + .catch(function(err) { + userConfigDir = correctConfigDir; + expect(err).toBeTruthy(); + }); + + }); + + it('should throw error if path is not defined for saveUserConfig()', function() { + + userConfigDir = null; + + return saveUserConfig('url2', 'hello world') + .catch(function (err) { + expect(err).toThrow(err); + }); + }); + }); }); diff --git a/tests/log.test.js b/tests/log.test.js new file mode 100644 index 00000000..62875ae2 --- /dev/null +++ b/tests/log.test.js @@ -0,0 +1,70 @@ +const { Logger } = require('../js/log.js'); + +describe('logger tests', function() { + let log; + + beforeEach(function() { + // get new rewired version for each test. + log = new Logger(); + }); + + it('when no logger registered then queue items', function() { + log.send('DEBUG', 'test'); + log.send('DEBUG', 'test2'); + let queue = log.logQueue; + expect(queue.length).toBe(2); + }); + + it('flush queue when logger get registered', function() { + log.send('DEBUG', 'test'); + log.send('DEBUG', 'test2'); + + let mockWin = { + send: jest.fn() + }; + + log.setLogWindow(mockWin); + + let queue = log.logQueue; + + expect(mockWin.send).toHaveBeenCalled(); + expect(queue.length).toBe(0); + }); + + it('send single log msg logger has already been registered', function() { + let mockWin = { + send: jest.fn() + }; + + log.setLogWindow(mockWin); + log.send('DEBUG', 'test'); + + let queue = log.logQueue; + + expect(mockWin.send).toHaveBeenCalled(); + expect(queue.length).toBe(0); + }); + + it('should cap at 100 queued log messages', function() { + for(let i = 0; i < 110; i++) { + log.send('DEBUG', 'test' + i); + } + + let queue = log.logQueue; + expect(queue.length).toBe(100); + }); + + it('should not send the logs', function() { + let mockWin = { + send: jest.fn() + }; + + log.setLogWindow(mockWin); + log.send(); + + let queue = log.logQueue; + + expect(mockWin.send).toHaveBeenCalledWith("log", {"msgs": []}); + expect(queue.length).toBe(0); + }); +}); diff --git a/tests/utils/getCmdLineArg.test.js b/tests/utils/getCmdLineArg.test.js new file mode 100644 index 00000000..eb8bd19e --- /dev/null +++ b/tests/utils/getCmdLineArg.test.js @@ -0,0 +1,28 @@ +const getCmdLineArg = require('../../js/utils/getCmdLineArg.js'); + +describe('getCmdLineArg tests', function() { + it('should return no exact match', function() { + var result = getCmdLineArg([ 'hello.exe', '--arg1', '--arg2'], '--arg', true); + expect(result).toBe(null); + }); + + it('should return exact match only', function() { + var result = getCmdLineArg([ 'hello.exe', '--arg1', '--arg2'], '--arg2', true); + expect(result).toBe('--arg2'); + }); + + it('should return starts with match', function() { + var result = getCmdLineArg([ 'hello.exe', '--hello=test', '--arg2'], '--hello='); + expect(result).toBe('--hello=test'); + }); + + it('should return no match for starts with', function() { + var result = getCmdLineArg([ 'hello.exe', '--hello=test', '--arg2'], '--help='); + expect(result).toBe(null); + }); + + it('should return no match invalid argv given', function() { + var result = getCmdLineArg('invalid argv', '--help='); + expect(result).toBe(null); + }); +}); diff --git a/tests/utils/getRegistry.test.js b/tests/utils/getRegistry.test.js new file mode 100644 index 00000000..97e382d5 --- /dev/null +++ b/tests/utils/getRegistry.test.js @@ -0,0 +1,70 @@ +const getRegistry = require('../../js/utils/getRegistry.js'); + +const { isMac } = require('../../js/utils/misc.js'); + +describe('Tests for getRegistry', function() { + + describe('Should not get registry for mac', function() { + + if (isMac){ + + it('should fail to get path for mac', function(done) { + + getRegistry('PodUrl').then(resolve).catch(reject); + + function resolve() { + // shouldn't get here + expect(true).toBe(false); + } + + function reject(err) { + expect(err).toBeTruthy(); + done(); + } + }); + + } + + }); + + describe('Should get registry for windows', function() { + + if (!isMac){ + + it('should get registry path', function(done) { + + getRegistry('PodUrl').then(resolve).catch(reject); + + function resolve(url) { + expect(url).toBe('string'); + done(); + } + + function reject(err) { + expect(err).toBeTruthy(); + done(); + } + + }); + + it('should not get the registry path', function(done) { + + getRegistry('wrongUrl').then(resolve).catch(reject); + + function resolve() { + expect(true).toBe(false) + } + + function reject(err) { + expect(err).toBeTruthy(); + expect(err).toBe('Cannot find PodUrl Registry. Using default url.'); + done(); + } + + }); + + } + + }); + +}); \ No newline at end of file diff --git a/tests/utils/throttle.test.js b/tests/utils/throttle.test.js index 35bb0c41..24e2a06b 100644 --- a/tests/utils/throttle.test.js +++ b/tests/utils/throttle.test.js @@ -55,6 +55,55 @@ describe('throttle tests', function() { expect(callback.mock.calls.length).toBe(2); }); + it('expect clearTimeout to be invoked', function() { + const callback = jest.fn(); + const throttledCB = throttle(1000, callback); + + expect(callback).not.toBeCalled(); + + throttledCB(); + expect(callback.mock.calls.length).toBe(1); + expect(clearTimeout.mock.calls.length).toBe(0); + + now -= 1000; + throttledCB(); + expect(callback.mock.calls.length).toBe(1); + + now += 1000; + throttledCB(); + expect(callback.mock.calls.length).toBe(1); + expect(clearTimeout.mock.calls.length).toBe(1); + }); + + describe('expect to throw exception', function() { + it('when calling throttle with time equal to zero', function(done) { + try { + throttle(0, function() {}); + } catch(error) { + expect(error.message).toBeDefined(); + done(); + } + }); + + it('when calling throttle with time less than zero', function(done) { + try { + throttle(-1, function() {}); + } catch(error) { + expect(error.message).toBeDefined(); + done(); + } + }); + + it('when calling throttle without a function callback', function(done) { + try { + throttle(1, 'not a func'); + } catch(error) { + expect(error.message).toBeDefined(); + done(); + } + }); + }); + afterEach(function() { // restore orig Date.now = origNow;