[MM-40277][MM-40279][MM-40301][MM-40307][MM-40310][MM-40313] Unit tests and refactors for main module (#1876)

* Refactor autoLauncher and remove unnecessary file

* [MM-40277] Unit tests for main/badge

* [MM-40279] Unit tests for main/certificateStore

* [MM-40301] Unit tests for main/contextMenu, also remove workaround

* [MM-40307] Unit tests for main/CriticalErrorHandler

* [MM-40310] Unit tests for main/utils

* [MM-40313] Unit tests for main/Validator

* Lint fix

* Added globals

* More things that should probably already be merged

* PR feedback
This commit is contained in:
Devin Binnie
2021-11-26 11:14:26 -05:00
committed by GitHub
parent fc06dc99a2
commit 5056ec7ace
14 changed files with 740 additions and 99 deletions

View File

@@ -17,35 +17,44 @@ export default class AutoLauncher {
});
}
isEnabled() {
return this.appLauncher.isEnabled();
async upgradeAutoLaunch() {
if (process.platform === 'darwin') {
return;
}
const appLauncher = new AutoLaunch({
name: 'Mattermost',
});
const enabled = await appLauncher.isEnabled();
if (enabled) {
await appLauncher.enable();
}
}
async blankPromise() {
return null;
isEnabled() {
return this.appLauncher.isEnabled();
}
async enable() {
if (isDev) {
log.warn('In development mode, autostart config never effects');
return this.blankPromise();
return Promise.resolve(null);
}
const enabled = await this.isEnabled();
if (!enabled) {
return this.appLauncher.enable();
}
return this.blankPromise();
return Promise.resolve(null);
}
async disable() {
if (isDev) {
log.warn('In development mode, autostart config never effects');
return this.blankPromise();
return Promise.resolve(null);
}
const enabled = await this.isEnabled();
if (enabled) {
return this.appLauncher.disable();
}
return this.blankPromise();
return Promise.resolve(null);
}
}

View File

@@ -0,0 +1,107 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
import {spawn} from 'child_process';
import path from 'path';
import {app, dialog} from 'electron';
import CriticalErrorHandler from './CriticalErrorHandler';
jest.mock('path', () => ({
join: jest.fn().mockImplementation((...args) => args.join('/')),
}));
jest.mock('electron', () => ({
app: {
name: 'Mattermost',
getVersion: () => '5.0.0',
getPath: (folder) => `/${folder}`,
relaunch: jest.fn(),
isReady: jest.fn(),
exit: jest.fn(),
},
dialog: {
showMessageBox: jest.fn(),
},
}));
jest.mock('electron-log', () => ({
error: jest.fn(),
}));
jest.mock('fs', () => ({
writeFileSync: jest.fn(),
}));
jest.mock('child_process', () => ({
spawn: jest.fn(),
}));
describe('main/CriticalErrorHandler', () => {
const criticalErrorHandler = new CriticalErrorHandler();
beforeEach(() => {
criticalErrorHandler.setMainWindow({});
});
describe('windowUnresponsiveHandler', () => {
it('should do nothing when mainWindow is null', () => {
criticalErrorHandler.setMainWindow(null);
criticalErrorHandler.windowUnresponsiveHandler();
expect(dialog.showMessageBox).not.toBeCalled();
});
it('should call app.relaunch when user elects not to wait', async () => {
const promise = Promise.resolve({response: 0});
dialog.showMessageBox.mockImplementation(() => promise);
criticalErrorHandler.windowUnresponsiveHandler();
await promise;
expect(app.relaunch).toBeCalled();
});
});
describe('processUncaughtExceptionHandler', () => {
beforeEach(() => {
app.isReady.mockImplementation(() => true);
criticalErrorHandler.setMainWindow({isVisible: true});
});
it('should throw error if app is not ready', () => {
app.isReady.mockImplementation(() => false);
expect(() => {
criticalErrorHandler.processUncaughtExceptionHandler(new Error('test'));
}).toThrow(Error);
expect(dialog.showMessageBox).not.toBeCalled();
});
it('should do nothing if main window is null or not visible', () => {
criticalErrorHandler.setMainWindow(null);
criticalErrorHandler.processUncaughtExceptionHandler(new Error('test'));
expect(dialog.showMessageBox).not.toBeCalled();
criticalErrorHandler.setMainWindow({isVisible: false});
criticalErrorHandler.processUncaughtExceptionHandler(new Error('test'));
expect(dialog.showMessageBox).not.toBeCalled();
});
it('should open external file on Show Details', async () => {
path.join.mockImplementation(() => 'testfile.txt');
const promise = Promise.resolve({response: process.platform === 'darwin' ? 2 : 0});
dialog.showMessageBox.mockImplementation(() => promise);
criticalErrorHandler.processUncaughtExceptionHandler(new Error('test'));
await promise;
expect(spawn).toBeCalledWith(expect.any(String), expect.arrayContaining(['testfile.txt']), expect.any(Object));
});
it('should restart app on Reopen', async () => {
path.join.mockImplementation(() => 'testfile.txt');
const promise = Promise.resolve({response: process.platform === 'darwin' ? 0 : 2});
dialog.showMessageBox.mockImplementation(() => promise);
criticalErrorHandler.processUncaughtExceptionHandler(new Error('test'));
await promise;
expect(app.relaunch).toBeCalled();
});
});
});

View File

@@ -57,7 +57,8 @@ export default class CriticalErrorHandler {
defaultId: 0,
}).then(({response}) => {
if (response === 0) {
throw new Error('BrowserWindow \'unresponsive\' event has been emitted');
log.error('BrowserWindow \'unresponsive\' event has been emitted');
app.relaunch();
}
});
}

