[MM-40329] Unit tests for main/views (#1883)
* WIP * [MM-40329] Unit tests for main/views * Lint/type fix * Merge'd
This commit is contained in:
@@ -11,8 +11,9 @@ import {BASIC_AUTH_PERMISSION} from 'common/permissions';
|
|||||||
import urlUtils from 'common/utils/url';
|
import urlUtils from 'common/utils/url';
|
||||||
|
|
||||||
import * as WindowManager from 'main/windows/windowManager';
|
import * as WindowManager from 'main/windows/windowManager';
|
||||||
import {addModal} from 'main/views/modalManager';
|
|
||||||
import {getLocalURLString, getLocalPreload} from 'main/utils';
|
import modalManager from './views/modalManager';
|
||||||
|
import {getLocalURLString, getLocalPreload} from './utils';
|
||||||
|
|
||||||
import TrustedOriginsStore from './trustedOrigins';
|
import TrustedOriginsStore from './trustedOrigins';
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ export class AuthManager {
|
|||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const modalPromise = addModal<LoginModalData, LoginModalResult>(authInfo.isProxy ? `proxy-${authInfo.host}` : `login-${request.url}`, loginModalHtml, modalPreload, {request, authInfo}, mainWindow);
|
const modalPromise = modalManager.addModal<LoginModalData, LoginModalResult>(authInfo.isProxy ? `proxy-${authInfo.host}` : `login-${request.url}`, loginModalHtml, modalPreload, {request, authInfo}, mainWindow);
|
||||||
if (modalPromise) {
|
if (modalPromise) {
|
||||||
modalPromise.then((data) => {
|
modalPromise.then((data) => {
|
||||||
const {username, password} = data;
|
const {username, password} = data;
|
||||||
@@ -83,7 +84,7 @@ export class AuthManager {
|
|||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const modalPromise = addModal(`permission-${request.url}`, permissionModalHtml, modalPreload, {url: request.url, permission}, mainWindow);
|
const modalPromise = modalManager.addModal(`permission-${request.url}`, permissionModalHtml, modalPreload, {url: request.url, permission}, mainWindow);
|
||||||
if (modalPromise) {
|
if (modalPromise) {
|
||||||
modalPromise.then(() => {
|
modalPromise.then(() => {
|
||||||
this.handlePermissionGranted(request.url, permission);
|
this.handlePermissionGranted(request.url, permission);
|
||||||
|
@@ -7,7 +7,7 @@ import {CertificateModalData} from 'types/certificate';
|
|||||||
|
|
||||||
import * as WindowManager from './windows/windowManager';
|
import * as WindowManager from './windows/windowManager';
|
||||||
|
|
||||||
import {addModal} from './views/modalManager';
|
import modalManager from './views/modalManager';
|
||||||
import {getLocalURLString, getLocalPreload} from './utils';
|
import {getLocalURLString, getLocalPreload} from './utils';
|
||||||
|
|
||||||
const modalPreload = getLocalPreload('modalPreload.js');
|
const modalPreload = getLocalPreload('modalPreload.js');
|
||||||
@@ -41,7 +41,7 @@ export class CertificateManager {
|
|||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const modalPromise = addModal<CertificateModalData, CertificateModalResult>(`certificate-${url}`, html, modalPreload, {url, list}, mainWindow);
|
const modalPromise = modalManager.addModal<CertificateModalData, CertificateModalResult>(`certificate-${url}`, html, modalPreload, {url, list}, mainWindow);
|
||||||
if (modalPromise) {
|
if (modalPromise) {
|
||||||
modalPromise.then((data) => {
|
modalPromise.then((data) => {
|
||||||
const {cert} = data;
|
const {cert} = data;
|
||||||
|
@@ -67,7 +67,7 @@ import * as WindowManager from './windows/windowManager';
|
|||||||
import {displayMention, displayDownloadCompleted} from './notifications';
|
import {displayMention, displayDownloadCompleted} from './notifications';
|
||||||
|
|
||||||
import parseArgs from './ParseArgs';
|
import parseArgs from './ParseArgs';
|
||||||
import {addModal} from './views/modalManager';
|
import modalManager from './views/modalManager';
|
||||||
import {getLocalURLString, getLocalPreload} from './utils';
|
import {getLocalURLString, getLocalPreload} from './utils';
|
||||||
import {destroyTray, refreshTrayImages, setTrayMenu, setupTray} from './tray/tray';
|
import {destroyTray, refreshTrayImages, setTrayMenu, setupTray} from './tray/tray';
|
||||||
import {AuthManager} from './authManager';
|
import {AuthManager} from './authManager';
|
||||||
@@ -553,7 +553,7 @@ function handleNewServerModal() {
|
|||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const modalPromise = addModal<unknown, Team>('newServer', html, modalPreload, {}, mainWindow, config.teams.length === 0);
|
const modalPromise = modalManager.addModal<unknown, Team>('newServer', html, modalPreload, {}, mainWindow, config.teams.length === 0);
|
||||||
if (modalPromise) {
|
if (modalPromise) {
|
||||||
modalPromise.then((data) => {
|
modalPromise.then((data) => {
|
||||||
const teams = config.teams;
|
const teams = config.teams;
|
||||||
@@ -587,7 +587,7 @@ function handleEditServerModal(e: IpcMainEvent, name: string) {
|
|||||||
if (serverIndex < 0) {
|
if (serverIndex < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const modalPromise = addModal<Team, Team>('editServer', html, modalPreload, config.teams[serverIndex], mainWindow);
|
const modalPromise = modalManager.addModal<Team, Team>('editServer', html, modalPreload, config.teams[serverIndex], mainWindow);
|
||||||
if (modalPromise) {
|
if (modalPromise) {
|
||||||
modalPromise.then((data) => {
|
modalPromise.then((data) => {
|
||||||
const teams = config.teams;
|
const teams = config.teams;
|
||||||
@@ -614,7 +614,7 @@ function handleRemoveServerModal(e: IpcMainEvent, name: string) {
|
|||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const modalPromise = addModal<string, boolean>('removeServer', html, modalPreload, name, mainWindow);
|
const modalPromise = modalManager.addModal<string, boolean>('removeServer', html, modalPreload, name, mainWindow);
|
||||||
if (modalPromise) {
|
if (modalPromise) {
|
||||||
modalPromise.then((remove) => {
|
modalPromise.then((remove) => {
|
||||||
if (remove) {
|
if (remove) {
|
||||||
|
319
src/main/views/MattermostView.test.js
Normal file
319
src/main/views/MattermostView.test.js
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import {LOAD_FAILED, TOGGLE_BACK_BUTTON, UPDATE_TARGET_URL} from 'common/communication';
|
||||||
|
import {MattermostServer} from 'common/servers/MattermostServer';
|
||||||
|
import MessagingTabView from 'common/tabs/MessagingTabView';
|
||||||
|
|
||||||
|
import * as WindowManager from '../windows/windowManager';
|
||||||
|
import * as appState from '../appState';
|
||||||
|
|
||||||
|
import {MattermostView} from './MattermostView';
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
app: {
|
||||||
|
getVersion: () => '5.0.0',
|
||||||
|
},
|
||||||
|
BrowserView: jest.fn().mockImplementation(() => ({
|
||||||
|
webContents: {
|
||||||
|
loadURL: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
getTitle: () => 'title',
|
||||||
|
getURL: () => 'http://server-1.com',
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
ipcMain: {
|
||||||
|
on: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('electron-log', () => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../windows/windowManager', () => ({
|
||||||
|
sendToRenderer: jest.fn(),
|
||||||
|
focusThreeDotMenu: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('../appState', () => ({
|
||||||
|
updateMentions: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('./webContentEvents', () => ({
|
||||||
|
removeWebContentsListeners: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('../contextMenu', () => jest.fn());
|
||||||
|
jest.mock('../utils', () => ({
|
||||||
|
getWindowBoundaries: jest.fn(),
|
||||||
|
getLocalPreload: (file) => file,
|
||||||
|
composeUserAgent: () => 'Mattermost/5.0.0',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const server = new MattermostServer('server_name', 'http://server-1.com');
|
||||||
|
const tabView = new MessagingTabView(server);
|
||||||
|
|
||||||
|
describe('main/views/MattermostView', () => {
|
||||||
|
describe('load', () => {
|
||||||
|
const mattermostView = new MattermostView(tabView, {}, {}, {});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mattermostView.loadSuccess = jest.fn();
|
||||||
|
mattermostView.loadRetry = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load provided URL when provided', async () => {
|
||||||
|
const promise = Promise.resolve();
|
||||||
|
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
|
||||||
|
mattermostView.load('http://server-2.com');
|
||||||
|
await promise;
|
||||||
|
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-2.com/', expect.any(Object));
|
||||||
|
expect(mattermostView.loadSuccess).toBeCalledWith('http://server-2.com/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load server URL when not provided', async () => {
|
||||||
|
const promise = Promise.resolve();
|
||||||
|
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
|
||||||
|
mattermostView.load();
|
||||||
|
await promise;
|
||||||
|
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
|
||||||
|
expect(mattermostView.loadSuccess).toBeCalledWith('http://server-1.com/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load server URL when bad url provided', async () => {
|
||||||
|
const promise = Promise.resolve();
|
||||||
|
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
|
||||||
|
mattermostView.load('a-bad<url');
|
||||||
|
await promise;
|
||||||
|
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
|
||||||
|
expect(mattermostView.loadSuccess).toBeCalledWith('http://server-1.com/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call retry when failing to load', async () => {
|
||||||
|
const error = new Error('test');
|
||||||
|
const promise = Promise.reject(error);
|
||||||
|
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
|
||||||
|
mattermostView.load('a-bad<url');
|
||||||
|
await expect(promise).rejects.toThrow(error);
|
||||||
|
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
|
||||||
|
expect(mattermostView.loadRetry).toBeCalledWith('http://server-1.com/', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('retry', () => {
|
||||||
|
const mattermostView = new MattermostView(tabView, {}, {}, {});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mattermostView.view.webContents.loadURL.mockImplementation(() => Promise.resolve());
|
||||||
|
mattermostView.loadSuccess = jest.fn();
|
||||||
|
mattermostView.loadRetry = jest.fn();
|
||||||
|
mattermostView.emit = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing when webcontents are destroyed', () => {
|
||||||
|
const webContents = mattermostView.view.webContents;
|
||||||
|
mattermostView.view.webContents = null;
|
||||||
|
mattermostView.retry('http://server-1.com')();
|
||||||
|
expect(mattermostView.loadSuccess).not.toBeCalled();
|
||||||
|
mattermostView.view.webContents = webContents;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call loadSuccess on successful load', async () => {
|
||||||
|
const promise = Promise.resolve();
|
||||||
|
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
|
||||||
|
mattermostView.retry('http://server-1.com')();
|
||||||
|
await promise;
|
||||||
|
expect(mattermostView.loadSuccess).toBeCalledWith('http://server-1.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call loadRetry if maxRetries are still remaining', async () => {
|
||||||
|
mattermostView.maxRetries = 10;
|
||||||
|
const error = new Error('test');
|
||||||
|
const promise = Promise.reject(error);
|
||||||
|
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
|
||||||
|
mattermostView.retry('http://server-1.com')();
|
||||||
|
await expect(promise).rejects.toThrow(error);
|
||||||
|
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object));
|
||||||
|
expect(mattermostView.loadRetry).toBeCalledWith('http://server-1.com', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set to error status when max retries are reached', async () => {
|
||||||
|
mattermostView.maxRetries = 0;
|
||||||
|
const error = new Error('test');
|
||||||
|
const promise = Promise.reject(error);
|
||||||
|
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
|
||||||
|
mattermostView.retry('http://server-1.com')();
|
||||||
|
await expect(promise).rejects.toThrow(error);
|
||||||
|
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object));
|
||||||
|
expect(mattermostView.loadRetry).not.toBeCalled();
|
||||||
|
expect(WindowManager.sendToRenderer).toBeCalledWith(LOAD_FAILED, mattermostView.tab.name, expect.any(String), expect.any(String));
|
||||||
|
expect(mattermostView.status).toBe(-1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadSuccess', () => {
|
||||||
|
const mattermostView = new MattermostView(tabView, {}, {}, {});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
mattermostView.emit = jest.fn();
|
||||||
|
mattermostView.setBounds = jest.fn();
|
||||||
|
mattermostView.setInitialized = jest.fn();
|
||||||
|
mattermostView.updateMentionsFromTitle = jest.fn();
|
||||||
|
mattermostView.findUnreadState = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset max retries', () => {
|
||||||
|
mattermostView.maxRetries = 1;
|
||||||
|
mattermostView.loadSuccess('http://server-1.com')();
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(mattermostView.maxRetries).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('show', () => {
|
||||||
|
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn()};
|
||||||
|
const mattermostView = new MattermostView(tabView, {}, window, {});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
mattermostView.setBounds = jest.fn();
|
||||||
|
mattermostView.focus = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add browser view to window and set bounds when request is true and view not currently visible', () => {
|
||||||
|
mattermostView.isVisible = false;
|
||||||
|
mattermostView.show(true);
|
||||||
|
expect(window.addBrowserView).toBeCalledWith(mattermostView.view);
|
||||||
|
expect(mattermostView.setBounds).toBeCalled();
|
||||||
|
expect(mattermostView.isVisible).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove browser view when request is false', () => {
|
||||||
|
mattermostView.isVisible = true;
|
||||||
|
mattermostView.show(false);
|
||||||
|
expect(window.removeBrowserView).toBeCalledWith(mattermostView.view);
|
||||||
|
expect(mattermostView.isVisible).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing when not toggling', () => {
|
||||||
|
mattermostView.isVisible = true;
|
||||||
|
mattermostView.show(true);
|
||||||
|
expect(window.addBrowserView).not.toBeCalled();
|
||||||
|
expect(window.removeBrowserView).not.toBeCalled();
|
||||||
|
|
||||||
|
mattermostView.isVisible = false;
|
||||||
|
mattermostView.show(false);
|
||||||
|
expect(window.addBrowserView).not.toBeCalled();
|
||||||
|
expect(window.removeBrowserView).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should focus view if view is ready', () => {
|
||||||
|
mattermostView.status = 1;
|
||||||
|
mattermostView.isVisible = false;
|
||||||
|
mattermostView.show(true);
|
||||||
|
expect(mattermostView.focus).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('destroy', () => {
|
||||||
|
const window = {removeBrowserView: jest.fn()};
|
||||||
|
const mattermostView = new MattermostView(tabView, {}, window, {});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mattermostView.view.webContents.destroy = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove browser view from window', () => {
|
||||||
|
mattermostView.destroy();
|
||||||
|
expect(window.removeBrowserView).toBeCalledWith(mattermostView.view);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear mentions', () => {
|
||||||
|
mattermostView.destroy();
|
||||||
|
expect(appState.updateMentions).toBeCalledWith(mattermostView.tab.name, 0, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear outstanding timeouts', () => {
|
||||||
|
const spy = jest.spyOn(global, 'clearTimeout');
|
||||||
|
mattermostView.retryLoad = 999;
|
||||||
|
mattermostView.removeLoading = 1000;
|
||||||
|
mattermostView.destroy();
|
||||||
|
expect(spy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleInputEvents', () => {
|
||||||
|
const mattermostView = new MattermostView(tabView, {}, {}, {});
|
||||||
|
|
||||||
|
it('should open three dot menu on pressing Alt', () => {
|
||||||
|
mattermostView.handleInputEvents(null, {key: 'Alt', type: 'keyDown'});
|
||||||
|
mattermostView.handleInputEvents(null, {key: 'Alt', type: 'keyUp'});
|
||||||
|
expect(WindowManager.focusThreeDotMenu).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not open three dot menu on holding Alt', () => {
|
||||||
|
mattermostView.handleInputEvents(null, {key: 'Alt', type: 'keyDown'});
|
||||||
|
expect(WindowManager.focusThreeDotMenu).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not open three dot menu on Alt as key combp', () => {
|
||||||
|
mattermostView.handleInputEvents(null, {key: 'Alt', type: 'keyDown'});
|
||||||
|
mattermostView.handleInputEvents(null, {key: 'F', type: 'keyDown'});
|
||||||
|
mattermostView.handleInputEvents(null, {key: 'F', type: 'keyUp'});
|
||||||
|
mattermostView.handleInputEvents(null, {key: 'Alt', type: 'keyUp'});
|
||||||
|
expect(WindowManager.focusThreeDotMenu).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleDidNavigate', () => {
|
||||||
|
const mattermostView = new MattermostView(tabView, {}, {}, {});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mattermostView.setBounds = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide back button on internal url', () => {
|
||||||
|
mattermostView.handleDidNavigate(null, 'http://server-1.com/path/to/channels');
|
||||||
|
expect(WindowManager.sendToRenderer).toHaveBeenCalledWith(TOGGLE_BACK_BUTTON, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show back button on external url', () => {
|
||||||
|
mattermostView.handleDidNavigate(null, 'http://server-2.com/some/other/path');
|
||||||
|
expect(WindowManager.sendToRenderer).toHaveBeenCalledWith(TOGGLE_BACK_BUTTON, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleUpdateTarget', () => {
|
||||||
|
const mattermostView = new MattermostView(tabView, {}, {}, {});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mattermostView.emit = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit tooltip URL if not internal', () => {
|
||||||
|
mattermostView.handleUpdateTarget(null, 'http://server-2.com/some/other/path');
|
||||||
|
expect(mattermostView.emit).toHaveBeenCalledWith(UPDATE_TARGET_URL, 'http://server-2.com/some/other/path');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not emit tooltip URL if internal', () => {
|
||||||
|
mattermostView.handleUpdateTarget(null, 'http://server-1.com/path/to/channels');
|
||||||
|
expect(mattermostView.emit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateMentionsFromTitle', () => {
|
||||||
|
const mattermostView = new MattermostView(tabView, {}, {}, {});
|
||||||
|
|
||||||
|
it('should parse mentions from title', () => {
|
||||||
|
mattermostView.updateMentionsFromTitle('(7) Mattermost');
|
||||||
|
expect(appState.updateMentions).toHaveBeenCalledWith(mattermostView.tab.name, 7, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse unreads from title', () => {
|
||||||
|
mattermostView.updateMentionsFromTitle('* Mattermost');
|
||||||
|
expect(appState.updateMentions).toHaveBeenCalledWith(mattermostView.tab.name, 0, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -30,7 +30,7 @@ import {getWindowBoundaries, getLocalPreload, composeUserAgent} from '../utils';
|
|||||||
import * as WindowManager from '../windows/windowManager';
|
import * as WindowManager from '../windows/windowManager';
|
||||||
import * as appState from '../appState';
|
import * as appState from '../appState';
|
||||||
|
|
||||||
import {removeWebContentsListeners} from './webContentEvents';
|
import WebContentsEventManager from './webContentEvents';
|
||||||
|
|
||||||
export enum Status {
|
export enum Status {
|
||||||
LOADING,
|
LOADING,
|
||||||
@@ -215,7 +215,7 @@ export class MattermostView extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy = () => {
|
destroy = () => {
|
||||||
removeWebContentsListeners(this.view.webContents.id);
|
WebContentsEventManager.removeWebContentsListeners(this.view.webContents.id);
|
||||||
appState.updateMentions(this.tab.name, 0, false);
|
appState.updateMentions(this.tab.name, 0, false);
|
||||||
if (this.window) {
|
if (this.window) {
|
||||||
this.window.removeBrowserView(this.view);
|
this.window.removeBrowserView(this.view);
|
||||||
|
161
src/main/views/modalManager.test.js
Normal file
161
src/main/views/modalManager.test.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as WindowManager from '../windows/windowManager';
|
||||||
|
|
||||||
|
import {ModalManager} from './modalManager';
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
app: {},
|
||||||
|
ipcMain: {
|
||||||
|
handle: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('electron-log', () => ({}));
|
||||||
|
|
||||||
|
jest.mock('./modalView', () => ({
|
||||||
|
ModalView: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('../windows/windowManager', () => ({
|
||||||
|
sendToRenderer: jest.fn(),
|
||||||
|
focusBrowserView: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('process', () => ({
|
||||||
|
env: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('main/views/modalManager', () => {
|
||||||
|
describe('addModal', () => {
|
||||||
|
const modalManager = new ModalManager();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
modalManager.modalQueue = [];
|
||||||
|
modalManager.modalPromises = new Map();
|
||||||
|
modalManager.showModal = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add modal with the same key, should return the existing promise', () => {
|
||||||
|
modalManager.modalQueue.push({key: 'existing_key'});
|
||||||
|
const promise = Promise.resolve();
|
||||||
|
modalManager.modalPromises.set('existing_key', promise);
|
||||||
|
expect(modalManager.addModal('existing_key', 'some_html', 'preload', {}, {})).toBe(promise);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add modal to queue and add the promise, but dont show', () => {
|
||||||
|
modalManager.modalQueue.push({key: 'existing_key'});
|
||||||
|
const promise = Promise.resolve();
|
||||||
|
modalManager.modalPromises.set('existing_key', promise);
|
||||||
|
modalManager.addModal('new_key', 'some_html', 'preload', {}, {});
|
||||||
|
expect(modalManager.modalPromises.has('new_key')).toBe(true);
|
||||||
|
expect(modalManager.modalQueue.length).toBe(2);
|
||||||
|
expect(modalManager.showModal).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add modal to queue and add the promise, but dont show', () => {
|
||||||
|
modalManager.addModal('new_key', 'some_html', 'preload', {}, {});
|
||||||
|
expect(modalManager.modalPromises.has('new_key')).toBe(true);
|
||||||
|
expect(modalManager.modalQueue.length).toBe(1);
|
||||||
|
expect(modalManager.showModal).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findModalByCaller', () => {
|
||||||
|
const modalManager = new ModalManager();
|
||||||
|
const modalView = {key: 'test', view: {webContents: {id: 1}}};
|
||||||
|
const promise = Promise.resolve();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
modalManager.modalQueue = [modalView];
|
||||||
|
modalManager.modalPromises = new Map([['test', promise]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return modal by webContentsId', () => {
|
||||||
|
expect(modalManager.findModalByCaller({sender: {id: 1}})).toBe(modalView);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showModal', () => {
|
||||||
|
const oldEnv = process.env;
|
||||||
|
const modalManager = new ModalManager();
|
||||||
|
const modalView = {key: 'test', view: {webContents: {id: 1}}, show: jest.fn(), hide: jest.fn()};
|
||||||
|
const modalView2 = {key: 'test2', view: {webContents: {id: 2}}, show: jest.fn(), hide: jest.fn()};
|
||||||
|
const promise = Promise.resolve();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
modalManager.modalQueue = [modalView, modalView2];
|
||||||
|
modalManager.modalPromises = new Map([['test', promise], ['test2', promise]]);
|
||||||
|
process.env = {...oldEnv};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = oldEnv;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show first modal and hide second one', () => {
|
||||||
|
modalManager.showModal();
|
||||||
|
expect(modalView.show).toBeCalled();
|
||||||
|
expect(modalView.hide).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include dev tools when env variable is enabled', () => {
|
||||||
|
process.env.MM_DEBUG_MODALS = true;
|
||||||
|
modalManager.showModal();
|
||||||
|
expect(modalView.show).toBeCalledWith(undefined, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleModalFinished', () => {
|
||||||
|
const modalManager = new ModalManager();
|
||||||
|
const modalView = {key: 'test', view: {webContents: {id: 1}}, resolve: jest.fn(), reject: jest.fn()};
|
||||||
|
const modalView2 = {key: 'test2', view: {webContents: {id: 2}}, resolve: jest.fn(), reject: jest.fn()};
|
||||||
|
const promise = Promise.resolve();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
modalManager.modalQueue = [modalView, modalView2];
|
||||||
|
modalManager.modalPromises = new Map([['test', promise], ['test2', promise]]);
|
||||||
|
modalManager.showModal = jest.fn();
|
||||||
|
modalManager.filterActive = () => {
|
||||||
|
modalManager.modalQueue.pop();
|
||||||
|
};
|
||||||
|
modalManager.findModalByCaller = (event) => {
|
||||||
|
switch (event.sender.id) {
|
||||||
|
case 1:
|
||||||
|
return modalView;
|
||||||
|
case 2:
|
||||||
|
return modalView2;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle results for specified modal and go to next modal', () => {
|
||||||
|
modalManager.handleModalFinished('resolve', {sender: {id: 1}}, 'something');
|
||||||
|
expect(modalView.resolve).toBeCalledWith('something');
|
||||||
|
expect(modalView.reject).not.toBeCalled();
|
||||||
|
expect(modalManager.modalPromises.has('test')).toBe(false);
|
||||||
|
expect(modalManager.modalQueue.length).toBe(1);
|
||||||
|
expect(modalManager.showModal).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle cancel for specified modal and go to next modal', () => {
|
||||||
|
modalManager.handleModalFinished('reject', {sender: {id: 1}}, 'something');
|
||||||
|
expect(modalView.reject).toBeCalledWith('something');
|
||||||
|
expect(modalView.resolve).not.toBeCalled();
|
||||||
|
expect(modalManager.modalPromises.has('test')).toBe(false);
|
||||||
|
expect(modalManager.modalQueue.length).toBe(1);
|
||||||
|
expect(modalManager.showModal).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should focus main browser view when all modals are gone', () => {
|
||||||
|
modalManager.modalQueue.pop();
|
||||||
|
modalManager.modalPromises.delete('test2');
|
||||||
|
modalManager.handleModalFinished('resolve', {sender: {id: 1}}, 'something');
|
||||||
|
expect(WindowManager.focusBrowserView).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -21,116 +21,120 @@ import * as WindowManager from '../windows/windowManager';
|
|||||||
|
|
||||||
import {ModalView} from './modalView';
|
import {ModalView} from './modalView';
|
||||||
|
|
||||||
let modalQueue: Array<ModalView<any, any>> = [];
|
export class ModalManager {
|
||||||
const modalPromises: Map<string, Promise<any>> = new Map();
|
modalQueue: Array<ModalView<any, any>>;
|
||||||
|
modalPromises: Map<string, Promise<any>>;
|
||||||
|
|
||||||
// TODO: add a queue/add differentiation, in case we need to put a modal first in line
|
constructor() {
|
||||||
export function addModal<T, T2>(key: string, html: string, preload: string, data: T, win: BrowserWindow, uncloseable = false) {
|
this.modalQueue = [];
|
||||||
const foundModal = modalQueue.find((modal) => modal.key === key);
|
this.modalPromises = new Map();
|
||||||
if (!foundModal) {
|
|
||||||
const modalPromise = new Promise((resolve: (value: T2) => void, reject) => {
|
|
||||||
const mv = new ModalView<T, T2>(key, html, preload, data, resolve, reject, win, uncloseable);
|
|
||||||
modalQueue.push(mv);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (modalQueue.length === 1) {
|
ipcMain.handle(GET_MODAL_UNCLOSEABLE, this.handleGetModalUncloseable);
|
||||||
showModal();
|
ipcMain.handle(RETRIEVE_MODAL_INFO, this.handleInfoRequest);
|
||||||
|
ipcMain.on(MODAL_RESULT, this.handleModalResult);
|
||||||
|
ipcMain.on(MODAL_CANCEL, this.handleModalCancel);
|
||||||
|
|
||||||
|
ipcMain.on(EMIT_CONFIGURATION, this.handleEmitConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add a queue/add differentiation, in case we need to put a modal first in line
|
||||||
|
addModal = <T, T2>(key: string, html: string, preload: string, data: T, win: BrowserWindow, uncloseable = false) => {
|
||||||
|
const foundModal = this.modalQueue.find((modal) => modal.key === key);
|
||||||
|
if (!foundModal) {
|
||||||
|
const modalPromise = new Promise((resolve: (value: T2) => void, reject) => {
|
||||||
|
const mv = new ModalView<T, T2>(key, html, preload, data, resolve, reject, win, uncloseable);
|
||||||
|
this.modalQueue.push(mv);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.modalQueue.length === 1) {
|
||||||
|
this.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modalPromises.set(key, modalPromise);
|
||||||
|
return modalPromise;
|
||||||
}
|
}
|
||||||
|
return this.modalPromises.get(key) as Promise<T2>;
|
||||||
modalPromises.set(key, modalPromise);
|
|
||||||
return modalPromise;
|
|
||||||
}
|
}
|
||||||
return modalPromises.get(key) as Promise<T2>;
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle(GET_MODAL_UNCLOSEABLE, handleGetModalUncloseable);
|
findModalByCaller = (event: IpcMainInvokeEvent) => {
|
||||||
ipcMain.handle(RETRIEVE_MODAL_INFO, handleInfoRequest);
|
if (this.modalQueue.length) {
|
||||||
ipcMain.on(MODAL_RESULT, handleModalResult);
|
const requestModal = this.modalQueue.find((modal) => {
|
||||||
ipcMain.on(MODAL_CANCEL, handleModalCancel);
|
return (modal.view && modal.view.webContents && modal.view.webContents.id === event.sender.id);
|
||||||
|
});
|
||||||
|
return requestModal;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function findModalByCaller(event: IpcMainInvokeEvent) {
|
handleInfoRequest = (event: IpcMainInvokeEvent) => {
|
||||||
if (modalQueue.length) {
|
const requestModal = this.findModalByCaller(event);
|
||||||
const requestModal = modalQueue.find((modal) => {
|
if (requestModal) {
|
||||||
return (modal.view && modal.view.webContents && modal.view.webContents.id === event.sender.id);
|
return requestModal.handleInfoRequest();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal = () => {
|
||||||
|
const withDevTools = process.env.MM_DEBUG_MODALS || false;
|
||||||
|
this.modalQueue.forEach((modal, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
WindowManager.sendToRenderer(MODAL_OPEN);
|
||||||
|
modal.show(undefined, Boolean(withDevTools));
|
||||||
|
} else {
|
||||||
|
WindowManager.sendToRenderer(MODAL_CLOSE);
|
||||||
|
modal.hide();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return requestModal;
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleInfoRequest(event: IpcMainInvokeEvent) {
|
handleModalFinished = (mode: 'resolve' | 'reject', event: IpcMainEvent, data: unknown) => {
|
||||||
const requestModal = findModalByCaller(event);
|
const requestModal = this.findModalByCaller(event);
|
||||||
if (requestModal) {
|
if (requestModal) {
|
||||||
return requestModal.handleInfoRequest();
|
if (mode === 'resolve') {
|
||||||
}
|
requestModal.resolve(data);
|
||||||
return null;
|
} else {
|
||||||
}
|
requestModal.reject(data);
|
||||||
|
}
|
||||||
export function showModal() {
|
this.modalPromises.delete(requestModal.key);
|
||||||
const withDevTools = process.env.MM_DEBUG_MODALS || false;
|
}
|
||||||
modalQueue.forEach((modal, index) => {
|
this.filterActive();
|
||||||
if (index === 0) {
|
if (this.modalQueue.length) {
|
||||||
WindowManager.sendToRenderer(MODAL_OPEN);
|
this.showModal();
|
||||||
modal.show(undefined, Boolean(withDevTools));
|
|
||||||
} else {
|
} else {
|
||||||
WindowManager.sendToRenderer(MODAL_CLOSE);
|
WindowManager.sendToRenderer(MODAL_CLOSE);
|
||||||
modal.hide();
|
WindowManager.focusBrowserView();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleModalResult(event: IpcMainEvent, data: unknown) {
|
|
||||||
const requestModal = findModalByCaller(event);
|
|
||||||
if (requestModal) {
|
|
||||||
requestModal.resolve(data);
|
|
||||||
modalPromises.delete(requestModal.key);
|
|
||||||
}
|
}
|
||||||
filterActive();
|
|
||||||
if (modalQueue.length) {
|
handleModalResult = (event: IpcMainEvent, data: unknown) => this.handleModalFinished('resolve', event, data);
|
||||||
showModal();
|
|
||||||
} else {
|
handleModalCancel = (event: IpcMainEvent, data: unknown) => this.handleModalFinished('reject', event, data);
|
||||||
WindowManager.sendToRenderer(MODAL_CLOSE);
|
|
||||||
WindowManager.focusBrowserView();
|
filterActive = () => {
|
||||||
|
this.modalQueue = this.modalQueue.filter((modal) => modal.isActive());
|
||||||
|
}
|
||||||
|
|
||||||
|
isModalDisplayed = () => {
|
||||||
|
return this.modalQueue.some((modal) => modal.isActive());
|
||||||
|
}
|
||||||
|
|
||||||
|
focusCurrentModal = () => {
|
||||||
|
if (this.isModalDisplayed()) {
|
||||||
|
this.modalQueue[0].view.webContents.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEmitConfiguration = (event: IpcMainEvent, config: CombinedConfig) => {
|
||||||
|
this.modalQueue.forEach((modal) => {
|
||||||
|
modal.view.webContents.send(DARK_MODE_CHANGE, config.darkMode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleGetModalUncloseable = (event: IpcMainInvokeEvent) => {
|
||||||
|
const modalView = this.modalQueue.find((modal) => modal.view.webContents.id === event.sender.id);
|
||||||
|
return modalView?.uncloseable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleModalCancel(event: IpcMainEvent, data: unknown) {
|
const modalManager = new ModalManager();
|
||||||
const requestModal = findModalByCaller(event);
|
export default modalManager;
|
||||||
if (requestModal) {
|
|
||||||
requestModal.reject(data);
|
|
||||||
modalPromises.delete(requestModal.key);
|
|
||||||
}
|
|
||||||
filterActive();
|
|
||||||
if (modalQueue.length) {
|
|
||||||
showModal();
|
|
||||||
} else {
|
|
||||||
WindowManager.sendToRenderer(MODAL_CLOSE);
|
|
||||||
WindowManager.focusBrowserView();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterActive() {
|
|
||||||
modalQueue = modalQueue.filter((modal) => modal.isActive());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isModalDisplayed() {
|
|
||||||
return modalQueue.some((modal) => modal.isActive());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function focusCurrentModal() {
|
|
||||||
if (isModalDisplayed()) {
|
|
||||||
modalQueue[0].view.webContents.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.on(EMIT_CONFIGURATION, (event: IpcMainEvent, config: CombinedConfig) => {
|
|
||||||
modalQueue.forEach((modal) => {
|
|
||||||
modal.view.webContents.send(DARK_MODE_CHANGE, config.darkMode);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleGetModalUncloseable(event: IpcMainInvokeEvent) {
|
|
||||||
const modalView = modalQueue.find((modal) => modal.view.webContents.id === event.sender.id);
|
|
||||||
return modalView?.uncloseable;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
123
src/main/views/modalView.test.js
Normal file
123
src/main/views/modalView.test.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import {ModalView} from './modalView';
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
BrowserView: jest.fn().mockImplementation(() => ({
|
||||||
|
webContents: {
|
||||||
|
loadURL: jest.fn(),
|
||||||
|
once: jest.fn(),
|
||||||
|
isLoading: jest.fn(),
|
||||||
|
focus: jest.fn(),
|
||||||
|
openDevTools: jest.fn(),
|
||||||
|
isDevToolsOpened: jest.fn(),
|
||||||
|
closeDevTools: jest.fn(),
|
||||||
|
destroy: jest.fn(),
|
||||||
|
},
|
||||||
|
setBounds: jest.fn(),
|
||||||
|
setAutoResize: jest.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('electron-log', () => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../contextMenu', () => jest.fn());
|
||||||
|
|
||||||
|
jest.mock('../utils', () => ({
|
||||||
|
getWindowBoundaries: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('main/views/modalView', () => {
|
||||||
|
describe('show', () => {
|
||||||
|
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn()};
|
||||||
|
const onResolve = jest.fn();
|
||||||
|
const onReject = jest.fn();
|
||||||
|
let modalView;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
modalView = new ModalView(
|
||||||
|
'test_modal',
|
||||||
|
'some_html',
|
||||||
|
'preload',
|
||||||
|
{value1: 'value-1', value2: 'value-2'},
|
||||||
|
onResolve,
|
||||||
|
onReject,
|
||||||
|
window,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
modalView.view.webContents.isLoading = jest.fn().mockReturnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add to window', () => {
|
||||||
|
modalView.show();
|
||||||
|
expect(window.addBrowserView).toBeCalledWith(modalView.view);
|
||||||
|
expect(modalView.status).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reattach if already attached', () => {
|
||||||
|
modalView.windowAttached = window;
|
||||||
|
modalView.show();
|
||||||
|
expect(window.removeBrowserView).toBeCalledWith(modalView.view);
|
||||||
|
expect(window.addBrowserView).toBeCalledWith(modalView.view);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delay call to focus when the modal is loading', () => {
|
||||||
|
let callback;
|
||||||
|
modalView.view.webContents.isLoading = jest.fn().mockReturnValue(true);
|
||||||
|
modalView.view.webContents.once = jest.fn().mockImplementation((event, cb) => {
|
||||||
|
callback = cb;
|
||||||
|
});
|
||||||
|
modalView.show();
|
||||||
|
expect(modalView.view.webContents.once).toHaveBeenCalled();
|
||||||
|
expect(modalView.view.webContents.focus).not.toHaveBeenCalled();
|
||||||
|
callback();
|
||||||
|
expect(modalView.view.webContents.focus).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open dev tools when specified', () => {
|
||||||
|
modalView.show(undefined, true);
|
||||||
|
expect(modalView.view.webContents.openDevTools).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hide', () => {
|
||||||
|
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn()};
|
||||||
|
const onResolve = jest.fn();
|
||||||
|
const onReject = jest.fn();
|
||||||
|
let modalView;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
modalView = new ModalView(
|
||||||
|
'test_modal',
|
||||||
|
'some_html',
|
||||||
|
'preload',
|
||||||
|
{value1: 'value-1', value2: 'value-2'},
|
||||||
|
onResolve,
|
||||||
|
onReject,
|
||||||
|
window,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
modalView.view.webContents.isLoading = jest.fn().mockReturnValue(false);
|
||||||
|
modalView.windowAttached = window;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove browser view and destroy web contents on hide', () => {
|
||||||
|
modalView.hide();
|
||||||
|
expect(modalView.view.webContents.destroy).toBeCalled();
|
||||||
|
expect(window.removeBrowserView).toBeCalledWith(modalView.view);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close dev tools when open', () => {
|
||||||
|
modalView.view.webContents.isDevToolsOpened = jest.fn().mockReturnValue(true);
|
||||||
|
modalView.hide();
|
||||||
|
expect(modalView.view.webContents.closeDevTools).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
60
src/main/views/teamDropdownView.test.js
Normal file
60
src/main/views/teamDropdownView.test.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants';
|
||||||
|
|
||||||
|
import TeamDropdownView from './teamDropdownView';
|
||||||
|
|
||||||
|
jest.mock('main/utils', () => ({
|
||||||
|
getLocalPreload: (file) => file,
|
||||||
|
getLocalURLString: (file) => file,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
BrowserView: jest.fn().mockImplementation(() => ({
|
||||||
|
webContents: {
|
||||||
|
loadURL: jest.fn(),
|
||||||
|
focus: jest.fn(),
|
||||||
|
},
|
||||||
|
setBounds: jest.fn(),
|
||||||
|
})),
|
||||||
|
ipcMain: {
|
||||||
|
on: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../windows/windowManager', () => ({
|
||||||
|
sendToRenderer: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('main/views/teamDropdownView', () => {
|
||||||
|
const window = {
|
||||||
|
getContentBounds: () => ({width: 500, height: 400, x: 0, y: 0}),
|
||||||
|
addBrowserView: jest.fn(),
|
||||||
|
setTopBrowserView: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('getBounds', () => {
|
||||||
|
const teamDropdownView = new TeamDropdownView(window, [], false, true);
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
it('should account for three dot menu, tab bar and shadow', () => {
|
||||||
|
expect(teamDropdownView.getBounds(400, 300)).toStrictEqual({x: THREE_DOT_MENU_WIDTH_MAC - MENU_SHADOW_WIDTH, y: TAB_BAR_HEIGHT - MENU_SHADOW_WIDTH, width: 400, height: 300});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
it('should account for three dot menu, tab bar and shadow', () => {
|
||||||
|
expect(teamDropdownView.getBounds(400, 300)).toStrictEqual({x: THREE_DOT_MENU_WIDTH - MENU_SHADOW_WIDTH, y: TAB_BAR_HEIGHT - MENU_SHADOW_WIDTH, width: 400, height: 300});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change the view bounds based on open/closed state', () => {
|
||||||
|
const teamDropdownView = new TeamDropdownView(window, [], false, true);
|
||||||
|
teamDropdownView.bounds = {width: 400, height: 300};
|
||||||
|
teamDropdownView.handleOpen();
|
||||||
|
expect(teamDropdownView.view.setBounds).toBeCalledWith(teamDropdownView.bounds);
|
||||||
|
teamDropdownView.handleClose();
|
||||||
|
expect(teamDropdownView.view.setBounds).toBeCalledWith({width: 0, height: 0, x: expect.any(Number), y: expect.any(Number)});
|
||||||
|
});
|
||||||
|
});
|
704
src/main/views/viewManager.test.js
Normal file
704
src/main/views/viewManager.test.js
Normal file
@@ -0,0 +1,704 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
/* eslint-disable max-lines */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import {dialog} from 'electron';
|
||||||
|
|
||||||
|
import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS} from 'common/communication';
|
||||||
|
import {MattermostServer} from 'common/servers/MattermostServer';
|
||||||
|
import {getServerView, getTabViewName} from 'common/tabs/TabView';
|
||||||
|
import urlUtils from 'common/utils/url';
|
||||||
|
|
||||||
|
import {MattermostView} from './MattermostView';
|
||||||
|
import {ViewManager} from './viewManager';
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
dialog: {
|
||||||
|
showErrorBox: jest.fn(),
|
||||||
|
},
|
||||||
|
ipcMain: {
|
||||||
|
emit: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('electron-log', () => ({
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('common/tabs/TabView', () => ({
|
||||||
|
getServerView: jest.fn(),
|
||||||
|
getTabViewName: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('common/servers/MattermostServer', () => ({
|
||||||
|
MattermostServer: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('common/utils/url', () => ({
|
||||||
|
parseURL: (url) => {
|
||||||
|
try {
|
||||||
|
return new URL(url);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getView: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('main/server/serverInfo', () => ({
|
||||||
|
ServerInfo: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./MattermostView', () => ({
|
||||||
|
MattermostView: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./modalManager', () => ({
|
||||||
|
showModal: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('./webContentEvents', () => ({}));
|
||||||
|
|
||||||
|
describe('main/views/viewManager', () => {
|
||||||
|
describe('loadView', () => {
|
||||||
|
const viewManager = new ViewManager({}, {});
|
||||||
|
const onceFn = jest.fn();
|
||||||
|
const loadFn = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
viewManager.createLoadingScreen = jest.fn();
|
||||||
|
viewManager.showByName = jest.fn();
|
||||||
|
getServerView.mockImplementation((srv, tab) => ({name: `${srv.name}-${tab.name}`}));
|
||||||
|
MattermostView.mockImplementation(() => ({
|
||||||
|
on: jest.fn(),
|
||||||
|
load: loadFn,
|
||||||
|
once: onceFn,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
viewManager.loadingScreen = undefined;
|
||||||
|
viewManager.closedViews = new Map();
|
||||||
|
viewManager.views = new Map();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add closed tabs to closedViews', () => {
|
||||||
|
viewManager.loadView({name: 'server1'}, {}, {name: 'tab1', isOpen: false});
|
||||||
|
expect(viewManager.closedViews.has('server1-tab1')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove from remove from closedViews when the tab is open', () => {
|
||||||
|
viewManager.closedViews.set('server1-tab1', {});
|
||||||
|
expect(viewManager.closedViews.has('server1-tab1')).toBe(true);
|
||||||
|
viewManager.loadView({name: 'server1'}, {}, {name: 'tab1', isOpen: true});
|
||||||
|
expect(viewManager.closedViews.has('server1-tab1')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add view to views map, add listeners and show the view', () => {
|
||||||
|
viewManager.loadView({name: 'server1'}, {}, {name: 'tab1', isOpen: true}, 'http://server-1.com/subpath');
|
||||||
|
expect(viewManager.views.has('server1-tab1')).toBe(true);
|
||||||
|
expect(viewManager.createLoadingScreen).toHaveBeenCalled();
|
||||||
|
expect(onceFn).toHaveBeenCalledWith(LOAD_SUCCESS, viewManager.activateView);
|
||||||
|
expect(loadFn).toHaveBeenCalledWith('http://server-1.com/subpath');
|
||||||
|
expect(viewManager.showByName).toHaveBeenCalledWith('server1-tab1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reloadViewIfNeeded', () => {
|
||||||
|
const viewManager = new ViewManager({}, {});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
viewManager.views = new Map();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload view when URL is not on subpath of original server URL', () => {
|
||||||
|
const view = {
|
||||||
|
load: jest.fn(),
|
||||||
|
view: {
|
||||||
|
webContents: {
|
||||||
|
getURL: () => 'http://server-2.com/subpath',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
url: new URL('http://server-1.com/'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
viewManager.views.set('view1', view);
|
||||||
|
viewManager.reloadViewIfNeeded('view1');
|
||||||
|
expect(view.load).toHaveBeenCalledWith(new URL('http://server-1.com/'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not reload if URLs are matching', () => {
|
||||||
|
const view = {
|
||||||
|
load: jest.fn(),
|
||||||
|
view: {
|
||||||
|
webContents: {
|
||||||
|
getURL: () => 'http://server-1.com/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
url: new URL('http://server-1.com/'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
viewManager.views.set('view1', view);
|
||||||
|
viewManager.reloadViewIfNeeded('view1');
|
||||||
|
expect(view.load).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not reload if URL is subpath of server URL', () => {
|
||||||
|
const view = {
|
||||||
|
load: jest.fn(),
|
||||||
|
view: {
|
||||||
|
webContents: {
|
||||||
|
getURL: () => 'http://server-1.com/subpath',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
url: new URL('http://server-1.com/'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
viewManager.views.set('view1', view);
|
||||||
|
viewManager.reloadViewIfNeeded('view1');
|
||||||
|
expect(view.load).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reloadConfiguration', () => {
|
||||||
|
const viewManager = new ViewManager({}, {});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
viewManager.loadView = jest.fn();
|
||||||
|
viewManager.showByName = jest.fn();
|
||||||
|
viewManager.showInitial = jest.fn();
|
||||||
|
getServerView.mockImplementation((srv, tab) => ({name: `${srv.name}-${tab.name}`, url: new URL(`http://${srv.name}.com`)}));
|
||||||
|
MattermostServer.mockImplementation((name, url) => ({
|
||||||
|
name,
|
||||||
|
url: new URL(url),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
delete viewManager.loadingScreen;
|
||||||
|
delete viewManager.currentView;
|
||||||
|
viewManager.closedViews = new Map();
|
||||||
|
viewManager.views = new Map();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should recycle existing views', () => {
|
||||||
|
const view = {
|
||||||
|
name: 'server1-tab1',
|
||||||
|
tab: {
|
||||||
|
name: 'server1-tab1',
|
||||||
|
url: new URL('http://server1.com'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
viewManager.views.set('server1-tab1', view);
|
||||||
|
viewManager.reloadConfiguration([
|
||||||
|
{
|
||||||
|
name: 'server1',
|
||||||
|
url: 'http://server1.com',
|
||||||
|
order: 1,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'tab1',
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(viewManager.views.get('server1-tab1')).toBe(view);
|
||||||
|
expect(viewManager.loadView).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close tabs that arent open', () => {
|
||||||
|
viewManager.reloadConfiguration([
|
||||||
|
{
|
||||||
|
name: 'server1',
|
||||||
|
url: 'http://server1.com',
|
||||||
|
order: 1,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'tab1',
|
||||||
|
isOpen: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(viewManager.closedViews.has('server1-tab1')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create new views for new tabs', () => {
|
||||||
|
viewManager.reloadConfiguration([
|
||||||
|
{
|
||||||
|
name: 'server1',
|
||||||
|
url: 'http://server1.com',
|
||||||
|
order: 1,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'tab1',
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(viewManager.loadView).toHaveBeenCalledWith({
|
||||||
|
name: 'server1',
|
||||||
|
url: new URL('http://server1.com'),
|
||||||
|
}, expect.any(Object), {
|
||||||
|
name: 'tab1',
|
||||||
|
isOpen: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set focus to current view on reload', () => {
|
||||||
|
const view = {
|
||||||
|
name: 'server1-tab1',
|
||||||
|
tab: {
|
||||||
|
name: 'server1-tab1',
|
||||||
|
url: new URL('http://server1.com'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
viewManager.currentView = 'server1-tab1';
|
||||||
|
viewManager.views.set('server1-tab1', view);
|
||||||
|
viewManager.reloadConfiguration([
|
||||||
|
{
|
||||||
|
name: 'server1',
|
||||||
|
url: 'http://server1.com',
|
||||||
|
order: 1,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'tab1',
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(viewManager.showByName).toHaveBeenCalledWith('server1-tab1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show initial if currentView has been removed', () => {
|
||||||
|
const view = {
|
||||||
|
name: 'server1-tab1',
|
||||||
|
tab: {
|
||||||
|
name: 'server1-tab1',
|
||||||
|
url: new URL('http://server1.com'),
|
||||||
|
},
|
||||||
|
destroy: jest.fn(),
|
||||||
|
};
|
||||||
|
viewManager.currentView = 'server1-tab1';
|
||||||
|
viewManager.views.set('server1-tab1', view);
|
||||||
|
viewManager.reloadConfiguration([
|
||||||
|
{
|
||||||
|
name: 'server2',
|
||||||
|
url: 'http://server2.com',
|
||||||
|
order: 1,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'tab1',
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(viewManager.currentView).toBeUndefined();
|
||||||
|
expect(viewManager.showInitial).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove unused views', () => {
|
||||||
|
const view = {
|
||||||
|
name: 'server1-tab1',
|
||||||
|
tab: {
|
||||||
|
name: 'server1-tab1',
|
||||||
|
url: new URL('http://server1.com'),
|
||||||
|
},
|
||||||
|
destroy: jest.fn(),
|
||||||
|
};
|
||||||
|
viewManager.views.set('server1-tab1', view);
|
||||||
|
viewManager.reloadConfiguration([
|
||||||
|
{
|
||||||
|
name: 'server2',
|
||||||
|
url: 'http://server2.com',
|
||||||
|
order: 1,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'tab1',
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(view.destroy).toBeCalled();
|
||||||
|
expect(viewManager.showInitial).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showInitial', () => {
|
||||||
|
const 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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
const viewManager = new ViewManager({teams}, {});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
viewManager.showByName = jest.fn();
|
||||||
|
getTabViewName.mockImplementation((server, tab) => `${server}_${tab}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
viewManager.configServers = teams;
|
||||||
|
delete viewManager.lastActiveServer;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show first server and first open tab in order when last active not defined', () => {
|
||||||
|
viewManager.showInitial();
|
||||||
|
expect(viewManager.showByName).toHaveBeenCalledWith('server-2_tab-3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show first tab in order of last active server', () => {
|
||||||
|
viewManager.lastActiveServer = 1;
|
||||||
|
viewManager.showInitial();
|
||||||
|
expect(viewManager.showByName).toHaveBeenCalledWith('server-1_tab-3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show last active tab of first server', () => {
|
||||||
|
viewManager.configServers = [{
|
||||||
|
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,
|
||||||
|
}];
|
||||||
|
viewManager.showInitial();
|
||||||
|
expect(viewManager.showByName).toHaveBeenCalledWith('server-2_tab-2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show next tab when last active tab is closed', () => {
|
||||||
|
viewManager.configServers = [{
|
||||||
|
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: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tab-2',
|
||||||
|
order: 2,
|
||||||
|
isOpen: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tab-3',
|
||||||
|
order: 1,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 2,
|
||||||
|
}];
|
||||||
|
viewManager.showInitial();
|
||||||
|
expect(viewManager.showByName).toHaveBeenCalledWith('server-2_tab-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showByName', () => {
|
||||||
|
const viewManager = new ViewManager({}, {});
|
||||||
|
const baseView = {
|
||||||
|
isReady: jest.fn(),
|
||||||
|
show: jest.fn(),
|
||||||
|
hide: jest.fn(),
|
||||||
|
needsLoadingScreen: jest.fn(),
|
||||||
|
window: {
|
||||||
|
webContents: {
|
||||||
|
send: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
server: {
|
||||||
|
name: 'server-1',
|
||||||
|
},
|
||||||
|
type: 'tab-1',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
viewManager.getCurrentView = jest.fn();
|
||||||
|
viewManager.showLoadingScreen = jest.fn();
|
||||||
|
viewManager.fadeLoadingScreen = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
viewManager.views = new Map();
|
||||||
|
delete viewManager.currentView;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing when view is already visible or if view doesnt exist', () => {
|
||||||
|
const view = {
|
||||||
|
...baseView,
|
||||||
|
isVisible: true,
|
||||||
|
};
|
||||||
|
viewManager.views.set('server1-tab1', view);
|
||||||
|
|
||||||
|
viewManager.showByName('server1-tab1');
|
||||||
|
expect(viewManager.currentView).toBeUndefined();
|
||||||
|
expect(view.isReady).not.toBeCalled();
|
||||||
|
expect(view.show).not.toBeCalled();
|
||||||
|
|
||||||
|
viewManager.showByName('some-view-name');
|
||||||
|
expect(viewManager.currentView).toBeUndefined();
|
||||||
|
expect(view.isReady).not.toBeCalled();
|
||||||
|
expect(view.show).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide current view when new view is shown', () => {
|
||||||
|
const oldView = {
|
||||||
|
...baseView,
|
||||||
|
isVisible: true,
|
||||||
|
};
|
||||||
|
const newView = {
|
||||||
|
...baseView,
|
||||||
|
isVisible: false,
|
||||||
|
};
|
||||||
|
viewManager.getCurrentView.mockImplementation(() => oldView);
|
||||||
|
viewManager.views.set('oldView', oldView);
|
||||||
|
viewManager.views.set('newView', newView);
|
||||||
|
viewManager.currentView = 'oldView';
|
||||||
|
viewManager.showByName('newView');
|
||||||
|
expect(oldView.hide).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show loading screen when the view needs it', () => {
|
||||||
|
const view = {...baseView};
|
||||||
|
view.needsLoadingScreen.mockImplementation(() => true);
|
||||||
|
viewManager.views.set('view1', view);
|
||||||
|
viewManager.showByName('view1');
|
||||||
|
expect(viewManager.showLoadingScreen).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the view when ready', () => {
|
||||||
|
const view = {...baseView};
|
||||||
|
view.needsLoadingScreen.mockImplementation(() => false);
|
||||||
|
view.isReady.mockImplementation(() => true);
|
||||||
|
viewManager.views.set('view1', view);
|
||||||
|
viewManager.showByName('view1');
|
||||||
|
expect(viewManager.currentView).toBe('view1');
|
||||||
|
expect(view.show).toHaveBeenCalled();
|
||||||
|
expect(viewManager.fadeLoadingScreen).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showLoadingScreen', () => {
|
||||||
|
const window = {
|
||||||
|
getBrowserViews: jest.fn(),
|
||||||
|
setTopBrowserView: jest.fn(),
|
||||||
|
addBrowserView: jest.fn(),
|
||||||
|
};
|
||||||
|
const viewManager = new ViewManager({}, window);
|
||||||
|
const loadingScreen = {webContents: {send: jest.fn()}};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
viewManager.createLoadingScreen = jest.fn();
|
||||||
|
viewManager.setLoadingScreenBounds = jest.fn();
|
||||||
|
window.getBrowserViews.mockImplementation(() => []);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
delete viewManager.loadingScreen;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create new loading screen if one doesnt exist and add it to the window', () => {
|
||||||
|
viewManager.createLoadingScreen.mockImplementation(() => {
|
||||||
|
viewManager.loadingScreen = loadingScreen;
|
||||||
|
});
|
||||||
|
viewManager.showLoadingScreen();
|
||||||
|
expect(viewManager.createLoadingScreen).toHaveBeenCalled();
|
||||||
|
expect(window.addBrowserView).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the browser view as top if already exists and needs to be shown', () => {
|
||||||
|
viewManager.loadingScreen = loadingScreen;
|
||||||
|
window.getBrowserViews.mockImplementation(() => [loadingScreen]);
|
||||||
|
viewManager.showLoadingScreen();
|
||||||
|
expect(window.setTopBrowserView).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleDeepLink', () => {
|
||||||
|
const viewManager = new ViewManager({}, {});
|
||||||
|
const baseView = {
|
||||||
|
resetLoadingStatus: jest.fn(),
|
||||||
|
load: jest.fn(),
|
||||||
|
once: jest.fn(),
|
||||||
|
isInitialized: jest.fn(),
|
||||||
|
view: {
|
||||||
|
webContents: {
|
||||||
|
send: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serverInfo: {
|
||||||
|
remoteInfo: {
|
||||||
|
serverVersion: '1.0.0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
viewManager.openClosedTab = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
viewManager.views = new Map();
|
||||||
|
viewManager.closedViews = new Map();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load URL into matching view', () => {
|
||||||
|
urlUtils.getView.mockImplementation(() => ({name: 'view1', url: 'http://server-1.com/'}));
|
||||||
|
const view = {...baseView};
|
||||||
|
viewManager.views.set('view1', view);
|
||||||
|
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
|
||||||
|
expect(view.load).toHaveBeenCalledWith('http://server-1.com/deep/link?thing=yes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send the URL to the view if its already loaded on a 6.0 server', () => {
|
||||||
|
urlUtils.getView.mockImplementation(() => ({name: 'view1', url: 'http://server-1.com/'}));
|
||||||
|
const view = {
|
||||||
|
...baseView,
|
||||||
|
serverInfo: {
|
||||||
|
remoteInfo: {
|
||||||
|
serverVersion: '6.0.0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
server: {
|
||||||
|
url: new URL('http://server-1.com'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
view.isInitialized.mockImplementation(() => true);
|
||||||
|
viewManager.views.set('view1', view);
|
||||||
|
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
|
||||||
|
expect(view.view.webContents.send).toHaveBeenCalledWith(BROWSER_HISTORY_PUSH, '/deep/link?thing=yes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if view is missing', () => {
|
||||||
|
urlUtils.getView.mockImplementation(() => ({name: 'view1', url: 'http://server-1.com/'}));
|
||||||
|
const view = {...baseView};
|
||||||
|
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
|
||||||
|
expect(view.load).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw dialog when cannot find the view', () => {
|
||||||
|
const view = {...baseView};
|
||||||
|
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
|
||||||
|
expect(view.load).not.toHaveBeenCalled();
|
||||||
|
expect(dialog.showErrorBox).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reopen closed tab if called upon', () => {
|
||||||
|
urlUtils.getView.mockImplementation(() => ({name: 'view1', url: 'https://server-1.com/'}));
|
||||||
|
viewManager.closedViews.set('view1', {});
|
||||||
|
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
|
||||||
|
expect(viewManager.openClosedTab).toHaveBeenCalledWith('view1', 'https://server-1.com/deep/link?thing=yes');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -21,16 +21,16 @@ import {
|
|||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import urlUtils from 'common/utils/url';
|
import urlUtils from 'common/utils/url';
|
||||||
import Utils from 'common/utils/util';
|
import Utils from 'common/utils/util';
|
||||||
|
import {MattermostServer} from 'common/servers/MattermostServer';
|
||||||
import {getServerView, getTabViewName} from 'common/tabs/TabView';
|
import {getServerView, getTabViewName} from 'common/tabs/TabView';
|
||||||
|
|
||||||
import {ServerInfo} from 'main/server/serverInfo';
|
import {ServerInfo} from 'main/server/serverInfo';
|
||||||
import {MattermostServer} from '../../common/servers/MattermostServer';
|
|
||||||
import {getLocalURLString, getLocalPreload, getWindowBoundaries} from '../utils';
|
import {getLocalURLString, getLocalPreload, getWindowBoundaries} from '../utils';
|
||||||
|
|
||||||
import {MattermostView, Status} from './MattermostView';
|
import {MattermostView} from './MattermostView';
|
||||||
import {showModal, isModalDisplayed, focusCurrentModal} from './modalManager';
|
import modalManager from './modalManager';
|
||||||
import {addWebContentsEventListeners} from './webContentEvents';
|
import WebContentsEventManager from './webContentEvents';
|
||||||
|
|
||||||
const URL_VIEW_DURATION = 10 * SECOND;
|
const URL_VIEW_DURATION = 10 * SECOND;
|
||||||
const URL_VIEW_HEIGHT = 36;
|
const URL_VIEW_HEIGHT = 36;
|
||||||
@@ -94,7 +94,7 @@ export class ViewManager {
|
|||||||
|
|
||||||
reloadViewIfNeeded = (viewName: string) => {
|
reloadViewIfNeeded = (viewName: string) => {
|
||||||
const view = this.views.get(viewName);
|
const view = this.views.get(viewName);
|
||||||
if (view && !view.view.webContents.getURL().startsWith(view.tab.url.toString())) {
|
if (view && view.view.webContents.getURL() !== view.tab.url.toString() && !view.view.webContents.getURL().startsWith(view.tab.url.toString())) {
|
||||||
view.load(view.tab.url);
|
view.load(view.tab.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ export class ViewManager {
|
|||||||
let tab = element.tabs.find((tab) => tab.order === element.lastActiveTab) || element.tabs.find((tab) => tab.order === 0);
|
let tab = element.tabs.find((tab) => tab.order === element.lastActiveTab) || element.tabs.find((tab) => tab.order === 0);
|
||||||
if (!tab?.isOpen) {
|
if (!tab?.isOpen) {
|
||||||
const openTabs = element.tabs.filter((tab) => tab.isOpen);
|
const openTabs = element.tabs.filter((tab) => tab.isOpen);
|
||||||
tab = openTabs.find((e) => e.order === 0) || openTabs[0];
|
tab = openTabs.find((e) => e.order === 0) || openTabs.concat().sort((a, b) => a.order - b.order)[0];
|
||||||
}
|
}
|
||||||
if (tab) {
|
if (tab) {
|
||||||
const tabView = getTabViewName(element.name, tab.name);
|
const tabView = getTabViewName(element.name, tab.name);
|
||||||
@@ -186,26 +186,21 @@ export class ViewManager {
|
|||||||
// if view is not ready, the renderer will have something to display instead.
|
// if view is not ready, the renderer will have something to display instead.
|
||||||
newView.show();
|
newView.show();
|
||||||
ipcMain.emit(UPDATE_LAST_ACTIVE, true, newView.tab.server.name, newView.tab.type);
|
ipcMain.emit(UPDATE_LAST_ACTIVE, true, newView.tab.server.name, newView.tab.type);
|
||||||
if (newView.needsLoadingScreen()) {
|
if (!newView.needsLoadingScreen()) {
|
||||||
this.showLoadingScreen();
|
|
||||||
} else {
|
|
||||||
this.fadeLoadingScreen();
|
this.fadeLoadingScreen();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn(`couldn't show ${name}, not ready`);
|
log.warn(`couldn't show ${name}, not ready`);
|
||||||
if (newView.needsLoadingScreen()) {
|
|
||||||
this.showLoadingScreen();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn(`Couldn't find a view with name: ${name}`);
|
log.warn(`Couldn't find a view with name: ${name}`);
|
||||||
}
|
}
|
||||||
showModal();
|
modalManager.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
focus = () => {
|
focus = () => {
|
||||||
if (isModalDisplayed()) {
|
if (modalManager.isModalDisplayed()) {
|
||||||
focusCurrentModal();
|
modalManager.focusCurrentModal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +209,7 @@ export class ViewManager {
|
|||||||
view.focus();
|
view.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
activateView = (viewName: string) => {
|
activateView = (viewName: string) => {
|
||||||
if (this.currentView === viewName) {
|
if (this.currentView === viewName) {
|
||||||
this.showByName(this.currentView);
|
this.showByName(this.currentView);
|
||||||
@@ -223,7 +219,7 @@ export class ViewManager {
|
|||||||
log.error(`Couldn't find a view with the name ${viewName}`);
|
log.error(`Couldn't find a view with the name ${viewName}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addWebContentsEventListeners(view, this.getServers);
|
WebContentsEventManager.addWebContentsEventListeners(view, this.getServers);
|
||||||
}
|
}
|
||||||
|
|
||||||
finishLoading = (server: string) => {
|
finishLoading = (server: string) => {
|
||||||
@@ -428,7 +424,7 @@ export class ViewManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (view.status === Status.READY && view.serverInfo.remoteInfo.serverVersion && Utils.isVersionGreaterThanOrEqualTo(view.serverInfo.remoteInfo.serverVersion, '6.0.0')) {
|
if (view.isInitialized() && view.serverInfo.remoteInfo.serverVersion && Utils.isVersionGreaterThanOrEqualTo(view.serverInfo.remoteInfo.serverVersion, '6.0.0')) {
|
||||||
const pathName = `/${urlWithSchema.replace(view.tab.server.url.toString(), '')}`;
|
const pathName = `/${urlWithSchema.replace(view.tab.server.url.toString(), '')}`;
|
||||||
view.view.webContents.send(BROWSER_HISTORY_PUSH, pathName);
|
view.view.webContents.send(BROWSER_HISTORY_PUSH, pathName);
|
||||||
this.deeplinkSuccess(view.name);
|
this.deeplinkSuccess(view.name);
|
||||||
|
236
src/main/views/webContentEvents.test.js
Normal file
236
src/main/views/webContentEvents.test.js
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import {shell} from 'electron';
|
||||||
|
|
||||||
|
import urlUtils from 'common/utils/url';
|
||||||
|
|
||||||
|
import * as WindowManager from '../windows/windowManager';
|
||||||
|
import allowProtocolDialog from '../allowProtocolDialog';
|
||||||
|
|
||||||
|
import {WebContentsEventManager} from './webContentEvents';
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
app: {},
|
||||||
|
shell: {
|
||||||
|
openExternal: jest.fn(),
|
||||||
|
},
|
||||||
|
BrowserWindow: jest.fn().mockImplementation(() => ({
|
||||||
|
once: jest.fn(),
|
||||||
|
show: jest.fn(),
|
||||||
|
loadURL: jest.fn(),
|
||||||
|
webContents: {
|
||||||
|
setWindowOpenHandler: jest.fn(),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('electron-log', () => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../allowProtocolDialog', () => ({}));
|
||||||
|
jest.mock('../windows/windowManager', () => ({
|
||||||
|
showMainWindow: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('common/utils/url', () => ({
|
||||||
|
parseURL: (url) => {
|
||||||
|
try {
|
||||||
|
return new URL(url);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getView: jest.fn(),
|
||||||
|
isTeamUrl: jest.fn(),
|
||||||
|
isAdminUrl: jest.fn(),
|
||||||
|
isTrustedPopupWindow: jest.fn(),
|
||||||
|
isTrustedURL: jest.fn(),
|
||||||
|
isCustomLoginURL: jest.fn(),
|
||||||
|
isInternalURL: jest.fn(),
|
||||||
|
isValidURI: jest.fn(),
|
||||||
|
isPluginUrl: jest.fn(),
|
||||||
|
isManagedResource: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../electron-builder.json', () => ({
|
||||||
|
protocols: [
|
||||||
|
{
|
||||||
|
name: 'Mattermost',
|
||||||
|
schemes: ['mattermost'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../allowProtocolDialog', () => ({
|
||||||
|
handleDialogEvent: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('main/views/webContentsEvents', () => {
|
||||||
|
const event = {preventDefault: jest.fn(), sender: {id: 1}};
|
||||||
|
|
||||||
|
describe('willNavigate', () => {
|
||||||
|
const webContentsEventManager = new WebContentsEventManager();
|
||||||
|
const willNavigate = webContentsEventManager.generateWillNavigate(jest.fn());
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
urlUtils.getView.mockImplementation(() => ({name: 'server_name', url: 'http://server-1.com'}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
webContentsEventManager.customLogins = {};
|
||||||
|
webContentsEventManager.popupWindow = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow navigation when url isTeamURL', () => {
|
||||||
|
urlUtils.isTeamUrl.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(serverURL));
|
||||||
|
willNavigate(event, 'http://server-1.com/subpath');
|
||||||
|
expect(event.preventDefault).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow navigation when url isAdminURL', () => {
|
||||||
|
urlUtils.isAdminUrl.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(`${serverURL}/admin_console`));
|
||||||
|
willNavigate(event, 'http://server-1.com/admin_console/subpath');
|
||||||
|
expect(event.preventDefault).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow navigation when isTrustedPopup', () => {
|
||||||
|
const spy = jest.spyOn(webContentsEventManager, 'isTrustedPopupWindow');
|
||||||
|
spy.mockReturnValue(true);
|
||||||
|
willNavigate(event, 'http://externalurl.com/popup/subpath');
|
||||||
|
expect(event.preventDefault).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow navigation when isCustomLoginURL', () => {
|
||||||
|
urlUtils.isCustomLoginURL.mockImplementation((parsedURL) => parsedURL.toString().startsWith('http://loginurl.com/login'));
|
||||||
|
willNavigate(event, 'http://loginurl.com/login/oauth');
|
||||||
|
expect(event.preventDefault).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow navigation when protocol is mailto', () => {
|
||||||
|
willNavigate(event, 'mailto:test@mattermost.com');
|
||||||
|
expect(event.preventDefault).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow navigation when a custom login is in progress', () => {
|
||||||
|
webContentsEventManager.customLogins[1] = {inProgress: true};
|
||||||
|
willNavigate(event, 'http://anyoldurl.com');
|
||||||
|
expect(event.preventDefault).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow navigation under any other circumstances', () => {
|
||||||
|
willNavigate(event, 'http://someotherurl.com');
|
||||||
|
expect(event.preventDefault).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('didStartNavigation', () => {
|
||||||
|
const webContentsEventManager = new WebContentsEventManager();
|
||||||
|
const didStartNavigation = webContentsEventManager.generateDidStartNavigation(jest.fn());
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
urlUtils.getView.mockImplementation(() => ({name: 'server_name', url: 'http://server-1.com'}));
|
||||||
|
urlUtils.isTrustedURL.mockReturnValue(true);
|
||||||
|
urlUtils.isInternalURL.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(serverURL));
|
||||||
|
urlUtils.isCustomLoginURL.mockImplementation((parsedURL) => parsedURL.toString().startsWith('http://loginurl.com/login'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
webContentsEventManager.customLogins = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add custom login entry on custom login URL', () => {
|
||||||
|
webContentsEventManager.customLogins[1] = {inProgress: false};
|
||||||
|
didStartNavigation(event, 'http://loginurl.com/login/oauth');
|
||||||
|
expect(webContentsEventManager.customLogins[1]).toStrictEqual({inProgress: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove custom login entry once navigating back to internal URL', () => {
|
||||||
|
webContentsEventManager.customLogins[1] = {inProgress: true};
|
||||||
|
didStartNavigation(event, 'http://server-1.com/subpath');
|
||||||
|
expect(webContentsEventManager.customLogins[1]).toStrictEqual({inProgress: false});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('newWindow', () => {
|
||||||
|
const webContentsEventManager = new WebContentsEventManager();
|
||||||
|
const newWindow = webContentsEventManager.generateNewWindowListener(jest.fn());
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
urlUtils.isValidURI.mockReturnValue(true);
|
||||||
|
urlUtils.getView.mockReturnValue({name: 'server_name', url: 'http://server-1.com'});
|
||||||
|
urlUtils.isTeamUrl.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(`${serverURL}/myteam`));
|
||||||
|
urlUtils.isAdminUrl.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(`${serverURL}/admin_console`));
|
||||||
|
urlUtils.isPluginUrl.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(`${serverURL}/myplugin`));
|
||||||
|
urlUtils.isManagedResource.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(`${serverURL}/myplugin`));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
it('should deny on bad URL', () => {
|
||||||
|
expect(newWindow({url: 'a-bad<url'})).toStrictEqual({action: 'deny'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow dev tools to open', () => {
|
||||||
|
expect(newWindow({url: 'devtools://aaaaaa.com'})).toStrictEqual({action: 'allow'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deny invalid URI', () => {
|
||||||
|
urlUtils.isValidURI.mockReturnValue(false);
|
||||||
|
expect(newWindow({url: 'baduri::'})).toStrictEqual({action: 'deny'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should divert to allowProtocolDialog for custom protocols that are not mattermost or http', () => {
|
||||||
|
expect(newWindow({url: 'spotify:album:2OZbaW9tgO62ndm375lFZr'})).toStrictEqual({action: 'deny'});
|
||||||
|
expect(allowProtocolDialog.handleDialogEvent).toBeCalledWith('spotify:', 'spotify:album:2OZbaW9tgO62ndm375lFZr');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open in the browser when there is no server matching', () => {
|
||||||
|
urlUtils.getView.mockReturnValue(null);
|
||||||
|
expect(newWindow({url: 'http://server-2.com/subpath'})).toStrictEqual({action: 'deny'});
|
||||||
|
expect(shell.openExternal).toBeCalledWith('http://server-2.com/subpath');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open public file links in browser', () => {
|
||||||
|
expect(newWindow({url: 'http://server-1.com/api/v4/public/files/myfile.img'})).toStrictEqual({action: 'deny'});
|
||||||
|
expect(shell.openExternal).toBeCalledWith('http://server-1.com/api/v4/public/files/myfile.img');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open help links in the browser', () => {
|
||||||
|
expect(newWindow({url: 'http://server-1.com/help/helplink'})).toStrictEqual({action: 'deny'});
|
||||||
|
expect(shell.openExternal).toBeCalledWith('http://server-1.com/help/helplink');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open team links in the app', () => {
|
||||||
|
expect(newWindow({url: 'http://server-1.com/myteam/channels/mychannel'})).toStrictEqual({action: 'deny'});
|
||||||
|
expect(WindowManager.showMainWindow).toBeCalledWith(new URL('http://server-1.com/myteam/channels/mychannel'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prevent admin links from opening in a new window', () => {
|
||||||
|
expect(newWindow({url: 'http://server-1.com/admin_console/somepage'})).toStrictEqual({action: 'deny'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prevent from opening a new window if popup already exists', () => {
|
||||||
|
webContentsEventManager.popupWindow = {webContents: {getURL: () => 'http://server-1.com/myplugin/login'}};
|
||||||
|
expect(newWindow({url: 'http://server-1.com/myplugin/login'})).toStrictEqual({action: 'deny'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open popup window for plugins', () => {
|
||||||
|
expect(newWindow({url: 'http://server-1.com/myplugin/login'})).toStrictEqual({action: 'deny'});
|
||||||
|
expect(webContentsEventManager.popupWindow).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open popup window for managed resources', () => {
|
||||||
|
expect(newWindow({url: 'http://server-1.com/trusted/login'})).toStrictEqual({action: 'deny'});
|
||||||
|
expect(webContentsEventManager.popupWindow).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -23,238 +23,247 @@ type CustomLogin = {
|
|||||||
inProgress: boolean;
|
inProgress: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customLogins: Record<number, CustomLogin> = {};
|
|
||||||
const listeners: Record<number, () => void> = {};
|
|
||||||
let popupWindow: BrowserWindow | undefined;
|
|
||||||
|
|
||||||
function isTrustedPopupWindow(webContents: WebContents) {
|
|
||||||
if (!webContents) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!popupWindow) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return BrowserWindow.fromWebContents(webContents) === popupWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheme = protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0];
|
const scheme = protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0];
|
||||||
|
|
||||||
const generateWillNavigate = (getServersFunction: () => TeamWithTabs[]) => {
|
export class WebContentsEventManager {
|
||||||
return (event: Event & {sender: WebContents}, url: string) => {
|
customLogins: Record<number, CustomLogin>;
|
||||||
const contentID = event.sender.id;
|
listeners: Record<number, () => void>;
|
||||||
const parsedURL = urlUtils.parseURL(url)!;
|
popupWindow?: BrowserWindow;
|
||||||
const configServers = getServersFunction();
|
|
||||||
const server = urlUtils.getView(parsedURL, configServers);
|
|
||||||
|
|
||||||
if (server && (urlUtils.isTeamUrl(server.url, parsedURL) || urlUtils.isAdminUrl(server.url, parsedURL) || isTrustedPopupWindow(event.sender))) {
|
constructor() {
|
||||||
return;
|
this.customLogins = {};
|
||||||
|
this.listeners = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
isTrustedPopupWindow = (webContents: WebContents) => {
|
||||||
|
if (!webContents) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!this.popupWindow) {
|
||||||
if (server && urlUtils.isCustomLoginURL(parsedURL, server, configServers)) {
|
return false;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (parsedURL.protocol === 'mailto:') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (customLogins[contentID].inProgress) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return BrowserWindow.fromWebContents(webContents) === this.popupWindow;
|
||||||
|
}
|
||||||
|
|
||||||
log.info(`Prevented desktop from navigating to: ${url}`);
|
generateWillNavigate = (getServersFunction: () => TeamWithTabs[]) => {
|
||||||
event.preventDefault();
|
return (event: Event & {sender: WebContents}, url: string) => {
|
||||||
};
|
const contentID = event.sender.id;
|
||||||
};
|
const parsedURL = urlUtils.parseURL(url)!;
|
||||||
|
const configServers = getServersFunction();
|
||||||
|
const server = urlUtils.getView(parsedURL, configServers);
|
||||||
|
|
||||||
const generateDidStartNavigation = (getServersFunction: () => TeamWithTabs[]) => {
|
if (server && (urlUtils.isTeamUrl(server.url, parsedURL) || urlUtils.isAdminUrl(server.url, parsedURL) || this.isTrustedPopupWindow(event.sender))) {
|
||||||
return (event: Event & {sender: WebContents}, url: string) => {
|
return;
|
||||||
const serverList = getServersFunction();
|
|
||||||
const contentID = event.sender.id;
|
|
||||||
const parsedURL = urlUtils.parseURL(url)!;
|
|
||||||
const server = urlUtils.getView(parsedURL, serverList);
|
|
||||||
|
|
||||||
if (!urlUtils.isTrustedURL(parsedURL, serverList)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverURL = urlUtils.parseURL(server?.url || '');
|
|
||||||
|
|
||||||
if (server && urlUtils.isCustomLoginURL(parsedURL, server, serverList)) {
|
|
||||||
customLogins[contentID].inProgress = true;
|
|
||||||
} else if (server && customLogins[contentID].inProgress && urlUtils.isInternalURL(serverURL || new URL(''), parsedURL)) {
|
|
||||||
customLogins[contentID].inProgress = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const denyNewWindow = (event: Event, url: string) => {
|
|
||||||
event.preventDefault();
|
|
||||||
log.warn(`Prevented popup window to open a new window to ${url}.`);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateNewWindowListener = (getServersFunction: () => TeamWithTabs[], spellcheck?: boolean) => {
|
|
||||||
return (details: Electron.HandlerDetails): {action: 'deny' | 'allow'} => {
|
|
||||||
const parsedURL = urlUtils.parseURL(details.url);
|
|
||||||
if (!parsedURL) {
|
|
||||||
log.warn(`Ignoring non-url ${details.url}`);
|
|
||||||
return {action: 'deny'};
|
|
||||||
}
|
|
||||||
|
|
||||||
const configServers = getServersFunction();
|
|
||||||
|
|
||||||
// Dev tools case
|
|
||||||
if (parsedURL.protocol === 'devtools:') {
|
|
||||||
return {action: 'allow'};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for valid URL
|
|
||||||
if (!urlUtils.isValidURI(details.url)) {
|
|
||||||
return {action: 'deny'};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for custom protocol
|
|
||||||
if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:' && parsedURL.protocol !== `${scheme}:`) {
|
|
||||||
allowProtocolDialog.handleDialogEvent(parsedURL.protocol, details.url);
|
|
||||||
return {action: 'deny'};
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = urlUtils.getView(parsedURL, configServers);
|
|
||||||
|
|
||||||
if (!server) {
|
|
||||||
shell.openExternal(details.url);
|
|
||||||
return {action: 'deny'};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public download links case
|
|
||||||
// We might be handling different types differently in the future, for now
|
|
||||||
// we are going to mimic the browser and just pop a new browser window for public links
|
|
||||||
if (parsedURL.pathname.match(/^(\/api\/v[3-4]\/public)*\/files\//)) {
|
|
||||||
shell.openExternal(details.url);
|
|
||||||
return {action: 'deny'};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image proxy case
|
|
||||||
if (parsedURL.pathname.match(/^\/api\/v[3-4]\/image/)) {
|
|
||||||
shell.openExternal(details.url);
|
|
||||||
return {action: 'deny'};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedURL.pathname.match(/^\/help\//)) {
|
|
||||||
// Help links case
|
|
||||||
// continue to open special case internal urls in default browser
|
|
||||||
shell.openExternal(details.url);
|
|
||||||
return {action: 'deny'};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlUtils.isTeamUrl(server.url, parsedURL, true)) {
|
|
||||||
WindowManager.showMainWindow(parsedURL);
|
|
||||||
return {action: 'deny'};
|
|
||||||
}
|
|
||||||
if (urlUtils.isAdminUrl(server.url, parsedURL)) {
|
|
||||||
log.info(`${details.url} is an admin console page, preventing to open a new window`);
|
|
||||||
return {action: 'deny'};
|
|
||||||
}
|
|
||||||
if (popupWindow && popupWindow.webContents.getURL() === details.url) {
|
|
||||||
log.info(`Popup window already open at provided url: ${details.url}`);
|
|
||||||
return {action: 'deny'};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move popups to its own and have more than one.
|
|
||||||
if (urlUtils.isPluginUrl(server.url, parsedURL) || urlUtils.isManagedResource(server.url, parsedURL)) {
|
|
||||||
if (!popupWindow) {
|
|
||||||
popupWindow = new BrowserWindow({
|
|
||||||
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
|
||||||
//parent: WindowManager.getMainWindow(),
|
|
||||||
show: false,
|
|
||||||
center: true,
|
|
||||||
webPreferences: {
|
|
||||||
nativeWindowOpen: true,
|
|
||||||
spellcheck: (typeof spellcheck === 'undefined' ? true : spellcheck),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
popupWindow.webContents.on('new-window', denyNewWindow);
|
|
||||||
popupWindow.once('ready-to-show', () => {
|
|
||||||
popupWindow!.show();
|
|
||||||
});
|
|
||||||
popupWindow.once('closed', () => {
|
|
||||||
popupWindow = undefined;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (urlUtils.isManagedResource(server.url, parsedURL)) {
|
if (server && urlUtils.isCustomLoginURL(parsedURL, server, configServers)) {
|
||||||
popupWindow.loadURL(details.url);
|
return;
|
||||||
} else {
|
}
|
||||||
// currently changing the userAgent for popup windows to allow plugins to go through google's oAuth
|
if (parsedURL.protocol === 'mailto:') {
|
||||||
// should be removed once a proper oAuth2 implementation is setup.
|
return;
|
||||||
popupWindow.loadURL(details.url, {
|
}
|
||||||
userAgent: composeUserAgent(),
|
if (this.customLogins[contentID]?.inProgress) {
|
||||||
});
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextMenu = new ContextMenu({}, popupWindow);
|
log.info(`Prevented desktop from navigating to: ${url}`);
|
||||||
contextMenu.reload();
|
event.preventDefault();
|
||||||
}
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
generateDidStartNavigation = (getServersFunction: () => TeamWithTabs[]) => {
|
||||||
|
return (event: Event & {sender: WebContents}, url: string) => {
|
||||||
|
const serverList = getServersFunction();
|
||||||
|
const contentID = event.sender.id;
|
||||||
|
const parsedURL = urlUtils.parseURL(url)!;
|
||||||
|
const server = urlUtils.getView(parsedURL, serverList);
|
||||||
|
|
||||||
|
if (!urlUtils.isTrustedURL(parsedURL, serverList)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverURL = urlUtils.parseURL(server?.url || '');
|
||||||
|
|
||||||
|
if (server && urlUtils.isCustomLoginURL(parsedURL, server, serverList)) {
|
||||||
|
this.customLogins[contentID].inProgress = true;
|
||||||
|
} else if (server && this.customLogins[contentID].inProgress && urlUtils.isInternalURL(serverURL || new URL(''), parsedURL)) {
|
||||||
|
this.customLogins[contentID].inProgress = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
denyNewWindow = (details: Electron.HandlerDetails): {action: 'deny' | 'allow'} => {
|
||||||
|
log.warn(`Prevented popup window to open a new window to ${details.url}.`);
|
||||||
return {action: 'deny'};
|
return {action: 'deny'};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
export const removeWebContentsListeners = (id: number) => {
|
generateNewWindowListener = (getServersFunction: () => TeamWithTabs[], spellcheck?: boolean) => {
|
||||||
if (listeners[id]) {
|
return (details: Electron.HandlerDetails): {action: 'deny' | 'allow'} => {
|
||||||
listeners[id]();
|
const parsedURL = urlUtils.parseURL(details.url);
|
||||||
}
|
if (!parsedURL) {
|
||||||
};
|
log.warn(`Ignoring non-url ${details.url}`);
|
||||||
|
return {action: 'deny'};
|
||||||
|
}
|
||||||
|
|
||||||
export const addWebContentsEventListeners = (mmview: MattermostView, getServersFunction: () => TeamWithTabs[]) => {
|
const configServers = getServersFunction();
|
||||||
const contents = mmview.view.webContents;
|
|
||||||
|
|
||||||
// initialize custom login tracking
|
// Dev tools case
|
||||||
customLogins[contents.id] = {
|
if (parsedURL.protocol === 'devtools:') {
|
||||||
inProgress: false,
|
return {action: 'allow'};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid URL
|
||||||
|
if (!urlUtils.isValidURI(details.url)) {
|
||||||
|
return {action: 'deny'};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for custom protocol
|
||||||
|
if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:' && parsedURL.protocol !== `${scheme}:`) {
|
||||||
|
allowProtocolDialog.handleDialogEvent(parsedURL.protocol, details.url);
|
||||||
|
return {action: 'deny'};
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = urlUtils.getView(parsedURL, configServers);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
shell.openExternal(details.url);
|
||||||
|
return {action: 'deny'};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public download links case
|
||||||
|
// TODO: We might be handling different types differently in the future, for now
|
||||||
|
// we are going to mimic the browser and just pop a new browser window for public links
|
||||||
|
if (parsedURL.pathname.match(/^(\/api\/v[3-4]\/public)*\/files\//)) {
|
||||||
|
shell.openExternal(details.url);
|
||||||
|
return {action: 'deny'};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image proxy case
|
||||||
|
if (parsedURL.pathname.match(/^\/api\/v[3-4]\/image/)) {
|
||||||
|
shell.openExternal(details.url);
|
||||||
|
return {action: 'deny'};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedURL.pathname.match(/^\/help\//)) {
|
||||||
|
// Help links case
|
||||||
|
// continue to open special case internal urls in default browser
|
||||||
|
shell.openExternal(details.url);
|
||||||
|
return {action: 'deny'};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlUtils.isTeamUrl(server.url, parsedURL, true)) {
|
||||||
|
WindowManager.showMainWindow(parsedURL);
|
||||||
|
return {action: 'deny'};
|
||||||
|
}
|
||||||
|
if (urlUtils.isAdminUrl(server.url, parsedURL)) {
|
||||||
|
log.info(`${details.url} is an admin console page, preventing to open a new window`);
|
||||||
|
return {action: 'deny'};
|
||||||
|
}
|
||||||
|
if (this.popupWindow && this.popupWindow.webContents.getURL() === details.url) {
|
||||||
|
log.info(`Popup window already open at provided url: ${details.url}`);
|
||||||
|
return {action: 'deny'};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move popups to its own and have more than one.
|
||||||
|
if (urlUtils.isPluginUrl(server.url, parsedURL) || urlUtils.isManagedResource(server.url, parsedURL)) {
|
||||||
|
if (!this.popupWindow) {
|
||||||
|
this.popupWindow = new BrowserWindow({
|
||||||
|
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||||
|
//parent: WindowManager.getMainWindow(),
|
||||||
|
show: false,
|
||||||
|
center: true,
|
||||||
|
webPreferences: {
|
||||||
|
nativeWindowOpen: true,
|
||||||
|
spellcheck: (typeof spellcheck === 'undefined' ? true : spellcheck),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.popupWindow.webContents.setWindowOpenHandler(this.denyNewWindow);
|
||||||
|
this.popupWindow.once('ready-to-show', () => {
|
||||||
|
this.popupWindow!.show();
|
||||||
|
});
|
||||||
|
this.popupWindow.once('closed', () => {
|
||||||
|
this.popupWindow = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlUtils.isManagedResource(server.url, parsedURL)) {
|
||||||
|
this.popupWindow.loadURL(details.url);
|
||||||
|
} else {
|
||||||
|
// currently changing the userAgent for popup windows to allow plugins to go through google's oAuth
|
||||||
|
// should be removed once a proper oAuth2 implementation is setup.
|
||||||
|
this.popupWindow.loadURL(details.url, {
|
||||||
|
userAgent: composeUserAgent(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextMenu = new ContextMenu({}, this.popupWindow);
|
||||||
|
contextMenu.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {action: 'deny'};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if (listeners[contents.id]) {
|
removeWebContentsListeners = (id: number) => {
|
||||||
removeWebContentsListeners(contents.id);
|
if (this.listeners[id]) {
|
||||||
}
|
this.listeners[id]();
|
||||||
|
|
||||||
const willNavigate = generateWillNavigate(getServersFunction);
|
|
||||||
contents.on('will-navigate', willNavigate as (e: Event, u: string) => void); // Electron types don't include sender for some reason
|
|
||||||
|
|
||||||
// handle custom login requests (oath, saml):
|
|
||||||
// 1. are we navigating to a supported local custom login path from the `/login` page?
|
|
||||||
// - indicate custom login is in progress
|
|
||||||
// 2. are we finished with the custom login process?
|
|
||||||
// - indicate custom login is NOT in progress
|
|
||||||
const didStartNavigation = generateDidStartNavigation(getServersFunction);
|
|
||||||
contents.on('did-start-navigation', didStartNavigation as (e: Event, u: string) => void);
|
|
||||||
|
|
||||||
const spellcheck = mmview.options.webPreferences?.spellcheck;
|
|
||||||
const newWindow = generateNewWindowListener(getServersFunction, spellcheck);
|
|
||||||
contents.setWindowOpenHandler(newWindow);
|
|
||||||
|
|
||||||
contents.on('page-title-updated', mmview.handleTitleUpdate);
|
|
||||||
contents.on('page-favicon-updated', mmview.handleFaviconUpdate);
|
|
||||||
contents.on('update-target-url', mmview.handleUpdateTarget);
|
|
||||||
contents.on('did-navigate', mmview.handleDidNavigate);
|
|
||||||
|
|
||||||
const removeListeners = () => {
|
|
||||||
try {
|
|
||||||
contents.removeListener('will-navigate', willNavigate as (e: Event, u: string) => void);
|
|
||||||
contents.removeListener('did-start-navigation', didStartNavigation as (e: Event, u: string) => void);
|
|
||||||
contents.removeListener('page-title-updated', mmview.handleTitleUpdate);
|
|
||||||
contents.removeListener('page-favicon-updated', mmview.handleFaviconUpdate);
|
|
||||||
contents.removeListener('update-target-url', mmview.handleUpdateTarget);
|
|
||||||
contents.removeListener('did-navigate', mmview.handleDidNavigate);
|
|
||||||
} catch (e) {
|
|
||||||
log.error(`Error while trying to detach listeners, this might be ok if the process crashed: ${e}`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
listeners[contents.id] = removeListeners;
|
addWebContentsEventListeners = (mmview: MattermostView, getServersFunction: () => TeamWithTabs[]) => {
|
||||||
contents.once('render-process-gone', (event, details) => {
|
const contents = mmview.view.webContents;
|
||||||
if (details.reason !== 'clean-exit') {
|
|
||||||
log.error('Renderer process for a webcontent is no longer available:', details.reason);
|
// initialize custom login tracking
|
||||||
|
this.customLogins[contents.id] = {
|
||||||
|
inProgress: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.listeners[contents.id]) {
|
||||||
|
this.removeWebContentsListeners(contents.id);
|
||||||
}
|
}
|
||||||
removeListeners();
|
|
||||||
});
|
const willNavigate = this.generateWillNavigate(getServersFunction);
|
||||||
};
|
contents.on('will-navigate', willNavigate as (e: Event, u: string) => void); // TODO: Electron types don't include sender for some reason
|
||||||
|
|
||||||
|
// handle custom login requests (oath, saml):
|
||||||
|
// 1. are we navigating to a supported local custom login path from the `/login` page?
|
||||||
|
// - indicate custom login is in progress
|
||||||
|
// 2. are we finished with the custom login process?
|
||||||
|
// - indicate custom login is NOT in progress
|
||||||
|
const didStartNavigation = this.generateDidStartNavigation(getServersFunction);
|
||||||
|
contents.on('did-start-navigation', didStartNavigation as (e: Event, u: string) => void);
|
||||||
|
|
||||||
|
const spellcheck = mmview.options.webPreferences?.spellcheck;
|
||||||
|
const newWindow = this.generateNewWindowListener(getServersFunction, spellcheck);
|
||||||
|
contents.setWindowOpenHandler(newWindow);
|
||||||
|
|
||||||
|
contents.on('page-title-updated', mmview.handleTitleUpdate);
|
||||||
|
contents.on('page-favicon-updated', mmview.handleFaviconUpdate);
|
||||||
|
contents.on('update-target-url', mmview.handleUpdateTarget);
|
||||||
|
contents.on('did-navigate', mmview.handleDidNavigate);
|
||||||
|
|
||||||
|
const removeListeners = () => {
|
||||||
|
try {
|
||||||
|
contents.removeListener('will-navigate', willNavigate as (e: Event, u: string) => void);
|
||||||
|
contents.removeListener('did-start-navigation', didStartNavigation as (e: Event, u: string) => void);
|
||||||
|
contents.removeListener('page-title-updated', mmview.handleTitleUpdate);
|
||||||
|
contents.removeListener('page-favicon-updated', mmview.handleFaviconUpdate);
|
||||||
|
contents.removeListener('update-target-url', mmview.handleUpdateTarget);
|
||||||
|
contents.removeListener('did-navigate', mmview.handleDidNavigate);
|
||||||
|
} catch (e) {
|
||||||
|
log.error(`Error while trying to detach listeners, this might be ok if the process crashed: ${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.listeners[contents.id] = removeListeners;
|
||||||
|
contents.once('render-process-gone', (event, details) => {
|
||||||
|
if (details.reason !== 'clean-exit') {
|
||||||
|
log.error('Renderer process for a webcontent is no longer available:', details.reason);
|
||||||
|
}
|
||||||
|
removeListeners();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const webContentsEventManager = new WebContentsEventManager();
|
||||||
|
export default webContentsEventManager;
|
||||||
|
Reference in New Issue
Block a user