[MM-40330] Unit tests for main/windows (#1885)
* [MM-40330] Unit tests for main/windows * Merge'd
This commit is contained in:
@@ -6,7 +6,7 @@ import fs from 'fs';
|
||||
|
||||
import {shell, dialog} from 'electron';
|
||||
|
||||
import {getMainWindow} from './windows/windowManager';
|
||||
import WindowManager from './windows/windowManager';
|
||||
|
||||
import {AllowProtocolDialog} from './allowProtocolDialog';
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('main/allowProtocolDialog', () => {
|
||||
});
|
||||
|
||||
it('should not open message box if main window is missing', () => {
|
||||
getMainWindow.mockImplementation(() => null);
|
||||
WindowManager.getMainWindow.mockImplementation(() => null);
|
||||
allowProtocolDialog.handleDialogEvent('mattermost:', 'mattermost://community.mattermost.com');
|
||||
expect(shell.openExternal).not.toBeCalled();
|
||||
expect(dialog.showMessageBox).not.toBeCalled();
|
||||
@@ -128,7 +128,7 @@ describe('main/allowProtocolDialog', () => {
|
||||
|
||||
describe('main window not null', () => {
|
||||
beforeEach(() => {
|
||||
getMainWindow.mockImplementation(() => ({}));
|
||||
WindowManager.getMainWindow.mockImplementation(() => ({}));
|
||||
});
|
||||
|
||||
it('should open the window but not save when clicking Yes', async () => {
|
||||
|
@@ -13,7 +13,7 @@ import log from 'electron-log';
|
||||
import {protocols} from '../../electron-builder.json';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
import {getMainWindow} from './windows/windowManager';
|
||||
import WindowManager from './windows/windowManager';
|
||||
|
||||
const allowedProtocolFile = path.resolve(app.getPath('userData'), 'allowedProtocols.json');
|
||||
|
||||
@@ -52,7 +52,7 @@ export class AllowProtocolDialog {
|
||||
shell.openExternal(URL);
|
||||
return;
|
||||
}
|
||||
const mainWindow = getMainWindow();
|
||||
const mainWindow = WindowManager.getMainWindow();
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import {ipcMain} from 'electron';
|
||||
|
||||
import {UPDATE_MENTIONS, UPDATE_TRAY, UPDATE_BADGE, SESSION_EXPIRED, UPDATE_DROPDOWN_MENTIONS} from 'common/communication';
|
||||
|
||||
import * as WindowManager from './windows/windowManager';
|
||||
import WindowManager from './windows/windowManager';
|
||||
|
||||
const status = {
|
||||
unreads: new Map<string, boolean>(),
|
||||
|
@@ -10,12 +10,10 @@ import {LoginModalData} from 'types/auth';
|
||||
import {BASIC_AUTH_PERMISSION} from 'common/permissions';
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
import * as WindowManager from 'main/windows/windowManager';
|
||||
|
||||
import modalManager from './views/modalManager';
|
||||
import {getLocalURLString, getLocalPreload} from './utils';
|
||||
|
||||
import TrustedOriginsStore from './trustedOrigins';
|
||||
import modalManager from 'main/views/modalManager';
|
||||
import TrustedOriginsStore from 'main/trustedOrigins';
|
||||
import {getLocalURLString, getLocalPreload} from 'main/utils';
|
||||
import WindowManager from 'main/windows/windowManager';
|
||||
|
||||
const modalPreload = getLocalPreload('modalPreload.js');
|
||||
const loginModalHtml = getLocalURLString('loginModal.html');
|
||||
|
@@ -6,7 +6,7 @@ import {app} from 'electron';
|
||||
|
||||
import * as Badge from './badge';
|
||||
|
||||
import {setOverlayIcon} from './windows/windowManager';
|
||||
import WindowManager from './windows/windowManager';
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
app: {
|
||||
@@ -28,28 +28,28 @@ 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));
|
||||
expect(WindowManager.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);
|
||||
expect(WindowManager.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);
|
||||
expect(WindowManager.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));
|
||||
expect(WindowManager.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));
|
||||
expect(WindowManager.setOverlayIcon).toBeCalledWith('•', expect.any(String), expect.any(Boolean));
|
||||
Badge.setUnreadBadgeSetting(false);
|
||||
});
|
||||
});
|
||||
|
@@ -6,7 +6,7 @@ import {app} from 'electron';
|
||||
|
||||
import {UPDATE_BADGE} from 'common/communication';
|
||||
|
||||
import * as WindowManager from './windows/windowManager';
|
||||
import WindowManager from './windows/windowManager';
|
||||
import * as AppState from './appState';
|
||||
|
||||
const MAX_WIN_COUNT = 99;
|
||||
|
@@ -5,7 +5,7 @@ import {Certificate, WebContents} from 'electron';
|
||||
|
||||
import {CertificateModalData} from 'types/certificate';
|
||||
|
||||
import * as WindowManager from './windows/windowManager';
|
||||
import WindowManager from './windows/windowManager';
|
||||
|
||||
import modalManager from './views/modalManager';
|
||||
import {getLocalURLString, getLocalPreload} from './utils';
|
||||
|
@@ -63,7 +63,7 @@ import allowProtocolDialog from './allowProtocolDialog';
|
||||
import AppVersionManager from './AppVersionManager';
|
||||
import initCookieManager from './cookieManager';
|
||||
import UserActivityMonitor from './UserActivityMonitor';
|
||||
import * as WindowManager from './windows/windowManager';
|
||||
import WindowManager from './windows/windowManager';
|
||||
import {displayMention, displayDownloadCompleted} from './notifications';
|
||||
|
||||
import parseArgs from './ParseArgs';
|
||||
|
@@ -3,7 +3,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as WindowManager from 'main/windows/windowManager';
|
||||
import WindowManager from 'main/windows/windowManager';
|
||||
|
||||
import {createTemplate} from './app';
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import {SHOW_NEW_SERVER_MODAL} from 'common/communication';
|
||||
import Config from 'common/config';
|
||||
import {TabType, getTabDisplayName} from 'common/tabs/TabView';
|
||||
|
||||
import * as WindowManager from 'main/windows/windowManager';
|
||||
import WindowManager from 'main/windows/windowManager';
|
||||
|
||||
export function createTemplate(config: Config) {
|
||||
const separatorItem: MenuItemConstructorOptions = {
|
||||
|
@@ -6,7 +6,7 @@
|
||||
import {Menu, MenuItem, MenuItemConstructorOptions} from 'electron';
|
||||
import {CombinedConfig} from 'types/config';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
import WindowManager from '../windows/windowManager';
|
||||
|
||||
export function createTemplate(config: CombinedConfig) {
|
||||
const teams = config.teams;
|
||||
|
@@ -8,7 +8,7 @@ import {Notification, shell} from 'electron';
|
||||
import {PLAY_SOUND} from 'common/communication';
|
||||
import {TAB_MESSAGING} from 'common/tabs/TabView';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
import WindowManager from '../windows/windowManager';
|
||||
|
||||
import {displayMention, displayDownloadCompleted, currentNotifications} from './index';
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import {MentionData} from 'types/notification';
|
||||
import {PLAY_SOUND} from 'common/communication';
|
||||
import {TAB_MESSAGING} from 'common/tabs/TabView';
|
||||
|
||||
import * as windowManager from '../windows/windowManager';
|
||||
import WindowManager from '../windows/windowManager';
|
||||
|
||||
import {Mention} from './Mention';
|
||||
import {DownloadNotification} from './Download';
|
||||
@@ -21,7 +21,7 @@ export function displayMention(title: string, body: string, channel: {id: string
|
||||
log.error('notification not supported');
|
||||
return;
|
||||
}
|
||||
const serverName = windowManager.getServerNameByWebContentsId(webcontents.id);
|
||||
const serverName = WindowManager.getServerNameByWebContentsId(webcontents.id);
|
||||
|
||||
const options = {
|
||||
title: `${serverName}: ${title}`,
|
||||
@@ -45,15 +45,15 @@ export function displayMention(title: string, body: string, channel: {id: string
|
||||
}
|
||||
const notificationSound = mention.getNotificationSound();
|
||||
if (notificationSound) {
|
||||
windowManager.sendToRenderer(PLAY_SOUND, notificationSound);
|
||||
WindowManager.sendToRenderer(PLAY_SOUND, notificationSound);
|
||||
}
|
||||
windowManager.flashFrame(true);
|
||||
WindowManager.flashFrame(true);
|
||||
});
|
||||
|
||||
mention.on('click', () => {
|
||||
log.info('notification click', serverName, mention);
|
||||
if (serverName) {
|
||||
windowManager.switchTab(serverName, TAB_MESSAGING);
|
||||
WindowManager.switchTab(serverName, TAB_MESSAGING);
|
||||
webcontents.send('notification-clicked', {channel, teamId, url});
|
||||
}
|
||||
});
|
||||
@@ -68,7 +68,7 @@ export function displayDownloadCompleted(fileName: string, path: string, serverN
|
||||
const download = new DownloadNotification(fileName, serverName);
|
||||
|
||||
download.on('show', () => {
|
||||
windowManager.flashFrame(true);
|
||||
WindowManager.flashFrame(true);
|
||||
});
|
||||
|
||||
download.on('click', () => {
|
||||
|
@@ -6,7 +6,7 @@ import {app, nativeImage, Tray, systemPreferences, nativeTheme} from 'electron';
|
||||
|
||||
import {UPDATE_TRAY} from 'common/communication';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
import WindowManager from '../windows/windowManager';
|
||||
import * as AppState from '../appState';
|
||||
|
||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
|
@@ -27,7 +27,7 @@ import {TabView} from 'common/tabs/TabView';
|
||||
import {ServerInfo} from 'main/server/serverInfo';
|
||||
import ContextMenu from '../contextMenu';
|
||||
import {getWindowBoundaries, getLocalPreload, composeUserAgent} from '../utils';
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
import WindowManager from '../windows/windowManager';
|
||||
import * as appState from '../appState';
|
||||
|
||||
import WebContentsEventManager from './webContentEvents';
|
||||
|
@@ -17,7 +17,7 @@ import {
|
||||
GET_MODAL_UNCLOSEABLE,
|
||||
} from 'common/communication';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
import WindowManager from '../windows/windowManager';
|
||||
|
||||
import {ModalView} from './modalView';
|
||||
|
||||
|
@@ -17,7 +17,7 @@ import {
|
||||
import * as AppState from '../appState';
|
||||
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants';
|
||||
import {getLocalPreload, getLocalURLString} from 'main/utils';
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
import WindowManager from '../windows/windowManager';
|
||||
|
||||
export default class TeamDropdownView {
|
||||
view: BrowserView;
|
||||
|
@@ -10,7 +10,7 @@ import urlUtils from 'common/utils/url';
|
||||
|
||||
import ContextMenu from 'main/contextMenu';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
import WindowManager from '../windows/windowManager';
|
||||
|
||||
import {protocols} from '../../../electron-builder.json';
|
||||
|
||||
|
341
src/main/windows/mainWindow.test.js
Normal file
341
src/main/windows/mainWindow.test.js
Normal file
@@ -0,0 +1,341 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import {BrowserWindow, screen, app, globalShortcut} from 'electron';
|
||||
|
||||
import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB} from 'common/communication';
|
||||
import {DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH} from 'common/utils/constants';
|
||||
|
||||
import ContextMenu from '../contextMenu';
|
||||
import * as Validator from '../Validator';
|
||||
|
||||
import createMainWindow from './mainWindow';
|
||||
|
||||
jest.mock('path', () => ({
|
||||
join: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
app: {
|
||||
getPath: jest.fn(),
|
||||
hide: jest.fn(),
|
||||
},
|
||||
BrowserWindow: jest.fn(),
|
||||
ipcMain: {
|
||||
handle: jest.fn(),
|
||||
},
|
||||
screen: {
|
||||
getDisplayMatching: jest.fn(),
|
||||
},
|
||||
globalShortcut: {
|
||||
register: jest.fn(),
|
||||
registerAll: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('electron-log', () => ({}));
|
||||
|
||||
jest.mock('global', () => ({
|
||||
willAppQuit: false,
|
||||
}));
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
readFileSync: jest.fn(),
|
||||
writeFileSync: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../Validator', () => ({
|
||||
validateBoundsInfo: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../contextMenu', () => jest.fn());
|
||||
|
||||
jest.mock('../utils', () => ({
|
||||
getLocalPreload: jest.fn(),
|
||||
getLocalURLString: jest.fn(),
|
||||
}));
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('main/windows/mainWindow', () => {
|
||||
describe('createMainWindow', () => {
|
||||
const baseWindow = {
|
||||
setMenuBarVisibility: jest.fn(),
|
||||
loadURL: jest.fn(),
|
||||
once: jest.fn(),
|
||||
on: jest.fn(),
|
||||
maximize: jest.fn(),
|
||||
show: jest.fn(),
|
||||
hide: jest.fn(),
|
||||
blur: jest.fn(),
|
||||
minimize: jest.fn(),
|
||||
webContents: {
|
||||
on: jest.fn(),
|
||||
send: jest.fn(),
|
||||
},
|
||||
isMaximized: jest.fn(),
|
||||
isFullScreen: jest.fn(),
|
||||
getBounds: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
baseWindow.loadURL.mockImplementation(() => ({
|
||||
catch: jest.fn(),
|
||||
}));
|
||||
BrowserWindow.mockImplementation(() => baseWindow);
|
||||
fs.readFileSync.mockImplementation(() => '{"x":400,"y":300,"width":1280,"height":700,"maximized":false,"fullscreen":false}');
|
||||
path.join.mockImplementation(() => 'anyfile.txt');
|
||||
screen.getDisplayMatching.mockImplementation(() => ({bounds: {x: 0, y: 0, width: 1920, height: 1080}}));
|
||||
Validator.validateBoundsInfo.mockImplementation((data) => data);
|
||||
ContextMenu.mockImplementation(() => ({
|
||||
reload: jest.fn(),
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should set window size using bounds read from file', () => {
|
||||
createMainWindow({}, {});
|
||||
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
||||
x: 400,
|
||||
y: 300,
|
||||
width: 1280,
|
||||
height: 700,
|
||||
maximized: false,
|
||||
fullscreen: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should set default window size when failing to read bounds from file', () => {
|
||||
fs.readFileSync.mockImplementation(() => 'just a bunch of garbage');
|
||||
createMainWindow({}, {});
|
||||
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
||||
width: DEFAULT_WINDOW_WIDTH,
|
||||
height: DEFAULT_WINDOW_HEIGHT,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should set default window size when bounds are outside the normal screen', () => {
|
||||
fs.readFileSync.mockImplementation(() => '{"x":-400,"y":-300,"width":1280,"height":700,"maximized":false,"fullscreen":false}');
|
||||
screen.getDisplayMatching.mockImplementation(() => ({bounds: {x: 0, y: 0, width: 1920, height: 1080}}));
|
||||
createMainWindow({}, {});
|
||||
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
||||
width: DEFAULT_WINDOW_WIDTH,
|
||||
height: DEFAULT_WINDOW_HEIGHT,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should set linux app icon', () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'linux',
|
||||
});
|
||||
createMainWindow({}, {linuxAppIcon: 'linux-icon.png'});
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
||||
icon: 'linux-icon.png',
|
||||
}));
|
||||
});
|
||||
|
||||
it('should reset zoom level and maximize if applicable on ready-to-show', () => {
|
||||
const window = {
|
||||
...baseWindow,
|
||||
once: jest.fn().mockImplementation((event, cb) => {
|
||||
if (event === 'ready-to-show') {
|
||||
cb();
|
||||
}
|
||||
}),
|
||||
};
|
||||
BrowserWindow.mockImplementation(() => window);
|
||||
fs.readFileSync.mockImplementation(() => '{"x":400,"y":300,"width":1280,"height":700,"maximized":true,"fullscreen":false}');
|
||||
createMainWindow({}, {});
|
||||
expect(window.webContents.zoomLevel).toStrictEqual(0);
|
||||
expect(window.maximize).toBeCalled();
|
||||
});
|
||||
|
||||
it('should save window state on close if the app will quit', () => {
|
||||
global.willAppQuit = true;
|
||||
const window = {
|
||||
...baseWindow,
|
||||
on: jest.fn().mockImplementation((event, cb) => {
|
||||
if (event === 'close') {
|
||||
cb({preventDefault: jest.fn()});
|
||||
}
|
||||
}),
|
||||
};
|
||||
BrowserWindow.mockImplementation(() => window);
|
||||
createMainWindow({}, {});
|
||||
global.willAppQuit = false;
|
||||
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should hide window on close for Windows if app wont quit', () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'win32',
|
||||
});
|
||||
const window = {
|
||||
...baseWindow,
|
||||
on: jest.fn().mockImplementation((event, cb) => {
|
||||
if (event === 'close') {
|
||||
cb({preventDefault: jest.fn()});
|
||||
}
|
||||
}),
|
||||
};
|
||||
BrowserWindow.mockImplementation(() => window);
|
||||
createMainWindow({}, {});
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(window.hide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should hide window on close for Linux if app wont quit and config item is set', () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'linux',
|
||||
});
|
||||
const window = {
|
||||
...baseWindow,
|
||||
on: jest.fn().mockImplementation((event, cb) => {
|
||||
if (event === 'close') {
|
||||
cb({preventDefault: jest.fn()});
|
||||
}
|
||||
}),
|
||||
};
|
||||
BrowserWindow.mockImplementation(() => window);
|
||||
createMainWindow({minimizeToTray: true}, {});
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(window.hide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should minimize window on close for Linux if app wont quit and config item is not set', () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'linux',
|
||||
});
|
||||
const window = {
|
||||
...baseWindow,
|
||||
on: jest.fn().mockImplementation((event, cb) => {
|
||||
if (event === 'close') {
|
||||
cb({preventDefault: jest.fn()});
|
||||
}
|
||||
}),
|
||||
};
|
||||
BrowserWindow.mockImplementation(() => window);
|
||||
createMainWindow({}, {});
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(window.minimize).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should hide window on close for Mac if app wont quit and window is not full screen', () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'darwin',
|
||||
});
|
||||
const window = {
|
||||
...baseWindow,
|
||||
on: jest.fn().mockImplementation((event, cb) => {
|
||||
if (event === 'close') {
|
||||
cb({preventDefault: jest.fn()});
|
||||
}
|
||||
}),
|
||||
};
|
||||
BrowserWindow.mockImplementation(() => window);
|
||||
createMainWindow({}, {});
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(app.hide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should leave full screen and then hide window on close for Mac if app wont quit and window is full screen', () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'darwin',
|
||||
});
|
||||
const window = {
|
||||
...baseWindow,
|
||||
isFullScreen: jest.fn().mockImplementation(() => true),
|
||||
setFullScreen: jest.fn(),
|
||||
once: jest.fn().mockImplementation((event, cb) => {
|
||||
if (event === 'leave-full-screen') {
|
||||
cb();
|
||||
}
|
||||
}),
|
||||
on: jest.fn().mockImplementation((event, cb) => {
|
||||
if (event === 'close') {
|
||||
cb({preventDefault: jest.fn()});
|
||||
}
|
||||
}),
|
||||
};
|
||||
BrowserWindow.mockImplementation(() => window);
|
||||
createMainWindow({}, {});
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(window.once).toHaveBeenCalledWith('leave-full-screen', expect.any(Function));
|
||||
expect(app.hide).toHaveBeenCalled();
|
||||
expect(window.setFullScreen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should select tabs using alt+cmd+arrow keys on Mac', () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'darwin',
|
||||
});
|
||||
const window = {
|
||||
...baseWindow,
|
||||
webContents: {
|
||||
...baseWindow.webContents,
|
||||
on: jest.fn().mockImplementation((event, cb) => {
|
||||
if (event === 'before-input-event') {
|
||||
cb(null, {alt: true, meta: true, key: 'ArrowRight'});
|
||||
cb(null, {alt: true, meta: true, key: 'ArrowLeft'});
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
BrowserWindow.mockImplementation(() => window);
|
||||
createMainWindow({}, {});
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(window.webContents.send).toHaveBeenCalledWith(SELECT_NEXT_TAB);
|
||||
expect(window.webContents.send).toHaveBeenCalledWith(SELECT_PREVIOUS_TAB);
|
||||
});
|
||||
|
||||
it('should add override shortcuts for the top menu on Linux to stop it showing up', () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'linux',
|
||||
});
|
||||
const window = {
|
||||
...baseWindow,
|
||||
on: jest.fn().mockImplementation((event, cb) => {
|
||||
if (event === 'focus') {
|
||||
cb();
|
||||
}
|
||||
}),
|
||||
};
|
||||
BrowserWindow.mockImplementation(() => window);
|
||||
createMainWindow({}, {});
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(globalShortcut.registerAll).toHaveBeenCalledWith(['Alt+F', 'Alt+E', 'Alt+V', 'Alt+H', 'Alt+W', 'Alt+P'], expect.any(Function));
|
||||
});
|
||||
});
|
||||
});
|
815
src/main/windows/windowManager.test.js
Normal file
815
src/main/windows/windowManager.test.js
Normal file
@@ -0,0 +1,815 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
'use strict';
|
||||
|
||||
import {app, systemPreferences} from 'electron';
|
||||
|
||||
import {getTabViewName, TAB_MESSAGING} from 'common/tabs/TabView';
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
import {getAdjustedWindowBoundaries} from 'main/utils';
|
||||
|
||||
import {WindowManager} from './windowManager';
|
||||
import createMainWindow from './mainWindow';
|
||||
import {createSettingsWindow} from './settingsWindow';
|
||||
|
||||
jest.mock('path', () => ({
|
||||
resolve: jest.fn(),
|
||||
join: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: jest.fn(),
|
||||
on: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
},
|
||||
app: {
|
||||
getAppPath: jest.fn(),
|
||||
quit: jest.fn(),
|
||||
dock: {
|
||||
show: jest.fn(),
|
||||
bounce: jest.fn(),
|
||||
},
|
||||
},
|
||||
systemPreferences: {
|
||||
getUserDefault: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('electron-log', () => ({
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('common/utils/url', () => ({
|
||||
isTeamUrl: jest.fn(),
|
||||
isAdminUrl: jest.fn(),
|
||||
getView: jest.fn(),
|
||||
}));
|
||||
jest.mock('common/tabs/TabView', () => ({
|
||||
getTabViewName: jest.fn(),
|
||||
TAB_MESSAGING: 'tab-messaging',
|
||||
}));
|
||||
jest.mock('../utils', () => ({
|
||||
getAdjustedWindowBoundaries: jest.fn(),
|
||||
}));
|
||||
jest.mock('../views/viewManager', () => ({
|
||||
ViewManager: jest.fn(),
|
||||
}));
|
||||
jest.mock('../CriticalErrorHandler', () => jest.fn());
|
||||
jest.mock('../views/teamDropdownView', () => jest.fn());
|
||||
jest.mock('./settingsWindow', () => ({
|
||||
createSettingsWindow: jest.fn(),
|
||||
}));
|
||||
jest.mock('./mainWindow', () => jest.fn());
|
||||
|
||||
describe('main/windows/windowManager', () => {
|
||||
describe('setConfig', () => {
|
||||
const windowManager = new WindowManager();
|
||||
|
||||
beforeEach(() => {
|
||||
windowManager.viewManager = {
|
||||
reloadConfiguration: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should reload config on set', () => {
|
||||
windowManager.setConfig({some: 'config item'});
|
||||
expect(windowManager.viewManager.reloadConfiguration).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('showSettingsWindow', () => {
|
||||
const windowManager = new WindowManager();
|
||||
windowManager.config = {};
|
||||
windowManager.showMainWindow = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
delete windowManager.settingsWindow;
|
||||
delete windowManager.mainWindow;
|
||||
});
|
||||
|
||||
it('should show settings window if it exists', () => {
|
||||
const settingsWindow = {show: jest.fn()};
|
||||
windowManager.settingsWindow = settingsWindow;
|
||||
windowManager.showSettingsWindow();
|
||||
expect(settingsWindow.show).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create windows if they dont exist and delete the settings window when it is closed', () => {
|
||||
let callback;
|
||||
createSettingsWindow.mockReturnValue({on: (event, cb) => {
|
||||
if (event === 'closed') {
|
||||
callback = cb;
|
||||
}
|
||||
}});
|
||||
windowManager.showSettingsWindow();
|
||||
expect(windowManager.showMainWindow).toHaveBeenCalled();
|
||||
expect(createSettingsWindow).toHaveBeenCalled();
|
||||
expect(windowManager.settingsWindow).toBeDefined();
|
||||
|
||||
callback();
|
||||
expect(windowManager.settingsWindow).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('showMainWindow', () => {
|
||||
const windowManager = new WindowManager();
|
||||
windowManager.config = {};
|
||||
windowManager.viewManager = {
|
||||
handleDeepLink: jest.fn(),
|
||||
updateMainWindow: jest.fn(),
|
||||
};
|
||||
windowManager.initializeViewManager = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
delete windowManager.mainWindow;
|
||||
});
|
||||
|
||||
it('should show main window if it exists and focus it if it is already visible', () => {
|
||||
windowManager.mainWindow = {
|
||||
visible: false,
|
||||
isVisible: () => windowManager.mainWindow.visible,
|
||||
show: jest.fn().mockImplementation(() => {
|
||||
windowManager.mainWindow.visible = true;
|
||||
}),
|
||||
focus: jest.fn(),
|
||||
};
|
||||
|
||||
windowManager.showMainWindow();
|
||||
expect(windowManager.mainWindow.show).toHaveBeenCalled();
|
||||
|
||||
windowManager.showMainWindow();
|
||||
expect(windowManager.mainWindow.focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should quit the app when the main window fails to create', () => {
|
||||
windowManager.showMainWindow();
|
||||
expect(app.quit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create the main window and add listeners', () => {
|
||||
const window = {
|
||||
on: jest.fn(),
|
||||
};
|
||||
createMainWindow.mockReturnValue(window);
|
||||
windowManager.showMainWindow();
|
||||
expect(windowManager.mainWindow).toBe(window);
|
||||
expect(window.on).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should open deep link when provided', () => {
|
||||
const window = {
|
||||
on: jest.fn(),
|
||||
};
|
||||
createMainWindow.mockReturnValue(window);
|
||||
windowManager.showMainWindow('mattermost://server-1.com/subpath');
|
||||
expect(windowManager.viewManager.handleDeepLink).toHaveBeenCalledWith('mattermost://server-1.com/subpath');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleResizeMainWindow', () => {
|
||||
const windowManager = new WindowManager();
|
||||
const view = {
|
||||
setBounds: jest.fn(),
|
||||
tab: {
|
||||
url: 'http://server-1.com',
|
||||
},
|
||||
view: {
|
||||
webContents: {
|
||||
getURL: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
windowManager.viewManager = {
|
||||
getCurrentView: () => view,
|
||||
setLoadingScreenBounds: jest.fn(),
|
||||
};
|
||||
windowManager.mainWindow = {
|
||||
getContentBounds: () => ({width: 800, height: 600}),
|
||||
getSize: () => [1000, 900],
|
||||
};
|
||||
windowManager.teamDropdown = {
|
||||
updateWindowBounds: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should update loading screen and team dropdown bounds', () => {
|
||||
windowManager.handleResizeMainWindow();
|
||||
expect(windowManager.viewManager.setLoadingScreenBounds).toHaveBeenCalled();
|
||||
expect(windowManager.teamDropdown.updateWindowBounds).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use getContentBounds when the platform is not linux', () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'win32',
|
||||
});
|
||||
windowManager.handleResizeMainWindow();
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(view.setBounds).toHaveBeenCalledWith({width: 800, height: 600});
|
||||
});
|
||||
|
||||
it('should use getSize when the platform is linux', () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'linux',
|
||||
});
|
||||
windowManager.handleResizeMainWindow();
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(view.setBounds).not.toHaveBeenCalled();
|
||||
jest.runAllTimers();
|
||||
expect(view.setBounds).toHaveBeenCalledWith({width: 1000, height: 900});
|
||||
});
|
||||
});
|
||||
|
||||
describe('restoreMain', () => {
|
||||
const windowManager = new WindowManager();
|
||||
windowManager.mainWindow = {
|
||||
isVisible: jest.fn(),
|
||||
isMinimized: jest.fn(),
|
||||
restore: jest.fn(),
|
||||
show: jest.fn(),
|
||||
focus: jest.fn(),
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
delete windowManager.settingsWindow;
|
||||
});
|
||||
|
||||
it('should restore main window if minimized', () => {
|
||||
windowManager.mainWindow.isMinimized.mockReturnValue(true);
|
||||
windowManager.restoreMain();
|
||||
expect(windowManager.mainWindow.restore).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show main window if not visible or minimized', () => {
|
||||
windowManager.mainWindow.isVisible.mockReturnValue(false);
|
||||
windowManager.mainWindow.isMinimized.mockReturnValue(false);
|
||||
windowManager.restoreMain();
|
||||
expect(windowManager.mainWindow.show).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should focus main window if visible and not minimized', () => {
|
||||
windowManager.mainWindow.isVisible.mockReturnValue(true);
|
||||
windowManager.mainWindow.isMinimized.mockReturnValue(false);
|
||||
windowManager.restoreMain();
|
||||
expect(windowManager.mainWindow.focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should focus settings window regardless of main window state if it exists', () => {
|
||||
windowManager.settingsWindow = {
|
||||
focus: jest.fn(),
|
||||
};
|
||||
|
||||
windowManager.mainWindow.isVisible.mockReturnValue(false);
|
||||
windowManager.mainWindow.isMinimized.mockReturnValue(false);
|
||||
windowManager.restoreMain();
|
||||
expect(windowManager.settingsWindow.focus).toHaveBeenCalled();
|
||||
windowManager.settingsWindow.focus.mockClear();
|
||||
|
||||
windowManager.mainWindow.isVisible.mockReturnValue(true);
|
||||
windowManager.mainWindow.isMinimized.mockReturnValue(false);
|
||||
windowManager.restoreMain();
|
||||
expect(windowManager.settingsWindow.focus).toHaveBeenCalled();
|
||||
windowManager.settingsWindow.focus.mockClear();
|
||||
|
||||
windowManager.mainWindow.isVisible.mockReturnValue(false);
|
||||
windowManager.mainWindow.isMinimized.mockReturnValue(true);
|
||||
windowManager.restoreMain();
|
||||
expect(windowManager.settingsWindow.focus).toHaveBeenCalled();
|
||||
windowManager.settingsWindow.focus.mockClear();
|
||||
|
||||
windowManager.mainWindow.isVisible.mockReturnValue(true);
|
||||
windowManager.mainWindow.isMinimized.mockReturnValue(true);
|
||||
windowManager.restoreMain();
|
||||
expect(windowManager.settingsWindow.focus).toHaveBeenCalled();
|
||||
windowManager.settingsWindow.focus.mockClear();
|
||||
});
|
||||
|
||||
it('should call macOS show on macOS', () => {
|
||||
windowManager.mainWindow.isVisible.mockReturnValue(false);
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'darwin',
|
||||
});
|
||||
windowManager.restoreMain();
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(app.dock.show).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('flashFrame', () => {
|
||||
const windowManager = new WindowManager();
|
||||
windowManager.mainWindow = {
|
||||
flashFrame: jest.fn(),
|
||||
};
|
||||
windowManager.settingsWindow = {
|
||||
flashFrame: jest.fn(),
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
delete windowManager.config;
|
||||
});
|
||||
|
||||
it('linux/windows - should not flash frame when config item is not set', () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'linux',
|
||||
});
|
||||
windowManager.flashFrame(true);
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(windowManager.mainWindow.flashFrame).not.toBeCalled();
|
||||
expect(windowManager.settingsWindow.flashFrame).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('linux/windows - should flash frame when config item is set', () => {
|
||||
windowManager.config = {
|
||||
notifications: {
|
||||
flashWindow: true,
|
||||
},
|
||||
};
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'linux',
|
||||
});
|
||||
windowManager.flashFrame(true);
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(windowManager.mainWindow.flashFrame).toBeCalledWith(true);
|
||||
expect(windowManager.settingsWindow.flashFrame).toBeCalledWith(true);
|
||||
});
|
||||
|
||||
it('mac - should not bounce icon when config item is not set', () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'darwin',
|
||||
});
|
||||
windowManager.flashFrame(true);
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(app.dock.bounce).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('mac - should bounce icon when config item is set', () => {
|
||||
windowManager.config = {
|
||||
notifications: {
|
||||
bounceIcon: true,
|
||||
bounceIconType: 'critical',
|
||||
},
|
||||
};
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'darwin',
|
||||
});
|
||||
windowManager.flashFrame(true);
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
expect(app.dock.bounce).toHaveBeenCalledWith('critical');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDoubleClick', () => {
|
||||
const windowManager = new WindowManager();
|
||||
const mainWindow = {
|
||||
isMinimized: jest.fn(),
|
||||
restore: jest.fn(),
|
||||
minimize: jest.fn(),
|
||||
isMaximized: jest.fn(),
|
||||
unmaximize: jest.fn(),
|
||||
maximize: jest.fn(),
|
||||
};
|
||||
const settingsWindow = {
|
||||
isMinimized: jest.fn(),
|
||||
restore: jest.fn(),
|
||||
minimize: jest.fn(),
|
||||
isMaximized: jest.fn(),
|
||||
unmaximize: jest.fn(),
|
||||
maximize: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
systemPreferences.getUserDefault.mockReturnValue('Maximize');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
delete windowManager.mainWindow;
|
||||
delete windowManager.settingsWindow;
|
||||
});
|
||||
|
||||
it('should do nothing when the windows arent set', () => {
|
||||
windowManager.handleDoubleClick(null, 'settings');
|
||||
expect(settingsWindow.isMaximized).not.toHaveBeenCalled();
|
||||
|
||||
windowManager.handleDoubleClick();
|
||||
expect(mainWindow.isMaximized).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should maximize when not maximized and vice versa', () => {
|
||||
windowManager.mainWindow = mainWindow;
|
||||
|
||||
windowManager.mainWindow.isMaximized.mockReturnValue(false);
|
||||
windowManager.handleDoubleClick();
|
||||
expect(mainWindow.maximize).toHaveBeenCalled();
|
||||
|
||||
windowManager.mainWindow.isMaximized.mockReturnValue(true);
|
||||
windowManager.handleDoubleClick();
|
||||
expect(mainWindow.unmaximize).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('mac - should minimize when not minimized and vice versa when setting is set', () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'darwin',
|
||||
});
|
||||
windowManager.flashFrame(true);
|
||||
|
||||
systemPreferences.getUserDefault.mockReturnValue('Minimize');
|
||||
windowManager.settingsWindow = settingsWindow;
|
||||
|
||||
windowManager.settingsWindow.isMinimized.mockReturnValue(false);
|
||||
windowManager.handleDoubleClick(null, 'settings');
|
||||
expect(settingsWindow.minimize).toHaveBeenCalled();
|
||||
|
||||
windowManager.settingsWindow.isMinimized.mockReturnValue(true);
|
||||
windowManager.handleDoubleClick(null, 'settings');
|
||||
expect(settingsWindow.restore).toHaveBeenCalled();
|
||||
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('switchServer', () => {
|
||||
const windowManager = new WindowManager();
|
||||
windowManager.config = {
|
||||
teams: [
|
||||
{
|
||||
name: 'server-1',
|
||||
order: 1,
|
||||
tabs: [
|
||||
{
|
||||
name: 'tab-1',
|
||||
order: 0,
|
||||
isOpen: false,
|
||||
},
|
||||
{
|
||||
name: 'tab-2',
|
||||
order: 2,
|
||||
isOpen: true,
|
||||
},
|
||||
{
|
||||
name: 'tab-3',
|
||||
order: 1,
|
||||
isOpen: true,
|
||||
},
|
||||
],
|
||||
}, {
|
||||
name: 'server-2',
|
||||
order: 0,
|
||||
tabs: [
|
||||
{
|
||||
name: 'tab-1',
|
||||
order: 0,
|
||||
isOpen: false,
|
||||
},
|
||||
{
|
||||
name: 'tab-2',
|
||||
order: 2,
|
||||
isOpen: true,
|
||||
},
|
||||
{
|
||||
name: 'tab-3',
|
||||
order: 1,
|
||||
isOpen: true,
|
||||
},
|
||||
],
|
||||
lastActiveTab: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
windowManager.viewManager = {
|
||||
showByName: jest.fn(),
|
||||
};
|
||||
const map = windowManager.config.teams.reduce((arr, item) => {
|
||||
item.tabs.forEach((tab) => {
|
||||
arr.push([`${item.name}_${tab.name}`, {}]);
|
||||
});
|
||||
return arr;
|
||||
}, []);
|
||||
windowManager.viewManager.views = new Map(map);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
getTabViewName.mockImplementation((server, tab) => `${server}_${tab}`);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should do nothing if cannot find the server', () => {
|
||||
windowManager.switchServer('server-3');
|
||||
expect(getTabViewName).not.toBeCalled();
|
||||
expect(windowManager.viewManager.showByName).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should show first open tab in order when last active not defined', () => {
|
||||
windowManager.switchServer('server-1');
|
||||
expect(windowManager.viewManager.showByName).toHaveBeenCalledWith('server-1_tab-3');
|
||||
});
|
||||
|
||||
it('should show last active tab of chosen server', () => {
|
||||
windowManager.switchServer('server-2');
|
||||
expect(windowManager.viewManager.showByName).toHaveBeenCalledWith('server-2_tab-2');
|
||||
});
|
||||
|
||||
it('should wait for view to exist if specified', () => {
|
||||
windowManager.viewManager.views.delete('server-1_tab-3');
|
||||
windowManager.switchServer('server-1', true);
|
||||
expect(windowManager.viewManager.showByName).not.toBeCalled();
|
||||
|
||||
jest.advanceTimersByTime(200);
|
||||
expect(windowManager.viewManager.showByName).not.toBeCalled();
|
||||
|
||||
windowManager.viewManager.views.set('server-1_tab-3', {});
|
||||
jest.advanceTimersByTime(200);
|
||||
expect(windowManager.viewManager.showByName).toBeCalledWith('server-1_tab-3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleHistory', () => {
|
||||
const windowManager = new WindowManager();
|
||||
windowManager.viewManager = {
|
||||
getCurrentView: jest.fn(),
|
||||
};
|
||||
|
||||
it('should only go to offset if it can', () => {
|
||||
const view = {
|
||||
view: {
|
||||
webContents: {
|
||||
goToOffset: jest.fn(),
|
||||
canGoToOffset: () => false,
|
||||
},
|
||||
},
|
||||
};
|
||||
windowManager.viewManager.getCurrentView.mockReturnValue(view);
|
||||
|
||||
windowManager.handleHistory(null, 1);
|
||||
expect(view.view.webContents.goToOffset).not.toBeCalled();
|
||||
|
||||
windowManager.viewManager.getCurrentView.mockReturnValue({
|
||||
...view,
|
||||
view: {
|
||||
...view.view,
|
||||
webContents: {
|
||||
...view.view.webContents,
|
||||
canGoToOffset: () => true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
windowManager.handleHistory(null, 1);
|
||||
expect(view.view.webContents.goToOffset).toBeCalled();
|
||||
});
|
||||
|
||||
it('should load base URL if an error occurs', () => {
|
||||
const view = {
|
||||
load: jest.fn(),
|
||||
tab: {
|
||||
url: 'http://server-1.com',
|
||||
},
|
||||
view: {
|
||||
webContents: {
|
||||
goToOffset: jest.fn(),
|
||||
canGoToOffset: () => true,
|
||||
},
|
||||
},
|
||||
};
|
||||
view.view.webContents.goToOffset.mockImplementation(() => {
|
||||
throw new Error('hi');
|
||||
});
|
||||
windowManager.viewManager.getCurrentView.mockReturnValue(view);
|
||||
|
||||
windowManager.handleHistory(null, 1);
|
||||
expect(view.load).toBeCalledWith('http://server-1.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectTab', () => {
|
||||
const windowManager = new WindowManager();
|
||||
windowManager.config = {
|
||||
teams: [
|
||||
{
|
||||
name: 'server-1',
|
||||
order: 1,
|
||||
tabs: [
|
||||
{
|
||||
name: 'tab-1',
|
||||
order: 0,
|
||||
isOpen: false,
|
||||
},
|
||||
{
|
||||
name: 'tab-2',
|
||||
order: 2,
|
||||
isOpen: true,
|
||||
},
|
||||
{
|
||||
name: 'tab-3',
|
||||
order: 1,
|
||||
isOpen: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
windowManager.viewManager = {
|
||||
getCurrentView: jest.fn(),
|
||||
};
|
||||
windowManager.switchTab = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should select next server when open', () => {
|
||||
windowManager.viewManager.getCurrentView.mockReturnValue({
|
||||
tab: {
|
||||
server: {
|
||||
name: 'server-1',
|
||||
},
|
||||
type: 'tab-3',
|
||||
},
|
||||
});
|
||||
|
||||
windowManager.selectTab((order) => order + 1);
|
||||
expect(windowManager.switchTab).toBeCalledWith('server-1', 'tab-2');
|
||||
});
|
||||
|
||||
it('should select previous server when open', () => {
|
||||
windowManager.viewManager.getCurrentView.mockReturnValue({
|
||||
tab: {
|
||||
server: {
|
||||
name: 'server-1',
|
||||
},
|
||||
type: 'tab-2',
|
||||
},
|
||||
});
|
||||
|
||||
windowManager.selectTab((order, length) => (length + (order - 1)));
|
||||
expect(windowManager.switchTab).toBeCalledWith('server-1', 'tab-3');
|
||||
});
|
||||
|
||||
it('should skip over closed tab', () => {
|
||||
windowManager.viewManager.getCurrentView.mockReturnValue({
|
||||
tab: {
|
||||
server: {
|
||||
name: 'server-1',
|
||||
},
|
||||
type: 'tab-2',
|
||||
},
|
||||
});
|
||||
|
||||
windowManager.selectTab((order) => order + 1);
|
||||
expect(windowManager.switchTab).toBeCalledWith('server-1', 'tab-3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleBrowserHistoryPush', () => {
|
||||
const windowManager = new WindowManager();
|
||||
windowManager.config = {
|
||||
teams: [
|
||||
{
|
||||
name: 'server-1',
|
||||
url: 'http://server-1.com',
|
||||
order: 0,
|
||||
tabs: [
|
||||
{
|
||||
name: 'tab-messaging',
|
||||
order: 0,
|
||||
isOpen: true,
|
||||
},
|
||||
{
|
||||
name: 'other_type_1',
|
||||
order: 2,
|
||||
isOpen: true,
|
||||
},
|
||||
{
|
||||
name: 'other_type_2',
|
||||
order: 1,
|
||||
isOpen: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const view1 = {
|
||||
name: 'server-1_tab-messaging',
|
||||
isLoggedIn: true,
|
||||
tab: {
|
||||
type: TAB_MESSAGING,
|
||||
server: {
|
||||
url: 'http://server-1.com',
|
||||
},
|
||||
},
|
||||
view: {
|
||||
webContents: {
|
||||
send: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
const view2 = {
|
||||
...view1,
|
||||
name: 'server-1_other_type_1',
|
||||
tab: {
|
||||
...view1.tab,
|
||||
type: 'other_type_1',
|
||||
},
|
||||
view: {
|
||||
webContents: {
|
||||
send: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
const view3 = {
|
||||
...view1,
|
||||
name: 'server-1_other_type_2',
|
||||
tab: {
|
||||
...view1.tab,
|
||||
type: 'other_type_2',
|
||||
},
|
||||
view: {
|
||||
webContents: {
|
||||
send: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
windowManager.viewManager = {
|
||||
views: new Map([
|
||||
['server-1_tab-messaging', view1],
|
||||
['server-1_other_type_1', view2],
|
||||
]),
|
||||
closedViews: new Map([
|
||||
['server-1_other_type_2', view3],
|
||||
]),
|
||||
openClosedTab: jest.fn(),
|
||||
showByName: jest.fn(),
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should open closed view if pushing to it', () => {
|
||||
urlUtils.getView.mockReturnValue({name: 'server-1_other_type_2'});
|
||||
windowManager.viewManager.openClosedTab.mockImplementation((name) => {
|
||||
const view = windowManager.viewManager.closedViews.get(name);
|
||||
windowManager.viewManager.closedViews.delete(name);
|
||||
windowManager.viewManager.views.set(name, view);
|
||||
});
|
||||
|
||||
windowManager.handleBrowserHistoryPush(null, 'server-1_tab-messaging', '/other_type_2/subpath');
|
||||
expect(windowManager.viewManager.openClosedTab).toBeCalledWith('server-1_other_type_2', 'http://server-1.com/other_type_2/subpath');
|
||||
expect(windowManager.viewManager.showByName).toBeCalledWith('server-1_other_type_2');
|
||||
});
|
||||
|
||||
it('should open redirect view if different from current view', () => {
|
||||
urlUtils.getView.mockReturnValue({name: 'server-1_other_type_1'});
|
||||
windowManager.handleBrowserHistoryPush(null, 'server-1_tab-messaging', '/other_type_1/subpath');
|
||||
expect(windowManager.viewManager.showByName).toBeCalledWith('server-1_other_type_1');
|
||||
});
|
||||
|
||||
it('should ignore redirects to "/" to Messages from other tabs', () => {
|
||||
urlUtils.getView.mockReturnValue({name: 'server-1_tab-messaging'});
|
||||
windowManager.handleBrowserHistoryPush(null, 'server-1_other_type_1', '/');
|
||||
expect(view1.view.webContents.send).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
import path from 'path';
|
||||
import {app, BrowserWindow, nativeImage, systemPreferences, ipcMain, IpcMainEvent, IpcMainInvokeEvent} from 'electron';
|
||||
import log from 'electron-log';
|
||||
@@ -38,141 +39,146 @@ import createMainWindow from './mainWindow';
|
||||
|
||||
// singleton module to manage application's windows
|
||||
|
||||
type WindowManagerStatus = {
|
||||
export class WindowManager {
|
||||
assetsDir: string;
|
||||
|
||||
mainWindow?: BrowserWindow;
|
||||
settingsWindow?: BrowserWindow;
|
||||
config?: CombinedConfig;
|
||||
viewManager?: ViewManager;
|
||||
teamDropdown?: TeamDropdownView;
|
||||
currentServerName?: string;
|
||||
};
|
||||
|
||||
const status: WindowManagerStatus = {};
|
||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
constructor() {
|
||||
this.assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
|
||||
ipcMain.on(HISTORY, handleHistory);
|
||||
ipcMain.handle(GET_LOADING_SCREEN_DATA, handleLoadingScreenDataRequest);
|
||||
ipcMain.handle(GET_DARK_MODE, handleGetDarkMode);
|
||||
ipcMain.on(REACT_APP_INITIALIZED, handleReactAppInitialized);
|
||||
ipcMain.on(LOADING_SCREEN_ANIMATION_FINISHED, handleLoadingScreenAnimationFinished);
|
||||
ipcMain.on(BROWSER_HISTORY_PUSH, handleBrowserHistoryPush);
|
||||
ipcMain.on(APP_LOGGED_IN, handleAppLoggedIn);
|
||||
ipcMain.on(APP_LOGGED_OUT, handleAppLoggedOut);
|
||||
ipcMain.handle(GET_VIEW_NAME, handleGetViewName);
|
||||
ipcMain.handle(GET_VIEW_WEBCONTENTS_ID, handleGetWebContentsId);
|
||||
ipcMain.on(HISTORY, this.handleHistory);
|
||||
ipcMain.handle(GET_LOADING_SCREEN_DATA, this.handleLoadingScreenDataRequest);
|
||||
ipcMain.handle(GET_DARK_MODE, this.handleGetDarkMode);
|
||||
ipcMain.on(REACT_APP_INITIALIZED, this.handleReactAppInitialized);
|
||||
ipcMain.on(LOADING_SCREEN_ANIMATION_FINISHED, this.handleLoadingScreenAnimationFinished);
|
||||
ipcMain.on(BROWSER_HISTORY_PUSH, this.handleBrowserHistoryPush);
|
||||
ipcMain.on(APP_LOGGED_IN, this.handleAppLoggedIn);
|
||||
ipcMain.on(APP_LOGGED_OUT, this.handleAppLoggedOut);
|
||||
ipcMain.handle(GET_VIEW_NAME, this.handleGetViewName);
|
||||
ipcMain.handle(GET_VIEW_WEBCONTENTS_ID, this.handleGetWebContentsId);
|
||||
}
|
||||
|
||||
export function setConfig(data: CombinedConfig) {
|
||||
setConfig = (data: CombinedConfig) => {
|
||||
if (data) {
|
||||
status.config = data;
|
||||
this.config = data;
|
||||
}
|
||||
if (this.viewManager && this.config) {
|
||||
this.viewManager.reloadConfiguration(this.config.teams || []);
|
||||
}
|
||||
if (status.viewManager && status.config) {
|
||||
status.viewManager.reloadConfiguration(status.config.teams || []);
|
||||
}
|
||||
}
|
||||
|
||||
export function showSettingsWindow() {
|
||||
if (status.settingsWindow) {
|
||||
status.settingsWindow.show();
|
||||
showSettingsWindow = () => {
|
||||
if (this.settingsWindow) {
|
||||
this.settingsWindow.show();
|
||||
} else {
|
||||
if (!status.mainWindow) {
|
||||
showMainWindow();
|
||||
if (!this.mainWindow) {
|
||||
this.showMainWindow();
|
||||
}
|
||||
const withDevTools = Boolean(process.env.MM_DEBUG_SETTINGS) || false;
|
||||
|
||||
if (!status.config) {
|
||||
if (!this.config) {
|
||||
return;
|
||||
}
|
||||
status.settingsWindow = createSettingsWindow(status.mainWindow!, status.config, withDevTools);
|
||||
status.settingsWindow.on('closed', () => {
|
||||
delete status.settingsWindow;
|
||||
this.settingsWindow = createSettingsWindow(this.mainWindow!, this.config, withDevTools);
|
||||
this.settingsWindow.on('closed', () => {
|
||||
delete this.settingsWindow;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function showMainWindow(deeplinkingURL?: string | URL) {
|
||||
if (status.mainWindow) {
|
||||
if (status.mainWindow.isVisible()) {
|
||||
status.mainWindow.focus();
|
||||
showMainWindow = (deeplinkingURL?: string | URL) => {
|
||||
if (this.mainWindow) {
|
||||
if (this.mainWindow.isVisible()) {
|
||||
this.mainWindow.focus();
|
||||
} else {
|
||||
status.mainWindow.show();
|
||||
this.mainWindow.show();
|
||||
}
|
||||
} else {
|
||||
if (!status.config) {
|
||||
if (!this.config) {
|
||||
return;
|
||||
}
|
||||
status.mainWindow = createMainWindow(status.config, {
|
||||
linuxAppIcon: path.join(assetsDir, 'linux', 'app_icon.png'),
|
||||
this.mainWindow = createMainWindow(this.config, {
|
||||
linuxAppIcon: path.join(this.assetsDir, 'linux', 'app_icon.png'),
|
||||
});
|
||||
|
||||
if (!status.mainWindow) {
|
||||
if (!this.mainWindow) {
|
||||
log.error('unable to create main window');
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
// window handlers
|
||||
status.mainWindow.on('closed', () => {
|
||||
this.mainWindow.on('closed', () => {
|
||||
log.warn('main window closed');
|
||||
delete status.mainWindow;
|
||||
delete this.mainWindow;
|
||||
});
|
||||
status.mainWindow.on('unresponsive', () => {
|
||||
this.mainWindow.on('unresponsive', () => {
|
||||
const criticalErrorHandler = new CriticalErrorHandler();
|
||||
criticalErrorHandler.setMainWindow(status.mainWindow!);
|
||||
criticalErrorHandler.setMainWindow(this.mainWindow!);
|
||||
criticalErrorHandler.windowUnresponsiveHandler();
|
||||
});
|
||||
status.mainWindow.on('maximize', handleMaximizeMainWindow);
|
||||
status.mainWindow.on('unmaximize', handleUnmaximizeMainWindow);
|
||||
status.mainWindow.on('resize', handleResizeMainWindow);
|
||||
status.mainWindow.on('focus', focusBrowserView);
|
||||
status.mainWindow.on('enter-full-screen', () => sendToRenderer('enter-full-screen'));
|
||||
status.mainWindow.on('leave-full-screen', () => sendToRenderer('leave-full-screen'));
|
||||
this.mainWindow.on('maximize', this.handleMaximizeMainWindow);
|
||||
this.mainWindow.on('unmaximize', this.handleUnmaximizeMainWindow);
|
||||
this.mainWindow.on('resize', this.handleResizeMainWindow);
|
||||
this.mainWindow.on('focus', this.focusBrowserView);
|
||||
this.mainWindow.on('enter-full-screen', () => this.sendToRenderer('enter-full-screen'));
|
||||
this.mainWindow.on('leave-full-screen', () => this.sendToRenderer('leave-full-screen'));
|
||||
|
||||
if (process.env.MM_DEBUG_SETTINGS) {
|
||||
status.mainWindow.webContents.openDevTools({mode: 'detach'});
|
||||
this.mainWindow.webContents.openDevTools({mode: 'detach'});
|
||||
}
|
||||
|
||||
if (status.viewManager) {
|
||||
status.viewManager.updateMainWindow(status.mainWindow);
|
||||
if (this.viewManager) {
|
||||
this.viewManager.updateMainWindow(this.mainWindow);
|
||||
}
|
||||
|
||||
status.teamDropdown = new TeamDropdownView(status.mainWindow, status.config.teams, status.config.darkMode, status.config.enableServerManagement);
|
||||
this.teamDropdown = new TeamDropdownView(this.mainWindow, this.config.teams, this.config.darkMode, this.config.enableServerManagement);
|
||||
}
|
||||
initializeViewManager();
|
||||
this.initializeViewManager();
|
||||
|
||||
if (deeplinkingURL) {
|
||||
status.viewManager!.handleDeepLink(deeplinkingURL);
|
||||
this.viewManager!.handleDeepLink(deeplinkingURL);
|
||||
}
|
||||
}
|
||||
|
||||
export function getMainWindow(ensureCreated?: boolean) {
|
||||
if (ensureCreated && !status.mainWindow) {
|
||||
showMainWindow();
|
||||
}
|
||||
return status.mainWindow;
|
||||
}
|
||||
|
||||
function handleMaximizeMainWindow() {
|
||||
sendToRenderer(MAXIMIZE_CHANGE, true);
|
||||
}
|
||||
getMainWindow = (ensureCreated?: boolean) => {
|
||||
if (ensureCreated && !this.mainWindow) {
|
||||
this.showMainWindow();
|
||||
}
|
||||
return this.mainWindow;
|
||||
}
|
||||
|
||||
function handleUnmaximizeMainWindow() {
|
||||
sendToRenderer(MAXIMIZE_CHANGE, false);
|
||||
}
|
||||
on = this.mainWindow?.on;
|
||||
|
||||
function handleResizeMainWindow() {
|
||||
if (!(status.viewManager && status.mainWindow)) {
|
||||
handleMaximizeMainWindow = () => {
|
||||
this.sendToRenderer(MAXIMIZE_CHANGE, true);
|
||||
}
|
||||
|
||||
handleUnmaximizeMainWindow = () => {
|
||||
this.sendToRenderer(MAXIMIZE_CHANGE, false);
|
||||
}
|
||||
|
||||
handleResizeMainWindow = () => {
|
||||
if (!(this.viewManager && this.mainWindow)) {
|
||||
return;
|
||||
}
|
||||
const currentView = status.viewManager.getCurrentView();
|
||||
const currentView = this.viewManager.getCurrentView();
|
||||
let bounds: Partial<Electron.Rectangle>;
|
||||
|
||||
// Workaround for linux maximizing/minimizing, which doesn't work properly because of these bugs:
|
||||
// https://github.com/electron/electron/issues/28699
|
||||
// https://github.com/electron/electron/issues/28106
|
||||
if (process.platform === 'linux') {
|
||||
const size = status.mainWindow.getSize();
|
||||
const size = this.mainWindow.getSize();
|
||||
bounds = {width: size[0], height: size[1]};
|
||||
} else {
|
||||
bounds = status.mainWindow.getContentBounds();
|
||||
bounds = this.mainWindow.getContentBounds();
|
||||
}
|
||||
|
||||
const setBoundsFunction = () => {
|
||||
@@ -188,68 +194,77 @@ function handleResizeMainWindow() {
|
||||
} else {
|
||||
setBoundsFunction();
|
||||
}
|
||||
status.viewManager.setLoadingScreenBounds();
|
||||
status.teamDropdown?.updateWindowBounds();
|
||||
}
|
||||
|
||||
export function sendToRenderer(channel: string, ...args: any[]) {
|
||||
if (!status.mainWindow) {
|
||||
showMainWindow();
|
||||
this.viewManager.setLoadingScreenBounds();
|
||||
this.teamDropdown?.updateWindowBounds();
|
||||
}
|
||||
status.mainWindow!.webContents.send(channel, ...args);
|
||||
if (status.settingsWindow && status.settingsWindow.isVisible()) {
|
||||
status.settingsWindow.webContents.send(channel, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export function sendToMattermostViews(channel: string, ...args: any[]) {
|
||||
if (status.viewManager) {
|
||||
status.viewManager.sendToAllViews(channel, ...args);
|
||||
sendToRenderer = (channel: string, ...args: any[]) => {
|
||||
if (!this.mainWindow) {
|
||||
this.showMainWindow();
|
||||
}
|
||||
this.mainWindow!.webContents.send(channel, ...args);
|
||||
if (this.settingsWindow && this.settingsWindow.isVisible()) {
|
||||
this.settingsWindow.webContents.send(channel, ...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function restoreMain() {
|
||||
sendToAll = (channel: string, ...args: any[]) => {
|
||||
this.sendToRenderer(channel, ...args);
|
||||
if (this.settingsWindow) {
|
||||
this.settingsWindow.webContents.send(channel, ...args);
|
||||
}
|
||||
|
||||
// TODO: should we include popups?
|
||||
}
|
||||
|
||||
sendToMattermostViews = (channel: string, ...args: any[]) => {
|
||||
if (this.viewManager) {
|
||||
this.viewManager.sendToAllViews(channel, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
restoreMain = () => {
|
||||
log.info('restoreMain');
|
||||
if (!status.mainWindow) {
|
||||
showMainWindow();
|
||||
if (!this.mainWindow) {
|
||||
this.showMainWindow();
|
||||
}
|
||||
if (!status.mainWindow!.isVisible() || status.mainWindow!.isMinimized()) {
|
||||
if (status.mainWindow!.isMinimized()) {
|
||||
status.mainWindow!.restore();
|
||||
if (!this.mainWindow!.isVisible() || this.mainWindow!.isMinimized()) {
|
||||
if (this.mainWindow!.isMinimized()) {
|
||||
this.mainWindow!.restore();
|
||||
} else {
|
||||
status.mainWindow!.show();
|
||||
this.mainWindow!.show();
|
||||
}
|
||||
if (status.settingsWindow) {
|
||||
status.settingsWindow.focus();
|
||||
if (this.settingsWindow) {
|
||||
this.settingsWindow.focus();
|
||||
} else {
|
||||
status.mainWindow!.focus();
|
||||
this.mainWindow!.focus();
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.show();
|
||||
}
|
||||
} else if (status.settingsWindow) {
|
||||
status.settingsWindow.focus();
|
||||
} else if (this.settingsWindow) {
|
||||
this.settingsWindow.focus();
|
||||
} else {
|
||||
status.mainWindow!.focus();
|
||||
this.mainWindow!.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function flashFrame(flash: boolean) {
|
||||
flashFrame = (flash: boolean) => {
|
||||
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||
if (status.config?.notifications.flashWindow) {
|
||||
status.mainWindow?.flashFrame(flash);
|
||||
if (status.settingsWindow) {
|
||||
if (this.config?.notifications.flashWindow) {
|
||||
this.mainWindow?.flashFrame(flash);
|
||||
if (this.settingsWindow) {
|
||||
// main might be hidden behind the settings
|
||||
status.settingsWindow.flashFrame(flash);
|
||||
this.settingsWindow.flashFrame(flash);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (process.platform === 'darwin' && status.config?.notifications.bounceIcon) {
|
||||
app.dock.bounce(status.config?.notifications.bounceIconType);
|
||||
if (process.platform === 'darwin' && this.config?.notifications.bounceIcon) {
|
||||
app.dock.bounce(this.config?.notifications.bounceIconType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawBadge(text: string, small: boolean) {
|
||||
drawBadge = (text: string, small: boolean) => {
|
||||
const scale = 2; // should rely display dpi
|
||||
const size = (small ? 20 : 16) * scale;
|
||||
const canvas = document.createElement('canvas');
|
||||
@@ -276,10 +291,10 @@ function drawBadge(text: string, small: boolean) {
|
||||
ctx.fillText(text, size / 2, size / 2, size);
|
||||
|
||||
return canvas.toDataURL();
|
||||
}
|
||||
}
|
||||
|
||||
function createDataURL(text: string, small: boolean) {
|
||||
const win = status.mainWindow;
|
||||
createDataURL = (text: string, small: boolean) => {
|
||||
const win = this.mainWindow;
|
||||
if (!win) {
|
||||
return null;
|
||||
}
|
||||
@@ -287,35 +302,39 @@ function createDataURL(text: string, small: boolean) {
|
||||
// since we don't have a document/canvas object in the main process, we use the webcontents from the window to draw.
|
||||
const safeSmall = Boolean(small);
|
||||
const code = `
|
||||
window.drawBadge = ${drawBadge};
|
||||
window.drawBadge = ${this.drawBadge};
|
||||
window.drawBadge('${text || ''}', ${safeSmall});
|
||||
`;
|
||||
return win.webContents.executeJavaScript(code);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setOverlayIcon(badgeText: string | undefined, description: string, small: boolean) {
|
||||
setOverlayIcon = async (badgeText: string | undefined, description: string, small: boolean) => {
|
||||
if (process.platform === 'win32') {
|
||||
let overlay = null;
|
||||
if (status.mainWindow) {
|
||||
if (this.mainWindow) {
|
||||
if (badgeText) {
|
||||
try {
|
||||
const dataUrl = await createDataURL(badgeText, small);
|
||||
const dataUrl = await this.createDataURL(badgeText, small);
|
||||
overlay = nativeImage.createFromDataURL(dataUrl);
|
||||
} catch (err) {
|
||||
log.error(`Couldn't generate a badge: ${err}`);
|
||||
}
|
||||
}
|
||||
status.mainWindow.setOverlayIcon(overlay, description);
|
||||
this.mainWindow.setOverlayIcon(overlay, description);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleDoubleClick(e: IpcMainEvent, windowType?: string) {
|
||||
isMainWindow = (window: BrowserWindow) => {
|
||||
return this.mainWindow && this.mainWindow === window;
|
||||
}
|
||||
|
||||
handleDoubleClick = (e: IpcMainEvent, windowType?: string) => {
|
||||
let action = 'Maximize';
|
||||
if (process.platform === 'darwin') {
|
||||
action = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
|
||||
}
|
||||
const win = (windowType === 'settings') ? status.settingsWindow : status.mainWindow;
|
||||
const win = (windowType === 'settings') ? this.settingsWindow : this.mainWindow;
|
||||
if (!win) {
|
||||
return;
|
||||
}
|
||||
@@ -336,123 +355,128 @@ export function handleDoubleClick(e: IpcMainEvent, windowType?: string) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function initializeViewManager() {
|
||||
if (!status.viewManager && status.config && status.mainWindow) {
|
||||
status.viewManager = new ViewManager(status.config, status.mainWindow);
|
||||
status.viewManager.load();
|
||||
status.viewManager.showInitial();
|
||||
initializeCurrentServerName();
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeCurrentServerName() {
|
||||
if (status.config && !status.currentServerName) {
|
||||
status.currentServerName = (status.config.teams.find((team) => team.order === status.config?.lastActiveTeam) || status.config.teams.find((team) => team.order === 0))?.name;
|
||||
initializeViewManager = () => {
|
||||
if (!this.viewManager && this.config && this.mainWindow) {
|
||||
this.viewManager = new ViewManager(this.config, this.mainWindow);
|
||||
this.viewManager.load();
|
||||
this.viewManager.showInitial();
|
||||
this.initializeCurrentServerName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function switchServer(serverName: string, waitForViewToExist = false) {
|
||||
showMainWindow();
|
||||
const server = status.config?.teams.find((team) => team.name === serverName);
|
||||
initializeCurrentServerName = () => {
|
||||
if (this.config && !this.currentServerName) {
|
||||
this.currentServerName = (this.config.teams.find((team) => team.order === this.config?.lastActiveTeam) || this.config.teams.find((team) => team.order === 0))?.name;
|
||||
}
|
||||
}
|
||||
|
||||
switchServer = (serverName: string, waitForViewToExist = false) => {
|
||||
this.showMainWindow();
|
||||
const server = this.config?.teams.find((team) => team.name === serverName);
|
||||
if (!server) {
|
||||
log.error('Cannot find server in config');
|
||||
return;
|
||||
}
|
||||
status.currentServerName = serverName;
|
||||
this.currentServerName = serverName;
|
||||
let nextTab = server.tabs.find((tab) => tab.isOpen && tab.order === (server.lastActiveTab || 0));
|
||||
if (!nextTab) {
|
||||
const openTabs = server.tabs.filter((tab) => tab.isOpen);
|
||||
nextTab = openTabs.find((e) => e.order === 0) || openTabs[0];
|
||||
nextTab = openTabs.find((e) => e.order === 0) || openTabs.concat().sort((a, b) => a.order - b.order)[0];
|
||||
}
|
||||
const tabViewName = getTabViewName(serverName, nextTab.name);
|
||||
if (waitForViewToExist) {
|
||||
const timeout = setInterval(() => {
|
||||
if (status.viewManager?.views.has(tabViewName)) {
|
||||
status.viewManager?.showByName(tabViewName);
|
||||
if (this.viewManager?.views.has(tabViewName)) {
|
||||
this.viewManager?.showByName(tabViewName);
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
status.viewManager?.showByName(tabViewName);
|
||||
this.viewManager?.showByName(tabViewName);
|
||||
}
|
||||
ipcMain.emit(UPDATE_SHORTCUT_MENU);
|
||||
}
|
||||
}
|
||||
|
||||
export function switchTab(serverName: string, tabName: string) {
|
||||
showMainWindow();
|
||||
switchTab = (serverName: string, tabName: string) => {
|
||||
this.showMainWindow();
|
||||
const tabViewName = getTabViewName(serverName, tabName);
|
||||
status.viewManager?.showByName(tabViewName);
|
||||
}
|
||||
this.viewManager?.showByName(tabViewName);
|
||||
}
|
||||
|
||||
export function focusBrowserView() {
|
||||
if (status.viewManager) {
|
||||
status.viewManager.focus();
|
||||
focusBrowserView = () => {
|
||||
if (this.viewManager) {
|
||||
this.viewManager.focus();
|
||||
} else {
|
||||
log.error('Trying to call focus when the viewmanager has not yet been initialized');
|
||||
}
|
||||
}
|
||||
|
||||
export function openBrowserViewDevTools() {
|
||||
if (status.viewManager) {
|
||||
status.viewManager.openViewDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
export function focusThreeDotMenu() {
|
||||
if (status.mainWindow) {
|
||||
status.mainWindow.webContents.focus();
|
||||
status.mainWindow.webContents.send(FOCUS_THREE_DOT_MENU);
|
||||
openBrowserViewDevTools = () => {
|
||||
if (this.viewManager) {
|
||||
this.viewManager.openViewDevTools();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleLoadingScreenDataRequest() {
|
||||
focusThreeDotMenu = () => {
|
||||
if (this.mainWindow) {
|
||||
this.mainWindow.webContents.focus();
|
||||
this.mainWindow.webContents.send(FOCUS_THREE_DOT_MENU);
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadingScreenDataRequest = () => {
|
||||
return {
|
||||
darkMode: status.config?.darkMode || false,
|
||||
darkMode: this.config?.darkMode || false,
|
||||
};
|
||||
}
|
||||
|
||||
function handleReactAppInitialized(e: IpcMainEvent, view: string) {
|
||||
if (status.viewManager) {
|
||||
status.viewManager.setServerInitialized(view);
|
||||
}
|
||||
}
|
||||
|
||||
function handleLoadingScreenAnimationFinished() {
|
||||
if (status.viewManager) {
|
||||
status.viewManager.hideLoadingScreen();
|
||||
handleReactAppInitialized = (e: IpcMainEvent, view: string) => {
|
||||
if (this.viewManager) {
|
||||
this.viewManager.setServerInitialized(view);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateLoadingScreenDarkMode(darkMode: boolean) {
|
||||
if (status.viewManager) {
|
||||
status.viewManager.updateLoadingScreenDarkMode(darkMode);
|
||||
}
|
||||
}
|
||||
|
||||
export function getServerNameByWebContentsId(webContentsId: number) {
|
||||
const view = status.viewManager?.findViewByWebContent(webContentsId);
|
||||
handleLoadingScreenAnimationFinished = () => {
|
||||
if (this.viewManager) {
|
||||
this.viewManager.hideLoadingScreen();
|
||||
}
|
||||
}
|
||||
|
||||
updateLoadingScreenDarkMode = (darkMode: boolean) => {
|
||||
if (this.viewManager) {
|
||||
this.viewManager.updateLoadingScreenDarkMode(darkMode);
|
||||
}
|
||||
}
|
||||
|
||||
getViewNameByWebContentsId = (webContentsId: number) => {
|
||||
const view = this.viewManager?.findViewByWebContent(webContentsId);
|
||||
return view?.name;
|
||||
}
|
||||
|
||||
getServerNameByWebContentsId = (webContentsId: number) => {
|
||||
const view = this.viewManager?.findViewByWebContent(webContentsId);
|
||||
return view?.tab.server.name;
|
||||
}
|
||||
}
|
||||
|
||||
export function close() {
|
||||
close = () => {
|
||||
const focused = BrowserWindow.getFocusedWindow();
|
||||
focused?.close();
|
||||
}
|
||||
export function maximize() {
|
||||
}
|
||||
maximize = () => {
|
||||
const focused = BrowserWindow.getFocusedWindow();
|
||||
if (focused) {
|
||||
focused.maximize();
|
||||
}
|
||||
}
|
||||
export function minimize() {
|
||||
}
|
||||
minimize = () => {
|
||||
const focused = BrowserWindow.getFocusedWindow();
|
||||
if (focused) {
|
||||
focused.minimize();
|
||||
}
|
||||
}
|
||||
export function restore() {
|
||||
}
|
||||
restore = () => {
|
||||
const focused = BrowserWindow.getFocusedWindow();
|
||||
if (focused) {
|
||||
focused.restore();
|
||||
@@ -460,26 +484,26 @@ export function restore() {
|
||||
if (focused?.isFullScreen()) {
|
||||
focused.setFullScreen(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function reload() {
|
||||
const currentView = status.viewManager?.getCurrentView();
|
||||
reload = () => {
|
||||
const currentView = this.viewManager?.getCurrentView();
|
||||
if (currentView) {
|
||||
status.viewManager?.showLoadingScreen();
|
||||
this.viewManager?.showLoadingScreen();
|
||||
currentView.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function sendToFind() {
|
||||
const currentView = status.viewManager?.getCurrentView();
|
||||
sendToFind = () => {
|
||||
const currentView = this.viewManager?.getCurrentView();
|
||||
if (currentView) {
|
||||
currentView.view.webContents.sendInputEvent({type: 'keyDown', keyCode: 'F', modifiers: [process.platform === 'darwin' ? 'cmd' : 'ctrl', 'shift']});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleHistory(event: IpcMainEvent, offset: number) {
|
||||
if (status.viewManager) {
|
||||
const activeView = status.viewManager.getCurrentView();
|
||||
handleHistory = (event: IpcMainEvent, offset: number) => {
|
||||
if (this.viewManager) {
|
||||
const activeView = this.viewManager.getCurrentView();
|
||||
if (activeView && activeView.view.webContents.canGoToOffset(offset)) {
|
||||
try {
|
||||
activeView.view.webContents.goToOffset(offset);
|
||||
@@ -489,15 +513,23 @@ function handleHistory(event: IpcMainEvent, offset: number) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function selectNextTab() {
|
||||
const currentView = status.viewManager?.getCurrentView();
|
||||
selectNextTab = () => {
|
||||
this.selectTab((order) => order + 1);
|
||||
}
|
||||
|
||||
selectPreviousTab = () => {
|
||||
this.selectTab((order, length) => (length + (order - 1)));
|
||||
}
|
||||
|
||||
selectTab = (fn: (order: number, length: number) => number) => {
|
||||
const currentView = this.viewManager?.getCurrentView();
|
||||
if (!currentView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTeamTabs = status.config?.teams.find((team) => team.name === currentView.tab.server.name)?.tabs;
|
||||
const currentTeamTabs = this.config?.teams.find((team) => team.name === currentView.tab.server.name)?.tabs;
|
||||
const filteredTabs = currentTeamTabs?.filter((tab) => tab.isOpen);
|
||||
const currentTab = currentTeamTabs?.find((tab) => tab.name === currentView.tab.type);
|
||||
if (!currentTeamTabs || !currentTab || !filteredTabs) {
|
||||
@@ -507,55 +539,29 @@ export function selectNextTab() {
|
||||
let currentOrder = currentTab.order;
|
||||
let nextIndex = -1;
|
||||
while (nextIndex === -1) {
|
||||
const nextOrder = ((currentOrder + 1) % currentTeamTabs.length);
|
||||
const nextOrder = (fn(currentOrder, currentTeamTabs.length) % currentTeamTabs.length);
|
||||
nextIndex = filteredTabs.findIndex((tab) => tab.order === nextOrder);
|
||||
currentOrder = nextOrder;
|
||||
}
|
||||
|
||||
const newTab = filteredTabs[nextIndex];
|
||||
switchTab(currentView.tab.server.name, newTab.name);
|
||||
}
|
||||
|
||||
export function selectPreviousTab() {
|
||||
const currentView = status.viewManager?.getCurrentView();
|
||||
if (!currentView) {
|
||||
return;
|
||||
this.switchTab(currentView.tab.server.name, newTab.name);
|
||||
}
|
||||
|
||||
const currentTeamTabs = status.config?.teams.find((team) => team.name === currentView.tab.server.name)?.tabs;
|
||||
const filteredTabs = currentTeamTabs?.filter((tab) => tab.isOpen);
|
||||
const currentTab = currentTeamTabs?.find((tab) => tab.name === currentView.tab.type);
|
||||
if (!currentTeamTabs || !currentTab || !filteredTabs) {
|
||||
return;
|
||||
handleGetDarkMode = () => {
|
||||
return this.config?.darkMode;
|
||||
}
|
||||
|
||||
// js modulo operator returns a negative number if result is negative, so we have to ensure it's positive
|
||||
let currentOrder = currentTab.order;
|
||||
let nextIndex = -1;
|
||||
while (nextIndex === -1) {
|
||||
const nextOrder = ((currentTeamTabs.length + (currentOrder - 1)) % currentTeamTabs.length);
|
||||
nextIndex = filteredTabs.findIndex((tab) => tab.order === nextOrder);
|
||||
currentOrder = nextOrder;
|
||||
handleBrowserHistoryPush = (e: IpcMainEvent, viewName: string, pathName: string) => {
|
||||
const currentView = this.viewManager?.views.get(viewName);
|
||||
const redirectedViewName = urlUtils.getView(`${currentView?.tab.server.url}${pathName}`, this.config!.teams)?.name || viewName;
|
||||
if (this.viewManager?.closedViews.has(redirectedViewName)) {
|
||||
this.viewManager.openClosedTab(redirectedViewName, `${currentView?.tab.server.url}${pathName}`);
|
||||
}
|
||||
|
||||
const newTab = filteredTabs[nextIndex];
|
||||
switchTab(currentView.tab.server.name, newTab.name);
|
||||
}
|
||||
|
||||
function handleGetDarkMode() {
|
||||
return status.config?.darkMode;
|
||||
}
|
||||
|
||||
function handleBrowserHistoryPush(e: IpcMainEvent, viewName: string, pathName: string) {
|
||||
const currentView = status.viewManager?.views.get(viewName);
|
||||
const redirectedViewName = urlUtils.getView(`${currentView?.tab.server.url}${pathName}`, status.config!.teams)?.name || viewName;
|
||||
if (status.viewManager?.closedViews.has(redirectedViewName)) {
|
||||
status.viewManager.openClosedTab(redirectedViewName, `${currentView?.tab.server.url}${pathName}`);
|
||||
}
|
||||
let redirectedView = status.viewManager?.views.get(redirectedViewName) || currentView;
|
||||
if (redirectedView !== currentView && redirectedView?.tab.server.name === status.currentServerName && redirectedView?.isLoggedIn) {
|
||||
let redirectedView = this.viewManager?.views.get(redirectedViewName) || currentView;
|
||||
if (redirectedView !== currentView && redirectedView?.tab.server.name === this.currentServerName && redirectedView?.isLoggedIn) {
|
||||
log.info('redirecting to a new view', redirectedView?.name || viewName);
|
||||
status.viewManager?.showByName(redirectedView?.name || viewName);
|
||||
this.viewManager?.showByName(redirectedView?.name || viewName);
|
||||
} else {
|
||||
redirectedView = currentView;
|
||||
}
|
||||
@@ -564,32 +570,35 @@ function handleBrowserHistoryPush(e: IpcMainEvent, viewName: string, pathName: s
|
||||
if (!(redirectedView !== currentView && redirectedView?.tab.type === TAB_MESSAGING && pathName === '/')) {
|
||||
redirectedView?.view.webContents.send(BROWSER_HISTORY_PUSH, pathName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentTeamName() {
|
||||
return status.currentServerName;
|
||||
}
|
||||
getCurrentTeamName = () => {
|
||||
return this.currentServerName;
|
||||
}
|
||||
|
||||
function handleAppLoggedIn(event: IpcMainEvent, viewName: string) {
|
||||
const view = status.viewManager?.views.get(viewName);
|
||||
handleAppLoggedIn = (event: IpcMainEvent, viewName: string) => {
|
||||
const view = this.viewManager?.views.get(viewName);
|
||||
if (view) {
|
||||
view.isLoggedIn = true;
|
||||
status.viewManager?.reloadViewIfNeeded(viewName);
|
||||
this.viewManager?.reloadViewIfNeeded(viewName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleAppLoggedOut(event: IpcMainEvent, viewName: string) {
|
||||
const view = status.viewManager?.views.get(viewName);
|
||||
handleAppLoggedOut = (event: IpcMainEvent, viewName: string) => {
|
||||
const view = this.viewManager?.views.get(viewName);
|
||||
if (view) {
|
||||
view.isLoggedIn = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleGetViewName(event: IpcMainInvokeEvent) {
|
||||
const view = status.viewManager?.findViewByWebContent(event.sender.id);
|
||||
return view?.name;
|
||||
}
|
||||
function handleGetWebContentsId(event: IpcMainInvokeEvent) {
|
||||
handleGetViewName = (event: IpcMainInvokeEvent) => {
|
||||
return this.getViewNameByWebContentsId(event.sender.id);
|
||||
}
|
||||
|
||||
handleGetWebContentsId = (event: IpcMainInvokeEvent) => {
|
||||
return event.sender.id;
|
||||
}
|
||||
}
|
||||
|
||||
const windowManager = new WindowManager();
|
||||
export default windowManager;
|
||||
|
Reference in New Issue
Block a user