205
src/main/Validator.test.js Normal file
View File

@@ -0,0 +1,205 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
import * as Validator from './Validator';
jest.mock('electron-log', () => ({
error: jest.fn(),
}));
describe('main/Validator', () => {
describe('validateV0ConfigData', () => {
const config = {url: 'http://server-1.com'};
it('should return null when not provided object', () => {
expect(Validator.validateV0ConfigData('notanobject')).toBe(null);
});
it('should return complete object when it is valid', () => {
expect(Validator.validateV0ConfigData(config)).toStrictEqual(config);
});
it('should remove fields that arent part of the schema', () => {
const modifiedConfig = {...config, anotherField: 'value'};
expect(Validator.validateV0ConfigData(modifiedConfig)).toStrictEqual(config);
});
});
describe('validateV1ConfigData', () => {
const config = {
autostart: true,
enableHardwareAcceleration: true,
minimizeToTray: false,
showTrayIcon: false,
showUnreadBadge: true,
spellCheckerLocale: 'en-US',
teams: [
{
name: 'server-1',
url: 'http://server-1.com',
},
],
trayIconTheme: 'light',
useSpellChecker: true,
version: 1,
};
it('should remove invalid urls', () => {
const modifiedConfig = {
...config,
teams: [
...config.teams,
{
name: 'server-2',
url: 'a-bad>url',
},
],
};
expect(Validator.validateV1ConfigData(modifiedConfig)).toStrictEqual(config);
});
it('should clean URLs with backslashes', () => {
const modifiedConfig = {
...config,
teams: [
...config.teams,
{
name: 'server-2',
url: 'http:\\\\server-2.com\\subpath',
},
],
};
expect(Validator.validateV1ConfigData(modifiedConfig)).toStrictEqual({
...config,
teams: [
...config.teams,
{
name: 'server-2',
url: 'http://server-2.com/subpath',
},
],
});
});
it('should invalidate bad spell checker locales', () => {
const modifiedConfig = {
...config,
spellCheckerLocale: 'not-a-locale',
};
expect(Validator.validateV1ConfigData(modifiedConfig)).toStrictEqual(null);
});
});
describe('validateV2ConfigData', () => {
const config = {
autostart: true,
darkMode: false,
enableHardwareAcceleration: true,
minimizeToTray: false,
showTrayIcon: false,
showUnreadBadge: true,
spellCheckerLocale: 'en-US',
spellCheckerURL: 'http://spellcheckerservice.com',
teams: [
{
name: 'server-1',
url: 'http://server-1.com',
order: 1,
},
],
trayIconTheme: 'light',
useSpellChecker: true,
version: 2,
};
it('should remove invalid spellchecker URLs', () => {
const modifiedConfig = {
...config,
spellCheckerURL: 'a-bad>url',
};
expect(Validator.validateV2ConfigData(modifiedConfig)).not.toHaveProperty('spellCheckerURL');
});
});
describe('validateV3ConfigData', () => {
const config = {
autostart: true,
darkMode: false,
enableHardwareAcceleration: true,
lastActiveTeam: 0,
minimizeToTray: false,
showTrayIcon: false,
showUnreadBadge: true,
spellCheckerLocales: ['en-US'],
spellCheckerURL: 'http://spellcheckerservice.com',
teams: [
{
lastActiveTab: 0,
name: 'server-1',
url: 'http://server-1.com',
order: 1,
tabs: [
{
name: 'tab-1',
isOpen: true,
},
],
},
],
trayIconTheme: 'light',
useSpellChecker: true,
version: 3,
};
it('should ensure messaging tab is open', () => {
const modifiedConfig = {
...config,
teams: [
{
...config.teams[0],
tabs: [
...config.teams[0].tabs,
{
name: 'TAB_MESSAGING',
isOpen: false,
},
],
},
],
};
expect(Validator.validateV3ConfigData(modifiedConfig)).toStrictEqual({
...config,
teams: [
{
...config.teams[0],
tabs: [
...config.teams[0].tabs,
{
name: 'TAB_MESSAGING',
isOpen: true,
},
],
},
],
});
});
});
describe('validateAllowedProtocols', () => {
const allowedProtocols = [
'spotify:',
'steam:',
'mattermost:',
];
it('should accept valid protocols', () => {
expect(Validator.validateAllowedProtocols(allowedProtocols)).toStrictEqual(allowedProtocols);
});
it('should reject invalid protocols', () => {
expect(Validator.validateAllowedProtocols([...allowedProtocols, 'not-a-protocol'])).toStrictEqual(null);
});
});
});

