[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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
107
src/main/CriticalErrorHandler.test.js
Normal file
107
src/main/CriticalErrorHandler.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -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
205
src/main/Validator.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
@@ -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;
|
||||
|
@@ -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
79
src/main/badge.test.js
Normal 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('•');
|
||||
});
|
||||
});
|
||||
});
|
@@ -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 = '•';
|
||||
|
102
src/main/certificateStore.test.js
Normal file
102
src/main/certificateStore.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
82
src/main/contextMenu.test.js
Normal file
82
src/main/contextMenu.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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
100
src/main/utils.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
@@ -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}`);
|
||||
}
|
||||
|
Reference in New Issue
Block a user