// 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, dialog} from 'electron'; import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB} from 'common/communication'; import Config from 'common/config'; import {DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH} from 'common/utils/constants'; import * as Validator from 'common/Validator'; import ContextMenu from '../contextMenu'; import {isInsideRectangle} from '../utils'; import {MainWindow} from './mainWindow'; jest.mock('path', () => ({ join: jest.fn(), resolve: jest.fn(), })); jest.mock('electron', () => ({ app: { getAppPath: jest.fn(), getPath: jest.fn(), hide: jest.fn(), quit: jest.fn(), relaunch: jest.fn(), }, dialog: { showMessageBox: jest.fn(), }, BrowserWindow: jest.fn(), ipcMain: { handle: jest.fn(), on: jest.fn(), }, screen: { getDisplayMatching: jest.fn(), }, globalShortcut: { register: jest.fn(), registerAll: jest.fn(), }, })); jest.mock('common/config', () => ({ set: jest.fn(), })); jest.mock('common/utils/util', () => ({ isVersionGreaterThanOrEqualTo: jest.fn(), })); jest.mock('global', () => ({ willAppQuit: false, })); jest.mock('fs', () => ({ readFileSync: jest.fn(), writeFileSync: jest.fn(), })); jest.mock('common/Validator', () => ({ validateBoundsInfo: jest.fn(), })); jest.mock('../contextMenu', () => jest.fn()); jest.mock('../utils', () => ({ isInsideRectangle: jest.fn(), getLocalPreload: jest.fn(), getLocalURLString: jest.fn(), })); jest.mock('main/i18nManager', () => ({ localizeMessage: jest.fn(), })); 'use strict'; describe('main/windows/mainWindow', () => { describe('init', () => { const baseWindow = { setMenuBarVisibility: jest.fn(), setAutoHideMenuBar: 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(), setWindowOpenHandler: 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}})); isInsideRectangle.mockReturnValue(true); Validator.validateBoundsInfo.mockImplementation((data) => data); ContextMenu.mockImplementation(() => ({ reload: jest.fn(), })); }); afterEach(() => { jest.resetAllMocks(); }); it('should set window size using bounds read from file', () => { const mainWindow = new MainWindow(); mainWindow.init(); 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'); const mainWindow = new MainWindow(); mainWindow.init(); 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}})); isInsideRectangle.mockReturnValue(false); const mainWindow = new MainWindow(); mainWindow.init(); expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({ width: DEFAULT_WINDOW_WIDTH, height: DEFAULT_WINDOW_HEIGHT, })); }); 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}'); Config.hideOnStart = false; const mainWindow = new MainWindow(); mainWindow.init(); expect(window.webContents.zoomLevel).toStrictEqual(0); expect(window.maximize).toBeCalled(); }); it('should not show window 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}'); Config.hideOnStart = true; const mainWindow = new MainWindow(); mainWindow.init(); expect(window.show).not.toHaveBeenCalled(); }); 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); const mainWindow = new MainWindow(); mainWindow.init(); global.willAppQuit = false; expect(fs.writeFileSync).toHaveBeenCalled(); }); it('should hide window on close for Windows if app wont quit and config item is set', () => { 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); Config.minimizeToTray = true; Config.alwaysMinimize = true; const mainWindow = new MainWindow(); mainWindow.init(); Config.minimizeToTray = false; Config.alwaysMinimize = false; Object.defineProperty(process, 'platform', { value: originalPlatform, }); expect(window.hide).toHaveBeenCalled(); }); it('should close app on close window for Windows if app wont quit and config item is not set', () => { 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); Config.alwaysClose = true; const mainWindow = new MainWindow(); mainWindow.init(); Config.alwaysClose = false; Object.defineProperty(process, 'platform', { value: originalPlatform, }); expect(app.quit).toHaveBeenCalled(); }); it('should close app on Windows if window closed depending on user input', async () => { 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); dialog.showMessageBox.mockResolvedValue({response: 1}); const mainWindow = new MainWindow(); mainWindow.init(); expect(app.quit).not.toHaveBeenCalled(); const promise = Promise.resolve({response: 0}); dialog.showMessageBox.mockImplementation(() => promise); const mainWindow2 = new MainWindow(); mainWindow2.init(); Object.defineProperty(process, 'platform', { value: originalPlatform, }); await promise; expect(app.quit).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); Config.minimizeToTray = true; Config.alwaysMinimize = true; const mainWindow = new MainWindow(); mainWindow.init(); Config.minimizeToTray = false; Config.alwaysMinimize = false; Object.defineProperty(process, 'platform', { value: originalPlatform, }); expect(window.hide).toHaveBeenCalled(); }); it('should close app on close window 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); Config.alwaysClose = true; const mainWindow = new MainWindow(); mainWindow.init(); Config.alwaysClose = false; Object.defineProperty(process, 'platform', { value: originalPlatform, }); expect(app.quit).toHaveBeenCalled(); }); it('should close app on linux if window closed depending on user input', async () => { 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); dialog.showMessageBox.mockResolvedValue({response: 1}); const mainWindow = new MainWindow(); mainWindow.init(); expect(app.quit).not.toHaveBeenCalled(); const promise = Promise.resolve({response: 0}); dialog.showMessageBox.mockImplementation(() => promise); const mainWindow2 = new MainWindow(); mainWindow2.init(); Object.defineProperty(process, 'platform', { value: originalPlatform, }); await promise; expect(app.quit).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); const mainWindow = new MainWindow(); mainWindow.init(); 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); const mainWindow = new MainWindow(); mainWindow.init(); 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); const mainWindow = new MainWindow(); mainWindow.init(); 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); const mainWindow = new MainWindow(); mainWindow.getBounds = jest.fn(); mainWindow.init(); 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)); }); }); describe('show', () => { const mainWindow = new MainWindow(); mainWindow.win = { visible: false, isVisible: () => mainWindow.visible, show: jest.fn(), focus: jest.fn(), on: jest.fn(), once: jest.fn(), webContents: { setWindowOpenHandler: jest.fn(), }, }; mainWindow.init = jest.fn(); beforeEach(() => { mainWindow.win.show.mockImplementation(() => { mainWindow.visible = true; }); }); afterEach(() => { mainWindow.ready = false; jest.resetAllMocks(); }); it('should show main window and focus it if it is exists', () => { mainWindow.ready = true; mainWindow.show(); expect(mainWindow.win.show).toHaveBeenCalled(); expect(mainWindow.win.focus).toHaveBeenCalled(); }); it('should init if the main window does not exist', () => { mainWindow.show(); expect(mainWindow.init).toHaveBeenCalled(); }); }); describe('onUnresponsive', () => { const mainWindow = new MainWindow(); beforeEach(() => { mainWindow.win = {}; }); it('should call app.relaunch when user elects not to wait', async () => { const promise = Promise.resolve({response: 0}); dialog.showMessageBox.mockImplementation(() => promise); mainWindow.onUnresponsive(); await promise; expect(app.relaunch).toBeCalled(); }); }); });