View File

@@ -5,7 +5,7 @@ import log from 'electron-log';
import Joi from '@hapi/joi';
import {Args} from 'types/args';
import {ConfigV0, ConfigV1, ConfigV2, ConfigV3} from 'types/config';
import {ConfigV0, ConfigV1, ConfigV2, ConfigV3, TeamWithTabs} from 'types/config';
import {SavedWindowState} from 'types/mainWindow';
import {AppState} from 'types/appState';
import {ComparableCertificate} from 'types/certificate';
@@ -168,27 +168,6 @@ export function validateV0ConfigData(data: ConfigV0) {
return validateAgainstSchema(data, configDataSchemaV0);
}
// validate v.1 config.json
export function validateV1ConfigData(data: ConfigV1) {
if (Array.isArray(data.teams) && data.teams.length) {
// first replace possible backslashes with forward slashes
let teams = data.teams.map(({name, url}) => {
let updatedURL = url;
if (updatedURL.includes('\\')) {
updatedURL = updatedURL.toLowerCase().replace(/\\/gi, '/');
}
return {name, url: updatedURL};
});
// next filter out urls that are still invalid so all is not lost
teams = teams.filter(({url}) => urlUtils.isValidURL(url));
// replace original teams
data.teams = teams;
}
return validateAgainstSchema(data, configDataSchemaV1);
}
function cleanURL(url: string): string {
let updatedURL = url;
if (updatedURL.includes('\\')) {
@@ -197,22 +176,45 @@ function cleanURL(url: string): string {
return updatedURL;
}
export function validateV2ConfigData(data: ConfigV2) {
if (Array.isArray(data.teams) && data.teams.length) {
// first replace possible backslashes with forward slashes
let teams = data.teams.map((team) => {
function cleanTeam<T extends {name: string; url: string}>(team: T) {
return {
...team,
url: cleanURL(team.url),
};
}
function cleanTeamWithTabs(team: TeamWithTabs) {
return {
...cleanTeam(team),
tabs: team.tabs.map((tab) => {
return {
...team,
url: cleanURL(team.url),
...tab,
isOpen: tab.name === TAB_MESSAGING ? true : tab.isOpen,
};
});
}),
};
}
function cleanTeams<T extends {name: string; url: string}>(teams: T[], func: (team: T) => T) {
let newTeams = teams;
if (Array.isArray(newTeams) && newTeams.length) {
// first replace possible backslashes with forward slashes
newTeams = newTeams.map((team) => func(team));
// next filter out urls that are still invalid so all is not lost
teams = teams.filter(({url}) => urlUtils.isValidURL(url));
// replace original teams
data.teams = teams;
newTeams = newTeams.filter(({url}) => urlUtils.isValidURL(url));
}
return newTeams;
}
// validate v.1 config.json
export function validateV1ConfigData(data: ConfigV1) {
data.teams = cleanTeams(data.teams, cleanTeam);
return validateAgainstSchema(data, configDataSchemaV1);
}
export function validateV2ConfigData(data: ConfigV2) {
data.teams = cleanTeams(data.teams, cleanTeam);
if (data.spellCheckerURL && !urlUtils.isValidURL(data.spellCheckerURL)) {
log.error('Invalid download location for spellchecker dictionary, removing from config');
delete data.spellCheckerURL;
@@ -221,29 +223,7 @@ export function validateV2ConfigData(data: ConfigV2) {
}
export function validateV3ConfigData(data: ConfigV3) {
if (Array.isArray(data.teams) && data.teams.length) {
// first replace possible backslashes with forward slashes
let teams = data.teams.map((team) => {
return {
...team,
url: cleanURL(team.url),
// Force messaging to stay open regardless of user config
tabs: team.tabs.map((tab) => {
return {
...tab,
isOpen: tab.name === TAB_MESSAGING ? true : tab.isOpen,
};
}),
};
});
// next filter out urls that are still invalid so all is not lost
teams = teams.filter(({url}) => urlUtils.isValidURL(url));
// replace original teams
data.teams = teams;
}
data.teams = cleanTeams(data.teams, cleanTeamWithTabs);
if (data.spellCheckerURL && !urlUtils.isValidURL(data.spellCheckerURL)) {
log.error('Invalid download location for spellchecker dictionary, removing from config');
delete data.spellCheckerURL;

View File

@@ -1,19 +0,0 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import AutoLaunch from 'auto-launch';
async function upgradeAutoLaunch() {
if (process.platform === 'darwin') {
return;
}
const appLauncher = new AutoLaunch({
name: 'Mattermost',
});
const enabled = await appLauncher.isEnabled();
if (enabled) {
await appLauncher.enable();
}
}
export default upgradeAutoLaunch;

79
src/main/badge.test.js Normal file
View File

@@ -0,0 +1,79 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
import {app} from 'electron';
import * as Badge from './badge';
import {setOverlayIcon} from './windows/windowManager';
jest.mock('electron', () => ({
app: {
dock: {
setBadge: jest.fn(),
},
},
}));
jest.mock('./appState', () => ({
updateBadge: jest.fn(),
}));
jest.mock('./windows/windowManager', () => ({
setOverlayIcon: jest.fn(),
}));
describe('main/badge', () => {
describe('showBadgeWindows', () => {
it('should show dot when session expired', () => {
Badge.showBadgeWindows(true, 7, false);
expect(setOverlayIcon).toBeCalledWith('•', expect.any(String), expect.any(Boolean));
});
it('should show mention count when has mention count', () => {
Badge.showBadgeWindows(false, 50, false);
expect(setOverlayIcon).toBeCalledWith('50', expect.any(String), false);
});
it('should show 99+ when has mention count over 99', () => {
Badge.showBadgeWindows(false, 200, false);
expect(setOverlayIcon).toBeCalledWith('99+', expect.any(String), true);
});
it('should not show dot when has unreads but setting is off', () => {
Badge.showBadgeWindows(false, 0, true);
expect(setOverlayIcon).not.toBeCalledWith('•', expect.any(String), expect.any(Boolean));
});
it('should show dot when has unreads', () => {
Badge.setUnreadBadgeSetting(true);
Badge.showBadgeWindows(false, 0, true);
expect(setOverlayIcon).toBeCalledWith('•', expect.any(String), expect.any(Boolean));
Badge.setUnreadBadgeSetting(false);
});
});
describe('showBadgeOSX', () => {
it('should show dot when session expired', () => {
Badge.showBadgeOSX(true, 7, false);
expect(app.dock.setBadge).toBeCalledWith('•');
});
it('should show mention count when has mention count', () => {
Badge.showBadgeOSX(false, 50, false);
expect(app.dock.setBadge).toBeCalledWith('50');
});
it('should not show dot when has unreads but setting is off', () => {
Badge.showBadgeOSX(false, 0, true);
expect(app.dock.setBadge).not.toBeCalledWith('•');
});
it('should show dot when has unreads', () => {
Badge.setUnreadBadgeSetting(true);
Badge.showBadgeOSX(false, 0, true);
expect(app.dock.setBadge).toBeCalledWith('•');
});
});
});

View File

@@ -13,7 +13,7 @@ const MAX_WIN_COUNT = 99;
let showUnreadBadgeSetting: boolean;
function showBadgeWindows(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
export function showBadgeWindows(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
let description = 'You have no unread messages';
let text;
if (sessionExpired) {
@@ -29,7 +29,7 @@ function showBadgeWindows(sessionExpired: boolean, mentionCount: number, showUnr
WindowManager.setOverlayIcon(text, description, mentionCount > 99);
}
function showBadgeOSX(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
export function showBadgeOSX(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
let badge = '';
if (sessionExpired) {
badge = '•';

View File

@@ -0,0 +1,102 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
import fs from 'fs';
import {validateCertificateStore} from './Validator';
import CertificateStore from './certificateStore';
jest.mock('./Validator', () => ({
validateCertificateStore: jest.fn(),
}));
jest.mock('fs', () => ({
readFileSync: jest.fn(),
}));
const certificateData = {
'https://server-1.com': {
data: 'somerandomdata',
issuerName: 'someissuer',
dontTrust: false,
},
'https://server-2.com': {
data: 'somerandomdata',
issuerName: 'someissuer',
dontTrust: true,
},
};
describe('main/certificateStore', () => {
it('should fail gracefully when loaded data is malformed', () => {
validateCertificateStore.mockImplementation(() => null);
let certificateStore;
expect(() => {
certificateStore = new CertificateStore('somefilename');
}).not.toThrow(Error);
expect(certificateStore.data).toStrictEqual({});
});
describe('isTrusted', () => {
let certificateStore;
beforeAll(() => {
validateCertificateStore.mockImplementation((data) => JSON.parse(data));
fs.readFileSync.mockImplementation(() => JSON.stringify(certificateData));
certificateStore = new CertificateStore('somefilename');
});
it('should return true for stored matching certificate', () => {
certificateStore = new CertificateStore('somefilename');
expect(certificateStore.isTrusted('https://server-1.com', {
data: 'somerandomdata',
issuerName: 'someissuer',
})).toBe(true);
});
it('should return false for missing url', () => {
expect(certificateStore.isTrusted('https://server-3.com', {
data: 'somerandomdata',
issuerName: 'someissuer',
})).toBe(false);
});
it('should return false for unmatching cert', () => {
expect(certificateStore.isTrusted('https://server-1.com', {
data: 'someotherrandomdata',
issuerName: 'someissuer',
})).toBe(false);
expect(certificateStore.isTrusted('https://server-1.com', {
data: 'somerandomdata',
issuerName: 'someotherissuer',
})).toBe(false);
});
});
describe('isExplicitlyUntrusted', () => {
let certificateStore;
beforeAll(() => {
validateCertificateStore.mockImplementation((data) => JSON.parse(data));
fs.readFileSync.mockImplementation(() => JSON.stringify(certificateData));
certificateStore = new CertificateStore('somefilename');
});
it('should return true for explicitly untrusted cert', () => {
expect(certificateStore.isExplicitlyUntrusted('https://server-2.com', {
data: 'somerandomdata',
issuerName: 'someissuer',
})).toBe(true);
});
it('should return false for trusted cert', () => {
expect(certificateStore.isExplicitlyUntrusted('https://server-1.com', {
data: 'somerandomdata',
issuerName: 'someissuer',
})).toBe(false);
});
});
});

View File

@@ -0,0 +1,82 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
import ContextMenu from './contextMenu';
jest.mock('electron-context-menu', () => {
return () => jest.fn();
});
describe('main/contextMenu', () => {
describe('shouldShowMenu', () => {
const contextMenu = new ContextMenu();
it('should not show menu on internal link', () => {
expect(contextMenu.menuOptions.shouldShowMenu(null, {
mediaType: 'none',
linkURL: 'http://server-1.com/subpath#',
pageURL: 'http://server-1.com/subpath',
srcURL: '',
misspelledWord: '',
selectionText: '',
})).toBe(false);
});
it('should not show menu on buttons', () => {
expect(contextMenu.menuOptions.shouldShowMenu(null, {
mediaType: 'none',
linkURL: '',
pageURL: 'http://server-1.com/subpath',
srcURL: '',
misspelledWord: '',
selectionText: '',
})).toBe(false);
});
it('should show menu on editables', () => {
expect(contextMenu.menuOptions.shouldShowMenu(null, {
mediaType: 'none',
linkURL: '',
pageURL: 'http://server-1.com/subpath',
srcURL: '',
misspelledWord: '',
selectionText: '',
isEditable: true,
})).toBe(true);
});
it('should show menu on images', () => {
expect(contextMenu.menuOptions.shouldShowMenu(null, {
mediaType: 'image',
linkURL: '',
pageURL: 'http://server-1.com/subpath',
srcURL: 'http://server-1.com/subpath/image.png',
misspelledWord: '',
selectionText: '',
isEditable: true,
})).toBe(true);
});
it('should show menu on external links', () => {
expect(contextMenu.menuOptions.shouldShowMenu(null, {
mediaType: 'none',
linkURL: 'http://server-2.com/link',
pageURL: 'http://server-1.com/subpath',
srcURL: '',
misspelledWord: '',
selectionText: '',
isEditable: true,
})).toBe(true);
});
});
describe('reload', () => {
it('should call dispose on reload', () => {
const contextMenu = new ContextMenu();
const fn = contextMenu.menuDispose;
contextMenu.reload();
expect(fn).toHaveBeenCalled();
});
});
});

View File

@@ -2,7 +2,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BrowserView, BrowserWindow, ContextMenuParams, Event, WebContents} from 'electron';
import {BrowserView, BrowserWindow, ContextMenuParams, Event} from 'electron';
import electronContextMenu, {Options} from 'electron-context-menu';
import urlUtils from 'common/utils/url';
@@ -51,11 +51,7 @@ export default class ContextMenu {
reload = () => {
this.dispose();
/**
* Work-around issue with passing `WebContents` to `electron-context-menu` in Electron 11
* @see https://github.com/sindresorhus/electron-context-menu/issues/123
*/
const options = {window: {webContents: this.view.webContents, inspectElement: this.view.webContents.inspectElement.bind(this.view.webContents), isDestroyed: this.view.webContents.isDestroyed.bind(this.view.webContents), off: this.view.webContents.off.bind(this.view.webContents)} as unknown as WebContents, ...this.menuOptions};
const options = {window: this.view, ...this.menuOptions};
this.menuDispose = electronContextMenu(options);
}
}

View File

@@ -55,7 +55,6 @@ import {protocols} from '../../electron-builder.json';
import AutoLauncher from './AutoLauncher';
import CriticalErrorHandler from './CriticalErrorHandler';
import upgradeAutoLaunch from './autoLaunch';
import CertificateStore from './certificateStore';
import TrustedOriginsStore from './trustedOrigins';
import {createMenu as createAppMenu} from './menus/app';
@@ -91,6 +90,7 @@ const {
} = electron;
const criticalErrorHandler = new CriticalErrorHandler();
const userActivityMonitor = new UserActivityMonitor();
const autoLauncher = new AutoLauncher();
const certificateErrorCallbacks = new Map();
// Keep a global reference of the window object, if you don't, the window will
@@ -281,8 +281,7 @@ function handleConfigUpdate(newConfig: CombinedConfig) {
return;
}
if (process.platform === 'win32' || process.platform === 'linux') {
const appLauncher = new AutoLauncher();
const autoStartTask = config.autostart ? appLauncher.enable() : appLauncher.disable();
const autoStartTask = config.autostart ? autoLauncher.enable() : autoLauncher.disable();
autoStartTask.then(() => {
log.info('config.autostart has been configured:', newConfig.autostart);
}).catch((err) => {
@@ -684,7 +683,7 @@ function initializeAfterAppReady() {
appVersion.lastAppVersion = app.getVersion();
if (!global.isDev) {
upgradeAutoLaunch();
autoLauncher.upgradeAutoLaunch();
}
if (global.isDev) {

100
src/main/utils.test.js Normal file
View File

@@ -0,0 +1,100 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
import {BACK_BAR_HEIGHT, TAB_BAR_HEIGHT} from 'common/utils/constants';
import {runMode} from 'common/utils/util';
import * as Utils from './utils';
jest.mock('electron', () => ({
app: {
getLoginItemSettings: () => ({
wasOpenedAsHidden: true,
}),
getAppPath: () => '/path/to/app',
},
}));
jest.mock('common/utils/util', () => ({
runMode: jest.fn(),
}));
jest.mock('path', () => {
const original = jest.requireActual('path');
return {
...original,
resolve: (basePath, ...restOfPath) => original.join('/path/to/app/src/main', ...restOfPath),
};
});
describe('main/utils', () => {
describe('shouldBeHiddenOnStartup', () => {
let originalPlatform;
beforeAll(() => {
originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'darwin',
});
});
it('should be hidden on mac when opened as hidden', () => {
expect(Utils.shouldBeHiddenOnStartup({})).toBe(true);
});
afterAll(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
});
});
describe('getWindowBoundaries', () => {
it('should include tab bar height', () => {
expect(Utils.getWindowBoundaries({
getContentBounds: () => ({width: 500, height: 400}),
})).toStrictEqual({
x: 0,
y: TAB_BAR_HEIGHT,
width: 500,
height: 400 - TAB_BAR_HEIGHT,
});
});
it('should include back bar height when specified', () => {
expect(Utils.getWindowBoundaries({
getContentBounds: () => ({width: 500, height: 400}),
}, true)).toStrictEqual({
x: 0,
y: TAB_BAR_HEIGHT + BACK_BAR_HEIGHT,
width: 500,
height: 400 - TAB_BAR_HEIGHT - BACK_BAR_HEIGHT,
});
});
});
describe('getLocalURLString', () => {
it('should return URL relative to current run directory', () => {
runMode.mockImplementation(() => 'development');
expect(Utils.getLocalURLString('index.html')).toStrictEqual('file:///path/to/app/dist/renderer/index.html');
});
it('should return URL relative to current run directory in production', () => {
runMode.mockImplementation(() => 'production');
expect(Utils.getLocalURLString('index.html')).toStrictEqual('file:///path/to/app/renderer/index.html');
});
it('should include query string when specified', () => {
const queryMap = new Map([['key', 'value']]);
runMode.mockImplementation(() => 'development');
expect(Utils.getLocalURLString('index.html', queryMap)).toStrictEqual('file:///path/to/app/dist/renderer/index.html?key=value');
});
it('should return URL relative to current run directory when using main process', () => {
runMode.mockImplementation(() => 'development');
expect(Utils.getLocalURLString('index.html', null, true)).toStrictEqual('file:///path/to/app/dist/index.html');
});
});
});

View File

@@ -2,7 +2,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import electron, {app, BrowserWindow} from 'electron';
import {app, BrowserWindow} from 'electron';
import path from 'path';
import {Args} from 'types/args';
@@ -49,7 +49,7 @@ export function getLocalURL(urlPath: string, query?: Map<string, string>, isMain
const hostname = '';
const port = '';
if (mode === PRODUCTION) {
pathname = path.join(electron.app.getAppPath(), `${processPath}/${urlPath}`);
pathname = path.join(app.getAppPath(), `${processPath}/${urlPath}`);
} else {
pathname = path.resolve(__dirname, `../../dist/${processPath}/${urlPath}`); // TODO: find a better way to work with webpack on this
}
@@ -66,7 +66,7 @@ export function getLocalURL(urlPath: string, query?: Map<string, string>, isMain
export function getLocalPreload(file: string) {
if (Utils.runMode() === PRODUCTION) {
return path.join(electron.app.getAppPath(), `${file}`);
return path.join(app.getAppPath(), `${file}`);
}
return path.resolve(__dirname, `../../dist/${file}`);
}