[MM-59823] Migrate BrowserView to WebContentsView (#3177)

* Migrate to WebContentsView from BrowserView

* A bit of cleanup, stop holding reference to the loading screen

* Fix tests

* Fix i18n

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Devin Binnie
2024-10-30 09:05:51 -04:00
committed by GitHub
parent f99c10a527
commit 5fccd0f837
36 changed files with 475 additions and 440 deletions

View File

@@ -43,7 +43,7 @@ describe('application', function desc() {
const browserWindow = await this.app.browserWindow(mainWindow);
const webContentsId = this.serverMap[`${config.teams[1].name}___TAB_MESSAGING`].webContentsId;
const isActive = await browserWindow.evaluate((window, id) => {
return window.getBrowserViews().find((view) => view.webContents.id === id).webContents.getURL();
return window.contentView.children.find((view) => view.webContents.id === id).webContents.getURL();
}, webContentsId);
isActive.should.equal('https://github.com/test/url/');
const dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton');

View File

@@ -54,17 +54,17 @@ describe('menu_bar/dropdown', function desc() {
after(afterFunc);
it('MM-T4406_1 should show the dropdown', async () => {
let dropdownHeight = await browserWindow.evaluate((window) => window.getBrowserViews().find((view) => view.webContents.getURL().includes('dropdown')).getBounds().height);
let dropdownHeight = await browserWindow.evaluate((window) => window.contentView.children.find((view) => view.webContents.getURL().includes('dropdown')).getBounds().height);
dropdownHeight.should.equal(0);
await mainWindow.click('.ServerDropdownButton');
dropdownHeight = await browserWindow.evaluate((window) => window.getBrowserViews().find((view) => view.webContents.getURL().includes('dropdown')).getBounds().height);
dropdownHeight = await browserWindow.evaluate((window) => window.contentView.children.find((view) => view.webContents.getURL().includes('dropdown')).getBounds().height);
dropdownHeight.should.be.greaterThan(0);
});
it('MM-T4406_2 should hide the dropdown', async () => {
await mainWindow.click('.TabBar');
const dropdownHeight = await browserWindow.evaluate((window) => window.getBrowserViews().find((view) => view.webContents.getURL().includes('dropdown')).getBounds().height);
const dropdownHeight = await browserWindow.evaluate((window) => window.contentView.children.find((view) => view.webContents.getURL().includes('dropdown')).getBounds().height);
dropdownHeight.should.equal(0);
});
});
@@ -100,9 +100,9 @@ describe('menu_bar/dropdown', function desc() {
after(afterFunc);
it('MM-T4408_1 should show the first view', async () => {
const firstViewIsAttached = await browserWindow.evaluate((window, url) => Boolean(window.getBrowserViews().find((view) => view.webContents.getURL() === url)), env.exampleURL);
const firstViewIsAttached = await browserWindow.evaluate((window, url) => Boolean(window.contentView.children.find((view) => view.webContents.getURL() === url)), env.exampleURL);
firstViewIsAttached.should.be.true;
const secondViewIsAttached = await browserWindow.evaluate((window) => Boolean(window.getBrowserViews().find((view) => view.webContents.getURL() === 'https://github.com/')));
const secondViewIsAttached = await browserWindow.evaluate((window) => Boolean(window.contentView.children.find((view) => view.webContents.getURL() === 'https://github.com/')));
secondViewIsAttached.should.be.false;
});
@@ -110,9 +110,9 @@ describe('menu_bar/dropdown', function desc() {
await mainWindow.click('.ServerDropdownButton');
await dropdownView.click('.ServerDropdown button.ServerDropdown__button:nth-child(2)');
const firstViewIsAttached = await browserWindow.evaluate((window, url) => Boolean(window.getBrowserViews().find((view) => view.webContents.getURL() === url)), env.exampleURL);
const firstViewIsAttached = await browserWindow.evaluate((window, url) => Boolean(window.contentView.children.find((view) => view.webContents.getURL() === url)), env.exampleURL);
firstViewIsAttached.should.be.false;
const secondViewIsAttached = await browserWindow.evaluate((window) => Boolean(window.getBrowserViews().find((view) => view.webContents.getURL() === 'https://github.com/')));
const secondViewIsAttached = await browserWindow.evaluate((window) => Boolean(window.contentView.children.find((view) => view.webContents.getURL() === 'https://github.com/')));
secondViewIsAttached.should.be.true;
});
});

View File

@@ -11,7 +11,7 @@ const {asyncSleep} = require('../../modules/utils');
async function setupPromise(window, id) {
const promise = new Promise((resolve) => {
const browserView = window.getBrowserViews().find((view) => view.webContents.id === id);
const browserView = window.contentView.children.find((view) => view.webContents.id === id);
browserView.webContents.on('did-finish-load', () => {
resolve();
});
@@ -22,13 +22,13 @@ async function setupPromise(window, id) {
function getZoomFactorOfServer(browserWindow, serverId) {
return browserWindow.evaluate(
(window, id) => window.getBrowserViews().find((view) => view.webContents.id === id).webContents.getZoomFactor(),
(window, id) => window.contentView.children.find((view) => view.webContents.id === id).webContents.getZoomFactor(),
serverId,
);
}
function setZoomFactorOfServer(browserWindow, serverId, zoomFactor) {
return browserWindow.evaluate(
(window, {id, zoom}) => window.getBrowserViews().find((view) => view.webContents.id === id).webContents.setZoomFactor(zoom),
(window, {id, zoom}) => window.contentView.children.find((view) => view.webContents.id === id).webContents.setZoomFactor(zoom),
{id: serverId, zoom: zoomFactor},
);
}
@@ -82,12 +82,12 @@ describe('menu/view', function desc() {
robot.keyTap('=', [env.cmdOrCtrl]);
await asyncSleep(1000);
let zoomLevel = await browserWindow.evaluate((window, id) => window.getBrowserViews().find((view) => view.webContents.id === id).webContents.getZoomFactor(), firstServerId);
let zoomLevel = await browserWindow.evaluate((window, id) => window.contentView.children.find((view) => view.webContents.id === id).webContents.getZoomFactor(), firstServerId);
zoomLevel.should.be.greaterThan(1);
robot.keyTap('0', [env.cmdOrCtrl]);
await asyncSleep(1000);
zoomLevel = await browserWindow.evaluate((window, id) => window.getBrowserViews().find((view) => view.webContents.id === id).webContents.getZoomFactor(), firstServerId);
zoomLevel = await browserWindow.evaluate((window, id) => window.contentView.children.find((view) => view.webContents.id === id).webContents.getZoomFactor(), firstServerId);
zoomLevel.should.be.equal(1);
});
@@ -104,7 +104,7 @@ describe('menu/view', function desc() {
robot.keyTap('=', [env.cmdOrCtrl]);
await asyncSleep(1000);
const zoomLevel = await browserWindow.evaluate((window, id) => window.getBrowserViews().find((view) => view.webContents.id === id).webContents.getZoomFactor(), firstServerId);
const zoomLevel = await browserWindow.evaluate((window, id) => window.contentView.children.find((view) => view.webContents.id === id).webContents.getZoomFactor(), firstServerId);
zoomLevel.should.be.greaterThan(1);
});
@@ -144,7 +144,7 @@ describe('menu/view', function desc() {
robot.keyTap('-', [env.cmdOrCtrl]);
await asyncSleep(1000);
const zoomLevel = await browserWindow.evaluate((window, id) => window.getBrowserViews().find((view) => view.webContents.id === id).webContents.getZoomFactor(), firstServerId);
const zoomLevel = await browserWindow.evaluate((window, id) => window.contentView.children.find((view) => view.webContents.id === id).webContents.getZoomFactor(), firstServerId);
zoomLevel.should.be.lessThan(1);
});

View File

@@ -159,9 +159,9 @@
"renderer.components.errorView.cannotConnectToAppName": "Cannot connect to {appName}",
"renderer.components.errorView.havingTroubleConnecting": "We're having trouble connecting to {appName}. We'll continue to try and establish a connection.",
"renderer.components.errorView.refreshThenVerify": "If refreshing this page (Ctrl+R or Command+R) does not work please verify that:",
"renderer.components.errorView.troubleshooting.browserView.canReachFromBrowserWindow": "You can reach <link>{url}</link> from a browser window.",
"renderer.components.errorView.troubleshooting.computerIsConnected": "Your computer is connected to the internet.",
"renderer.components.errorView.troubleshooting.urlIsCorrect.appNameIsCorrect": "The {appName} URL <link>{url}</link> is correct",
"renderer.components.errorView.troubleshooting.webContentsView.canReachFromBrowserWindow": "You can reach <link>{url}</link> from a browser window.",
"renderer.components.input.required": "This field is required",
"renderer.components.mainPage.contextMenu.ariaLabel": "Context menu",
"renderer.components.mainPage.titleBar": "{appName}",

229
package-lock.json generated
View File

@@ -16,7 +16,7 @@
"bootstrap": "4.6.1",
"bootstrap-dark": "1.0.3",
"classnames": "2.5.1",
"electron-context-menu": "3.6.1",
"electron-context-menu": "4.0.4",
"electron-extension-installer": "1.2.0",
"electron-is-dev": "2.0.0",
"electron-log": "5.2.0",
@@ -5281,6 +5281,8 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true,
"optional": true,
"engines": {
"node": ">=8"
}
@@ -6226,6 +6228,8 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
"dev": true,
"optional": true,
"dependencies": {
"slice-ansi": "^3.0.0",
"string-width": "^4.2.0"
@@ -7443,26 +7447,136 @@
"dev": true
},
"node_modules/electron-context-menu": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-3.6.1.tgz",
"integrity": "sha512-lcpO6tzzKUROeirhzBjdBWNqayEThmdW+2I2s6H6QMrwqTVyT3EK47jW3Nxm60KTxl5/bWfEoIruoUNn57/QkQ==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-4.0.4.tgz",
"integrity": "sha512-XPGj35npL8+MG9Lx5ukmK/h8KLmjYJ3e1GvwWKrNZvf2ocv746WXIyltoV1yWtkEPT7g2kQ8hFmu0ZupK5KieA==",
"dependencies": {
"cli-truncate": "^2.1.0",
"electron-dl": "^3.2.1",
"electron-is-dev": "^2.0.0"
"cli-truncate": "^4.0.0",
"electron-dl": "^4.0.0",
"electron-is-dev": "^3.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/electron-context-menu/node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/electron-context-menu/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/electron-context-menu/node_modules/cli-truncate": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
"integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
"dependencies": {
"slice-ansi": "^5.0.0",
"string-width": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/electron-context-menu/node_modules/electron-is-dev": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-3.0.1.tgz",
"integrity": "sha512-8TjjAh8Ec51hUi3o4TaU0mD3GMTOESi866oRNavj9A3IQJ7pmv+MJVmdZBFGw4GFT36X7bkqnuDNYvkQgvyI8Q==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/electron-context-menu/node_modules/is-fullwidth-code-point": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
"integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/electron-context-menu/node_modules/slice-ansi": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
"integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
"dependencies": {
"ansi-styles": "^6.0.0",
"is-fullwidth-code-point": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/electron-context-menu/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/electron-context-menu/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/electron-dl": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-3.5.0.tgz",
"integrity": "sha512-Oj+VSuScVx8hEKM2HEvTQswTX6G3MLh7UoAz/oZuvKyNDfudNi1zY6PK/UnFoK1nCl9DF6k+3PFwElKbtZlDig==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-4.0.0.tgz",
"integrity": "sha512-USiB9816d2JzKv0LiSbreRfTg5lDk3lWh0vlx/gugCO92ZIJkHVH0UM18EHvKeadErP6Xn4yiTphWzYfbA2Ong==",
"dependencies": {
"ext-name": "^5.0.0",
"pupa": "^2.0.1",
"unused-filename": "^2.1.0"
"pupa": "^3.1.0",
"unused-filename": "^4.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -7655,8 +7769,7 @@
"node_modules/emoji-regex": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
"dev": true
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
},
"node_modules/emojis-list": {
"version": "3.0.0",
@@ -7914,11 +8027,14 @@
}
},
"node_modules/escape-goat": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
"integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz",
"integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==",
"engines": {
"node": ">=8"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/escape-string-regexp": {
@@ -9288,6 +9404,17 @@
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
"integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@@ -13296,14 +13423,6 @@
"node": ">=12"
}
},
"node_modules/modify-filename": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz",
"integrity": "sha512-EickqnKq3kVVaZisYuCxhtKbZjInCuwgwZWyAmRIp1NTMhri7r3380/uqwrUHfaDiPzLVTuoNy4whX66bxPVog==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
@@ -14008,6 +14127,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -14535,14 +14655,17 @@
}
},
"node_modules/pupa": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz",
"integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz",
"integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==",
"dependencies": {
"escape-goat": "^2.0.0"
"escape-goat": "^4.0.0"
},
"engines": {
"node": ">=8"
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pure-rand": {
@@ -15644,6 +15767,8 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
"dev": true,
"optional": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
@@ -15657,6 +15782,8 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"optional": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -15671,6 +15798,8 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"optional": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -15681,7 +15810,9 @@
"node_modules/slice-ansi/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"optional": true
},
"node_modules/smart-buffer": {
"version": "4.2.0",
@@ -16806,15 +16937,37 @@
}
},
"node_modules/unused-filename": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-2.1.0.tgz",
"integrity": "sha512-BMiNwJbuWmqCpAM1FqxCTD7lXF97AvfQC8Kr/DIeA6VtvhJaMDupZ82+inbjl5yVP44PcxOuCSxye1QMS0wZyg==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-4.0.1.tgz",
"integrity": "sha512-ZX6U1J04K1FoSUeoX1OicAhw4d0aro2qo+L8RhJkiGTNtBNkd/Fi1Wxoc9HzcVu6HfOzm0si/N15JjxFmD1z6A==",
"dependencies": {
"modify-filename": "^1.1.0",
"path-exists": "^4.0.0"
"escape-string-regexp": "^5.0.0",
"path-exists": "^5.0.0"
},
"engines": {
"node": ">=8"
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/unused-filename/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/unused-filename/node_modules/path-exists": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
"integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/update-browserslist-db": {

View File

@@ -165,7 +165,7 @@
"bootstrap": "4.6.1",
"bootstrap-dark": "1.0.3",
"classnames": "2.5.1",
"electron-context-menu": "3.6.1",
"electron-context-menu": "4.0.4",
"electron-extension-installer": "1.2.0",
"electron-is-dev": "2.0.0",
"electron-log": "5.2.0",

View File

@@ -108,8 +108,6 @@ export const PING_DOMAIN = 'ping-domain';
export const GET_LANGUAGE_INFORMATION = 'get-language-information';
export const GET_AVAILABLE_LANGUAGES = 'get-available-languages';
export const VIEW_FINISHED_RESIZING = 'view-finished-resizing';
// Calls
export const GET_DESKTOP_SOURCES = 'get-desktop-sources';
export const DESKTOP_SOURCES_MODAL_REQUEST = 'desktop-sources-modal-request';

View File

@@ -36,7 +36,7 @@ export const DOWNLOADS_DROPDOWN_MENU_HEIGHT = 160;
export const DOWNLOADS_DROPDOWN_MENU_WIDTH = 154;
export const DOWNLOADS_DROPDOWN_MENU_PADDING = 12;
// In order to display the box-shadow & radius on the left + right, use this WIDTH in the browserView for downloadsDropdown
// In order to display the box-shadow & radius on the left + right, use this WIDTH in the webContentsView for downloadsDropdown
export const DOWNLOADS_DROPDOWN_FULL_WIDTH = DOWNLOADS_DROPDOWN_PADDING + DOWNLOADS_DROPDOWN_WIDTH + TAB_BAR_PADDING;
export const DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH = (DOWNLOADS_DROPDOWN_MENU_PADDING * 2) + DOWNLOADS_DROPDOWN_MENU_WIDTH;
export const DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT = DOWNLOADS_DROPDOWN_MENU_HEIGHT + TAB_BAR_PADDING; // only bottom padding included for better positioning

View File

@@ -162,7 +162,7 @@ describe('main/app/app', () => {
expect(CertificateStore.save).toHaveBeenCalled();
});
it('should load URL using MattermostBrowserView when trusting certificate', async () => {
it('should load URL using MattermostWebContentsView when trusting certificate', async () => {
dialog.showMessageBox.mockResolvedValue({response: 0});
await handleAppCertificateError(event, webContents, testURL, 'error-1', certificate, callback);
expect(callback).toHaveBeenCalledWith(true);

View File

@@ -10,7 +10,7 @@ jest.mock('electron-context-menu', () => {
describe('main/contextMenu', () => {
describe('shouldShowMenu', () => {
const contextMenu = new ContextMenu();
const contextMenu = new ContextMenu({}, {webContents: {}});
it('should not show menu on internal link', () => {
expect(contextMenu.menuOptions.shouldShowMenu(null, {
@@ -73,7 +73,7 @@ describe('main/contextMenu', () => {
describe('reload', () => {
it('should call dispose on reload', () => {
const contextMenu = new ContextMenu();
const contextMenu = new ContextMenu({}, {webContents: {}});
const fn = contextMenu.menuDispose;
contextMenu.reload();
expect(fn).toHaveBeenCalled();

View File

@@ -2,7 +2,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {BrowserView, BrowserWindow, ContextMenuParams, Event} from 'electron';
import type {WebContentsView, BrowserWindow, ContextMenuParams, Event} from 'electron';
import type {Options} from 'electron-context-menu';
import electronContextMenu from 'electron-context-menu';
@@ -29,11 +29,11 @@ const defaultMenuOptions = {
};
export default class ContextMenu {
view: BrowserWindow | BrowserView;
view: BrowserWindow | WebContentsView;
menuOptions: Options;
menuDispose?: () => void;
constructor(options: Options, view: BrowserWindow | BrowserView) {
constructor(options: Options, view: BrowserWindow | WebContentsView) {
const providedOptions: Options = options || {};
this.menuOptions = Object.assign({}, defaultMenuOptions, providedOptions);
@@ -52,7 +52,7 @@ export default class ContextMenu {
reload = () => {
this.dispose();
const options = {window: this.view, ...this.menuOptions};
const options = {window: this.view.webContents, ...this.menuOptions};
this.menuDispose = electronContextMenu(options);
};
}

View File

@@ -91,7 +91,8 @@ describe('main/diagnostics/utils', () => {
isDestroyed: () => false,
isVisible: () => true,
isEnabled: () => true,
getBrowserViews: () => [{
contentView: {
children: [{
getBounds: () => ({
x: 0,
y: 0,
@@ -99,6 +100,7 @@ describe('main/diagnostics/utils', () => {
height: 500,
}),
}],
},
};
it('should return true if window ok', () => {
expect(browserWindowVisibilityStatus('testWindow', bWindow).every((check) => check.ok)).toBe(true);
@@ -118,10 +120,11 @@ describe('main/diagnostics/utils', () => {
it('should return false if window is not enabled', () => {
expect(browserWindowVisibilityStatus('testWindow', {...bWindow, isEnabled: () => false}).every((check) => check.ok)).toBe(false);
});
it('should return false if a child browserView has invalid bounds', () => {
it('should return false if a child webContentsView has invalid bounds', () => {
expect(browserWindowVisibilityStatus('testWindow', {
...bWindow,
getBrowserViews: () => [{
contentView: {
children: [{
getBounds: () => ({
x: -1,
y: -4000,
@@ -129,6 +132,7 @@ describe('main/diagnostics/utils', () => {
height: 500,
}),
}],
},
}).every((check) => check.ok)).toBe(false);
});
});

View File

@@ -109,7 +109,7 @@ export function browserWindowVisibilityStatus(name: string, bWindow?: BrowserWin
const destroyed = bWindow.isDestroyed();
const visible = bWindow.isVisible();
const enabled = bWindow.isEnabled();
const browserViewsBounds = bWindow.getBrowserViews()?.map((view) => view.getBounds());
const webContentsViewsBounds = bWindow.contentView.children.map((view) => view.getBounds());
status.push({
name: 'windowExists',
@@ -141,9 +141,9 @@ export function browserWindowVisibilityStatus(name: string, bWindow?: BrowserWin
ok: enabled,
});
status.push({
name: 'browserViewsBounds',
ok: browserViewsBounds.every((bounds) => boundsOk(bounds)),
data: browserViewsBounds,
name: 'webContentsViewsBounds',
ok: webContentsViewsBounds.every((bounds) => boundsOk(bounds)),
data: webContentsViewsBounds,
});
return status;

View File

@@ -30,7 +30,7 @@ jest.mock('electron', () => {
getAppPath: jest.fn(),
getPath: jest.fn(() => '/valid/downloads/path'),
},
BrowserView: jest.fn().mockImplementation(() => ({
WebContentsView: jest.fn().mockImplementation(() => ({
webContents: {
loadURL: jest.fn(),
focus: jest.fn(),

View File

@@ -13,7 +13,6 @@ import {
USER_ACTIVITY_UPDATE,
BROWSER_HISTORY_PUSH,
GET_VIEW_INFO_FOR_TEST,
VIEW_FINISHED_RESIZING,
CALLS_JOIN_CALL,
CALLS_JOINED_CALL,
CALLS_LEAVE_CALL,
@@ -140,12 +139,6 @@ if (process.env.NODE_ENV === 'test') {
****************************************************************************
*/
// Let the main process know when the window has finished resizing
// This is to reduce the amount of white box that happens when expand the BrowserView
window.addEventListener('resize', () => {
ipcRenderer.send(VIEW_FINISHED_RESIZING);
});
// Enable secure input on macOS clients when the user is on a password input
let isPasswordBox = false;
const shouldSecureInput = (element: {tagName?: string; type?: string} | null, force = false) => {

View File

@@ -89,7 +89,6 @@ import {
OPEN_WINDOWS_CAMERA_PREFERENCES,
OPEN_WINDOWS_MICROPHONE_PREFERENCES,
GET_MEDIA_ACCESS_STATUS,
VIEW_FINISHED_RESIZING,
GET_NONCE,
IS_DEVELOPER_MODE_ENABLED,
METRICS_REQUEST,
@@ -177,7 +176,6 @@ contextBridge.exposeInMainWorld('desktop', {
openWindowsCameraPreferences: () => ipcRenderer.send(OPEN_WINDOWS_CAMERA_PREFERENCES),
openWindowsMicrophonePreferences: () => ipcRenderer.send(OPEN_WINDOWS_MICROPHONE_PREFERENCES),
getMediaAccessStatus: (mediaType) => ipcRenderer.invoke(GET_MEDIA_ACCESS_STATUS, mediaType),
viewFinishedResizing: () => ipcRenderer.send(VIEW_FINISHED_RESIZING),
downloadsDropdown: {
toggleDownloadsDropdownMenu: (payload) => ipcRenderer.send(TOGGLE_DOWNLOADS_DROPDOWN_MENU, payload),

View File

@@ -8,7 +8,7 @@ import {LOAD_FAILED, UPDATE_TARGET_URL} from 'common/communication';
import {MattermostServer} from 'common/servers/MattermostServer';
import MessagingView from 'common/views/MessagingView';
import {MattermostBrowserView} from './MattermostBrowserView';
import {MattermostWebContentsView} from './MattermostWebContentsView';
import ContextMenu from '../contextMenu';
import MainWindow from '../windows/mainWindow';
@@ -18,7 +18,7 @@ jest.mock('electron', () => ({
getVersion: () => '5.0.0',
getPath: jest.fn(() => '/valid/downloads/path'),
},
BrowserView: jest.fn().mockImplementation(() => ({
WebContentsView: jest.fn().mockImplementation(() => ({
webContents: {
loadURL: jest.fn(),
on: jest.fn(),
@@ -72,10 +72,10 @@ jest.mock('main/performanceMonitor', () => ({
const server = new MattermostServer({name: 'server_name', url: 'http://server-1.com'});
const view = new MessagingView(server, true);
describe('main/views/MattermostBrowserView', () => {
describe('main/views/MattermostWebContentsView', () => {
describe('load', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostBrowserView(view, {}, {});
const mattermostView = new MattermostWebContentsView(view, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
@@ -85,38 +85,38 @@ describe('main/views/MattermostBrowserView', () => {
it('should load provided URL when provided', async () => {
const promise = Promise.resolve();
mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.webContentsView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load('http://server-2.com');
await promise;
expect(mattermostView.browserView.webContents.loadURL).toBeCalledWith('http://server-2.com/', expect.any(Object));
expect(mattermostView.webContentsView.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.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.webContentsView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load();
await promise;
expect(mattermostView.browserView.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
expect(mattermostView.webContentsView.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.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.webContentsView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load('a-bad<url');
await promise;
expect(mattermostView.browserView.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
expect(mattermostView.webContentsView.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.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.webContentsView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load('a-bad<url');
await expect(promise).rejects.toThrow(error);
expect(mattermostView.browserView.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
expect(mattermostView.webContentsView.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
expect(mattermostView.loadRetry).toBeCalledWith('http://server-1.com/', error);
});
@@ -124,23 +124,23 @@ describe('main/views/MattermostBrowserView', () => {
const error = new Error('test');
error.code = 'ERR_CERT_ERROR';
const promise = Promise.reject(error);
mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.webContentsView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load('a-bad<url');
await expect(promise).rejects.toThrow(error);
expect(mattermostView.browserView.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
expect(mattermostView.webContentsView.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
expect(mattermostView.loadRetry).not.toBeCalled();
});
});
describe('retry', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostBrowserView(view, {}, {});
const mattermostView = new MattermostWebContentsView(view, {}, {});
const retryInBackgroundFn = jest.fn();
beforeEach(() => {
jest.useFakeTimers();
MainWindow.get.mockReturnValue(window);
mattermostView.browserView.webContents.loadURL.mockImplementation(() => Promise.resolve());
mattermostView.webContentsView.webContents.loadURL.mockImplementation(() => Promise.resolve());
mattermostView.loadSuccess = jest.fn();
mattermostView.loadRetry = jest.fn();
mattermostView.emit = jest.fn();
@@ -154,16 +154,16 @@ describe('main/views/MattermostBrowserView', () => {
});
it('should do nothing when webcontents are destroyed', () => {
const webContents = mattermostView.browserView.webContents;
mattermostView.browserView.webContents = null;
const webContents = mattermostView.webContentsView.webContents;
mattermostView.webContentsView.webContents = null;
mattermostView.retry('http://server-1.com')();
expect(mattermostView.loadSuccess).not.toBeCalled();
mattermostView.browserView.webContents = webContents;
mattermostView.webContentsView.webContents = webContents;
});
it('should call loadSuccess on successful load', async () => {
const promise = Promise.resolve();
mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.webContentsView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.retry('http://server-1.com')();
await promise;
expect(mattermostView.loadSuccess).toBeCalledWith('http://server-1.com');
@@ -173,10 +173,10 @@ describe('main/views/MattermostBrowserView', () => {
mattermostView.maxRetries = 10;
const error = new Error('test');
const promise = Promise.reject(error);
mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.webContentsView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.retry('http://server-1.com')();
await expect(promise).rejects.toThrow(error);
expect(mattermostView.browserView.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object));
expect(mattermostView.webContentsView.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object));
expect(mattermostView.loadRetry).toBeCalledWith('http://server-1.com', error);
});
@@ -184,10 +184,10 @@ describe('main/views/MattermostBrowserView', () => {
mattermostView.maxRetries = 0;
const error = new Error('test');
const promise = Promise.reject(error);
mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.webContentsView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.retry('http://server-1.com')();
await expect(promise).rejects.toThrow(error);
expect(mattermostView.browserView.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object));
expect(mattermostView.webContentsView.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object));
expect(mattermostView.loadRetry).not.toBeCalled();
expect(MainWindow.sendToRenderer).toBeCalledWith(LOAD_FAILED, mattermostView.view.id, expect.any(String), expect.any(String));
expect(mattermostView.status).toBe(-1);
@@ -198,7 +198,7 @@ describe('main/views/MattermostBrowserView', () => {
describe('goToOffset', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostBrowserView(view, {}, {});
const mattermostView = new MattermostWebContentsView(view, {}, {});
mattermostView.reload = jest.fn();
afterEach(() => {
@@ -207,18 +207,18 @@ describe('main/views/MattermostBrowserView', () => {
});
it('should only go to offset if it can', () => {
mattermostView.browserView.webContents.navigationHistory.canGoToOffset.mockReturnValue(false);
mattermostView.webContentsView.webContents.navigationHistory.canGoToOffset.mockReturnValue(false);
mattermostView.goToOffset(1);
expect(mattermostView.browserView.webContents.navigationHistory.goToOffset).not.toBeCalled();
expect(mattermostView.webContentsView.webContents.navigationHistory.goToOffset).not.toBeCalled();
mattermostView.browserView.webContents.navigationHistory.canGoToOffset.mockReturnValue(true);
mattermostView.webContentsView.webContents.navigationHistory.canGoToOffset.mockReturnValue(true);
mattermostView.goToOffset(1);
expect(mattermostView.browserView.webContents.navigationHistory.goToOffset).toBeCalled();
expect(mattermostView.webContentsView.webContents.navigationHistory.goToOffset).toBeCalled();
});
it('should call reload if an error occurs', () => {
mattermostView.browserView.webContents.navigationHistory.canGoToOffset.mockReturnValue(true);
mattermostView.browserView.webContents.navigationHistory.goToOffset.mockImplementation(() => {
mattermostView.webContentsView.webContents.navigationHistory.canGoToOffset.mockReturnValue(true);
mattermostView.webContentsView.webContents.navigationHistory.goToOffset.mockImplementation(() => {
throw new Error('hi');
});
mattermostView.goToOffset(1);
@@ -228,8 +228,8 @@ describe('main/views/MattermostBrowserView', () => {
describe('onLogin', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostBrowserView(view, {}, {});
mattermostView.browserView.webContents.getURL = jest.fn();
const mattermostView = new MattermostWebContentsView(view, {}, {});
mattermostView.webContentsView.webContents.getURL = jest.fn();
mattermostView.reload = jest.fn();
afterEach(() => {
@@ -238,19 +238,19 @@ describe('main/views/MattermostBrowserView', () => {
});
it('should reload view when URL is not on subpath of original server URL', () => {
mattermostView.browserView.webContents.getURL.mockReturnValue('http://server-2.com/subpath');
mattermostView.webContentsView.webContents.getURL.mockReturnValue('http://server-2.com/subpath');
mattermostView.onLogin(true);
expect(mattermostView.reload).toHaveBeenCalled();
});
it('should not reload if URLs are matching', () => {
mattermostView.browserView.webContents.getURL.mockReturnValue('http://server-1.com');
mattermostView.webContentsView.webContents.getURL.mockReturnValue('http://server-1.com');
mattermostView.onLogin(true);
expect(mattermostView.reload).not.toHaveBeenCalled();
});
it('should not reload if URL is subpath of server URL', () => {
mattermostView.browserView.webContents.getURL.mockReturnValue('http://server-1.com/subpath');
mattermostView.webContentsView.webContents.getURL.mockReturnValue('http://server-1.com/subpath');
mattermostView.onLogin(true);
expect(mattermostView.reload).not.toHaveBeenCalled();
});
@@ -258,7 +258,7 @@ describe('main/views/MattermostBrowserView', () => {
describe('loadSuccess', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostBrowserView(view, {}, {});
const mattermostView = new MattermostWebContentsView(view, {}, {});
beforeEach(() => {
jest.useFakeTimers();
@@ -285,8 +285,14 @@ describe('main/views/MattermostBrowserView', () => {
});
describe('show', () => {
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn(), on: jest.fn(), setTopBrowserView: jest.fn()};
const mattermostView = new MattermostBrowserView(view, {}, {});
const window = {
contentView: {
addChildView: jest.fn(),
removeChildView: jest.fn(),
},
on: jest.fn(),
};
const mattermostView = new MattermostWebContentsView(view, {}, {});
beforeEach(() => {
jest.useFakeTimers();
@@ -304,7 +310,7 @@ describe('main/views/MattermostBrowserView', () => {
it('should add browser view to window and set bounds when request is true and view not currently visible', () => {
mattermostView.isVisible = false;
mattermostView.show();
expect(window.addBrowserView).toBeCalledWith(mattermostView.browserView);
expect(window.contentView.addChildView).toBeCalledWith(mattermostView.webContentsView);
expect(mattermostView.setBounds).toBeCalled();
expect(mattermostView.isVisible).toBe(true);
});
@@ -312,7 +318,7 @@ describe('main/views/MattermostBrowserView', () => {
it('should do nothing when not toggling', () => {
mattermostView.isVisible = true;
mattermostView.show();
expect(window.addBrowserView).not.toBeCalled();
expect(window.contentView.addChildView).not.toBeCalled();
});
it('should focus view if view is ready', () => {
@@ -324,8 +330,14 @@ describe('main/views/MattermostBrowserView', () => {
});
describe('hide', () => {
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn(), on: jest.fn(), setTopBrowserView: jest.fn()};
const mattermostView = new MattermostBrowserView(view, {}, {});
const window = {
contentView: {
addChildView: jest.fn(),
removeChildView: jest.fn(),
},
on: jest.fn(),
};
const mattermostView = new MattermostWebContentsView(view, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
@@ -334,20 +346,20 @@ describe('main/views/MattermostBrowserView', () => {
it('should remove browser view', () => {
mattermostView.isVisible = true;
mattermostView.hide();
expect(window.removeBrowserView).toBeCalledWith(mattermostView.browserView);
expect(window.contentView.removeChildView).toBeCalledWith(mattermostView.webContentsView);
expect(mattermostView.isVisible).toBe(false);
});
it('should do nothing when not toggling', () => {
mattermostView.isVisible = false;
mattermostView.hide();
expect(window.removeBrowserView).not.toBeCalled();
expect(window.contentView.removeChildView).not.toBeCalled();
});
});
describe('updateHistoryButton', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostBrowserView(view, {}, {});
const mattermostView = new MattermostWebContentsView(view, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
@@ -356,13 +368,13 @@ describe('main/views/MattermostBrowserView', () => {
it('should erase history and set isAtRoot when navigating to root URL', () => {
mattermostView.atRoot = false;
mattermostView.updateHistoryButton();
expect(mattermostView.browserView.webContents.navigationHistory.clear).toHaveBeenCalled();
expect(mattermostView.webContentsView.webContents.navigationHistory.clear).toHaveBeenCalled();
expect(mattermostView.isAtRoot).toBe(true);
});
});
describe('destroy', () => {
const window = {removeBrowserView: jest.fn(), on: jest.fn()};
const window = {contentView: {removeChildView: jest.fn()}, on: jest.fn()};
const contextMenu = {
dispose: jest.fn(),
};
@@ -373,22 +385,22 @@ describe('main/views/MattermostBrowserView', () => {
});
it('should remove browser view from window', () => {
const mattermostView = new MattermostBrowserView(view, {}, {});
mattermostView.browserView.webContents.close = jest.fn();
const mattermostView = new MattermostWebContentsView(view, {}, {});
mattermostView.webContentsView.webContents.close = jest.fn();
mattermostView.destroy();
expect(window.removeBrowserView).toBeCalledWith(mattermostView.browserView);
expect(window.contentView.removeChildView).toBeCalledWith(mattermostView.webContentsView);
});
it('should clear mentions', () => {
const mattermostView = new MattermostBrowserView(view, {}, {});
mattermostView.browserView.webContents.close = jest.fn();
const mattermostView = new MattermostWebContentsView(view, {}, {});
mattermostView.webContentsView.webContents.close = jest.fn();
mattermostView.destroy();
expect(AppState.clear).toBeCalledWith(mattermostView.view.id);
});
it('should clear outstanding timeouts', () => {
const mattermostView = new MattermostBrowserView(view, {}, {});
mattermostView.browserView.webContents.close = jest.fn();
const mattermostView = new MattermostWebContentsView(view, {}, {});
mattermostView.webContentsView.webContents.close = jest.fn();
const spy = jest.spyOn(global, 'clearTimeout');
mattermostView.retryLoad = 999;
mattermostView.removeLoading = 1000;
@@ -399,7 +411,7 @@ describe('main/views/MattermostBrowserView', () => {
describe('handleInputEvents', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostBrowserView(view, {}, {});
const mattermostView = new MattermostWebContentsView(view, {}, {});
it('should open three dot menu on pressing Alt', () => {
MainWindow.get.mockReturnValue(window);
@@ -424,7 +436,7 @@ describe('main/views/MattermostBrowserView', () => {
describe('handleUpdateTarget', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostBrowserView(view, {}, {});
const mattermostView = new MattermostWebContentsView(view, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);

View File

@@ -1,8 +1,8 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BrowserView, app, ipcMain} from 'electron';
import type {BrowserViewConstructorOptions, Event, Input} from 'electron/main';
import {WebContentsView, app, ipcMain} from 'electron';
import type {WebContentsViewConstructorOptions, Event, Input} from 'electron/main';
import {EventEmitter} from 'events';
import AppState from 'common/appState';
@@ -38,16 +38,15 @@ enum Status {
WAITING_MM,
ERROR = -1,
}
export class MattermostBrowserView extends EventEmitter {
export class MattermostWebContentsView extends EventEmitter {
view: MattermostView;
isVisible: boolean;
private log: Logger;
private browserView: BrowserView;
private webContentsView: WebContentsView;
private loggedIn: boolean;
private atRoot: boolean;
private options: BrowserViewConstructorOptions;
private options: WebContentsViewConstructorOptions;
private removeLoading?: NodeJS.Timeout;
private contextMenu?: ContextMenu;
private status?: Status;
@@ -55,7 +54,7 @@ export class MattermostBrowserView extends EventEmitter {
private maxRetries: number;
private altPressStatus: boolean;
constructor(view: MattermostView, options: BrowserViewConstructorOptions) {
constructor(view: MattermostView, options: WebContentsViewConstructorOptions) {
super();
this.view = view;
@@ -72,29 +71,28 @@ export class MattermostBrowserView extends EventEmitter {
this.isVisible = false;
this.loggedIn = false;
this.atRoot = true;
this.browserView = new BrowserView(this.options);
this.webContentsView = new WebContentsView(this.options);
this.resetLoadingStatus();
this.log = ServerManager.getViewLog(this.id, 'MattermostBrowserView');
this.log = ServerManager.getViewLog(this.id, 'MattermostWebContentsView');
this.log.verbose('View created');
this.browserView.webContents.on('update-target-url', this.handleUpdateTarget);
this.webContentsView.webContents.on('update-target-url', this.handleUpdateTarget);
if (process.platform !== 'darwin') {
this.browserView.webContents.on('before-input-event', this.handleInputEvents);
this.webContentsView.webContents.on('before-input-event', this.handleInputEvents);
}
this.browserView.webContents.on('input-event', (_, inputEvent) => {
this.webContentsView.webContents.on('input-event', (_, inputEvent) => {
if (inputEvent.type === 'mouseDown') {
ipcMain.emit(CLOSE_SERVERS_DROPDOWN);
ipcMain.emit(CLOSE_DOWNLOADS_DROPDOWN);
}
});
WebContentsEventManager.addWebContentsEventListeners(this.browserView.webContents);
WebContentsEventManager.addWebContentsEventListeners(this.webContentsView.webContents);
if (!DeveloperMode.get('disableContextMenu')) {
this.contextMenu = new ContextMenu({}, this.browserView);
this.contextMenu = new ContextMenu({}, this.webContentsView);
}
this.maxRetries = MAX_SERVER_RETRIES;
this.altPressStatus = false;
@@ -116,10 +114,10 @@ export class MattermostBrowserView extends EventEmitter {
return this.loggedIn;
}
get currentURL() {
return parseURL(this.browserView.webContents.getURL());
return parseURL(this.webContentsView.webContents.getURL());
}
get webContentsId() {
return this.browserView.webContents.id;
return this.webContentsView.webContents.id;
}
onLogin = (loggedIn: boolean) => {
@@ -139,9 +137,9 @@ export class MattermostBrowserView extends EventEmitter {
};
goToOffset = (offset: number) => {
if (this.browserView.webContents.navigationHistory.canGoToOffset(offset)) {
if (this.webContentsView.webContents.navigationHistory.canGoToOffset(offset)) {
try {
this.browserView.webContents.navigationHistory.goToOffset(offset);
this.webContentsView.webContents.navigationHistory.goToOffset(offset);
this.updateHistoryButton();
} catch (error) {
this.log.error(error);
@@ -152,25 +150,25 @@ export class MattermostBrowserView extends EventEmitter {
getBrowserHistoryStatus = () => {
if (this.currentURL?.toString() === this.view.url.toString()) {
this.browserView.webContents.navigationHistory.clear();
this.webContentsView.webContents.navigationHistory.clear();
this.atRoot = true;
} else {
this.atRoot = false;
}
return {
canGoBack: this.browserView.webContents.navigationHistory.canGoBack(),
canGoForward: this.browserView.webContents.navigationHistory.canGoForward(),
canGoBack: this.webContentsView.webContents.navigationHistory.canGoBack(),
canGoForward: this.webContentsView.webContents.navigationHistory.canGoForward(),
};
};
updateHistoryButton = () => {
const {canGoBack, canGoForward} = this.getBrowserHistoryStatus();
this.browserView.webContents.send(BROWSER_HISTORY_STATUS_UPDATED, canGoBack, canGoForward);
this.webContentsView.webContents.send(BROWSER_HISTORY_STATUS_UPDATED, canGoBack, canGoForward);
};
load = (someURL?: URL | string) => {
if (!this.browserView) {
if (!this.webContentsView) {
return;
}
@@ -188,11 +186,11 @@ export class MattermostBrowserView extends EventEmitter {
}
this.log.verbose(`Loading ${loadURL}`);
if (this.view.type === TAB_MESSAGING) {
performanceMonitor.registerServerView(`Server ${this.browserView.webContents.id}`, this.browserView.webContents, this.view.server.id);
performanceMonitor.registerServerView(`Server ${this.webContentsView.webContents.id}`, this.webContentsView.webContents, this.view.server.id);
} else {
performanceMonitor.registerView(`Server ${this.browserView.webContents.id}`, this.browserView.webContents, this.view.server.id);
performanceMonitor.registerView(`Server ${this.webContentsView.webContents.id}`, this.webContentsView.webContents, this.view.server.id);
}
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent(DeveloperMode.get('browserOnly'))});
const loading = this.webContentsView.webContents.loadURL(loadURL, {userAgent: composeUserAgent(DeveloperMode.get('browserOnly'))});
loading.then(this.loadSuccess(loadURL)).catch((err) => {
if (err.code && err.code.startsWith('ERR_CERT')) {
MainWindow.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString());
@@ -221,8 +219,7 @@ export class MattermostBrowserView extends EventEmitter {
return;
}
this.isVisible = true;
mainWindow.addBrowserView(this.browserView);
mainWindow.setTopBrowserView(this.browserView);
mainWindow.contentView.addChildView(this.webContentsView);
this.setBounds(getWindowBoundaries(mainWindow));
if (this.status === Status.READY) {
this.focus();
@@ -232,7 +229,7 @@ export class MattermostBrowserView extends EventEmitter {
hide = () => {
if (this.isVisible) {
this.isVisible = false;
MainWindow.get()?.removeBrowserView(this.browserView);
MainWindow.get()?.contentView.removeChildView(this.webContentsView);
}
};
@@ -243,23 +240,23 @@ export class MattermostBrowserView extends EventEmitter {
};
getBounds = () => {
return this.browserView.getBounds();
return this.webContentsView.getBounds();
};
openFind = () => {
this.browserView.webContents.sendInputEvent({type: 'keyDown', keyCode: 'F', modifiers: [process.platform === 'darwin' ? 'cmd' : 'ctrl', 'shift']});
this.webContentsView.webContents.sendInputEvent({type: 'keyDown', keyCode: 'F', modifiers: [process.platform === 'darwin' ? 'cmd' : 'ctrl', 'shift']});
};
setBounds = (boundaries: Electron.Rectangle) => {
this.browserView.setBounds(boundaries);
this.webContentsView.setBounds(boundaries);
};
destroy = () => {
WebContentsEventManager.removeWebContentsListeners(this.webContentsId);
AppState.clear(this.id);
MainWindow.get()?.removeBrowserView(this.browserView);
performanceMonitor.unregisterView(this.browserView.webContents.id);
this.browserView.webContents.close();
performanceMonitor.unregisterView(this.webContentsView.webContents.id);
MainWindow.get()?.contentView.removeChildView(this.webContentsView);
this.webContentsView.webContents.close();
this.isVisible = false;
if (this.retryLoad) {
@@ -311,17 +308,17 @@ export class MattermostBrowserView extends EventEmitter {
// So what we do here is check to see if it's opened correctly and if not we reset it
if (process.platform === 'darwin') {
const timeout = setTimeout(() => {
if (this.browserView.webContents.isDevToolsOpened()) {
this.browserView.webContents.closeDevTools();
this.browserView.webContents.openDevTools({mode: 'detach'});
if (this.webContentsView.webContents.isDevToolsOpened()) {
this.webContentsView.webContents.closeDevTools();
this.webContentsView.webContents.openDevTools({mode: 'detach'});
}
}, 500);
this.browserView.webContents.on('devtools-opened', () => {
this.webContentsView.webContents.on('devtools-opened', () => {
clearTimeout(timeout);
});
}
this.browserView.webContents.openDevTools({mode: 'detach'});
this.webContentsView.webContents.openDevTools({mode: 'detach'});
};
/**
@@ -329,16 +326,16 @@ export class MattermostBrowserView extends EventEmitter {
*/
sendToRenderer = (channel: string, ...args: any[]) => {
this.browserView.webContents.send(channel, ...args);
this.webContentsView.webContents.send(channel, ...args);
};
isDestroyed = () => {
return this.browserView.webContents.isDestroyed();
return this.webContentsView.webContents.isDestroyed();
};
focus = () => {
if (this.browserView.webContents) {
this.browserView.webContents.focus();
if (this.webContentsView.webContents) {
this.webContentsView.webContents.focus();
} else {
this.log.warn('trying to focus the browserview, but it doesn\'t yet have webcontents.');
}
@@ -381,10 +378,10 @@ export class MattermostBrowserView extends EventEmitter {
private retry = (loadURL: string) => {
return () => {
// window was closed while retrying
if (!this.browserView || !this.browserView.webContents) {
if (!this.webContentsView || !this.webContentsView.webContents) {
return;
}
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent(DeveloperMode.get('browserOnly'))});
const loading = this.webContentsView.webContents.loadURL(loadURL, {userAgent: composeUserAgent(DeveloperMode.get('browserOnly'))});
loading.then(this.loadSuccess(loadURL)).catch((err) => {
if (this.maxRetries-- > 0) {
this.loadRetry(loadURL, err);
@@ -402,7 +399,7 @@ export class MattermostBrowserView extends EventEmitter {
private retryInBackground = (loadURL: string) => {
return () => {
// window was closed while retrying
if (!this.browserView || !this.browserView.webContents) {
if (!this.webContentsView || !this.webContentsView.webContents) {
return;
}
const parsedURL = parseURL(loadURL);

View File

@@ -30,7 +30,8 @@ jest.mock('electron', () => {
getAppPath: () => '',
getPath: jest.fn(() => '/valid/downloads/path'),
},
BrowserView: jest.fn().mockImplementation(() => ({
WebContentsView: jest.fn().mockImplementation(() => ({
setBackgroundColor: jest.fn(),
webContents: {
loadURL: jest.fn(),
focus: jest.fn(),
@@ -69,7 +70,7 @@ jest.mock('fs', () => ({
describe('main/views/DownloadsDropdownMenuView', () => {
beforeEach(() => {
MainWindow.get.mockReturnValue({addBrowserView: jest.fn(), setTopBrowserView: jest.fn()});
MainWindow.get.mockReturnValue({contentView: {addChildView: jest.fn()}});
MainWindow.getBounds.mockReturnValue({width: 800, height: 600, x: 0, y: 0});
getDarwinDoNotDisturb.mockReturnValue(false);
});

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {IpcMainEvent} from 'electron';
import {BrowserView, ipcMain} from 'electron';
import {WebContentsView, ipcMain} from 'electron';
import {
CLOSE_DOWNLOADS_DROPDOWN_MENU,
@@ -37,7 +37,7 @@ const log = new Logger('DownloadsDropdownMenuView');
export class DownloadsDropdownMenuView {
private open: boolean;
private view?: BrowserView;
private view?: WebContentsView;
private bounds?: Electron.Rectangle;
private item?: DownloadedItem;
private coordinates?: CoordinatesToJsonType;
@@ -66,19 +66,11 @@ export class DownloadsDropdownMenuView {
throw new Error('Cannot initialize downloadsDropdownMenuView, missing MainWindow');
}
this.bounds = this.getBounds(this.windowBounds.width, DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT);
const preload = getLocalPreload('internalAPI.js');
this.view = new BrowserView({webPreferences: {
preload,
// Workaround for this issue: https://github.com/electron/electron/issues/30993
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
transparent: true,
}});
this.view = new WebContentsView({webPreferences: {preload: getLocalPreload('internalAPI.js')}});
this.view.setBackgroundColor('#00000000');
performanceMonitor.registerView('DownloadsDropdownMenuView', this.view.webContents);
this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdownMenu.html');
MainWindow.get()?.addBrowserView(this.view);
MainWindow.get()?.contentView.addChildView(this.view);
};
/**
@@ -128,7 +120,7 @@ export class DownloadsDropdownMenuView {
this.item = item;
this.bounds = this.getBounds(this.windowBounds.width, DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT);
this.view.setBounds(this.bounds);
MainWindow.get()?.setTopBrowserView(this.view);
MainWindow.get()?.contentView.addChildView(this.view);
this.view.webContents.focus();
this.updateDownloadsDropdownMenu();
};

View File

@@ -38,7 +38,8 @@ jest.mock('electron', () => {
getAppPath: () => '',
getPath: jest.fn(() => '/valid/downloads/path'),
},
BrowserView: jest.fn().mockImplementation(() => ({
WebContentsView: jest.fn().mockImplementation(() => ({
setBackgroundColor: jest.fn(),
webContents: {
loadURL: jest.fn(),
focus: jest.fn(),
@@ -77,7 +78,7 @@ jest.mock('main/windows/mainWindow', () => ({
describe('main/views/DownloadsDropdownView', () => {
beforeEach(() => {
MainWindow.get.mockReturnValue({addBrowserView: jest.fn(), setTopBrowserView: jest.fn()});
MainWindow.get.mockReturnValue({contentView: {addChildView: jest.fn()}});
getDarwinDoNotDisturb.mockReturnValue(false);
});
describe('getBounds', () => {

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import type {IpcMainEvent} from 'electron';
import {BrowserView, ipcMain} from 'electron';
import {WebContentsView, ipcMain} from 'electron';
import {
CLOSE_DOWNLOADS_DROPDOWN,
@@ -33,7 +33,7 @@ export class DownloadsDropdownView {
private bounds?: Electron.Rectangle;
private windowBounds?: Electron.Rectangle;
private item?: DownloadedItem;
private view?: BrowserView;
private view?: WebContentsView;
constructor() {
MainWindow.on(MAIN_WINDOW_CREATED, this.init);
@@ -55,20 +55,11 @@ export class DownloadsDropdownView {
throw new Error('Cannot initialize, no main window');
}
this.bounds = this.getBounds(this.windowBounds.width, DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT);
const preload = getLocalPreload('internalAPI.js');
this.view = new BrowserView({webPreferences: {
preload,
// Workaround for this issue: https://github.com/electron/electron/issues/30993
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
transparent: true,
}});
this.view = new WebContentsView({webPreferences: {preload: getLocalPreload('internalAPI.js')}});
this.view.setBackgroundColor('#00000000');
performanceMonitor.registerView('DownloadsDropdownView', this.view.webContents);
this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdown.html');
MainWindow.get()?.addBrowserView(this.view);
MainWindow.get()?.contentView.addChildView(this.view);
};
/**
@@ -109,7 +100,7 @@ export class DownloadsDropdownView {
}
this.view.setBounds(this.bounds);
MainWindow.get()?.setTopBrowserView(this.view);
MainWindow.get()?.contentView.addChildView(this.view);
this.view.webContents.focus();
downloadsManager.onOpen();
MainWindow.sendToRenderer(OPEN_DOWNLOADS_DROPDOWN);

View File

@@ -21,9 +21,10 @@ jest.mock('main/windows/mainWindow', () => ({
describe('main/views/loadingScreen', () => {
describe('show', () => {
const mainWindow = {
getBrowserViews: jest.fn(),
setTopBrowserView: jest.fn(),
addBrowserView: jest.fn(),
contentView: {
addChildView: jest.fn(),
children: [],
},
};
const loadingScreen = new LoadingScreen();
loadingScreen.create = jest.fn();
@@ -31,7 +32,7 @@ describe('main/views/loadingScreen', () => {
const view = {webContents: {send: jest.fn(), isLoading: () => false}};
beforeEach(() => {
mainWindow.getBrowserViews.mockImplementation(() => []);
mainWindow.contentView.children = [];
MainWindow.get.mockReturnValue(mainWindow);
});
@@ -46,14 +47,14 @@ describe('main/views/loadingScreen', () => {
});
loadingScreen.show();
expect(loadingScreen.create).toHaveBeenCalled();
expect(mainWindow.addBrowserView).toHaveBeenCalled();
expect(mainWindow.contentView.addChildView).toHaveBeenCalled();
});
it('should set the browser view as top if already exists and needs to be shown', () => {
loadingScreen.view = view;
mainWindow.getBrowserViews.mockImplementation(() => [view]);
mainWindow.contentView.children = [view];
loadingScreen.show();
expect(mainWindow.setTopBrowserView).toHaveBeenCalled();
expect(mainWindow.contentView.addChildView).toHaveBeenCalled();
});
});
});

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BrowserView, app, ipcMain} from 'electron';
import {WebContentsView, app, ipcMain} from 'electron';
import {DARK_MODE_CHANGE, LOADING_SCREEN_ANIMATION_FINISHED, MAIN_WINDOW_RESIZED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication';
import {Logger} from 'common/log';
@@ -18,7 +18,7 @@ enum LoadingScreenState {
const log = new Logger('LoadingScreen');
export class LoadingScreen {
private view?: BrowserView;
private view?: WebContentsView;
private state: LoadingScreenState;
constructor() {
@@ -55,15 +55,11 @@ export class LoadingScreen {
if (this.view?.webContents.isLoading()) {
this.view.webContents.once('did-finish-load', () => {
this.view!.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
mainWindow.contentView.addChildView(this.view!);
});
} else {
this.view!.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
}
if (mainWindow.getBrowserViews().includes(this.view!)) {
mainWindow.setTopBrowserView(this.view!);
} else {
mainWindow.addBrowserView(this.view!);
mainWindow.contentView.addChildView(this.view!);
}
this.setBounds();
@@ -77,19 +73,9 @@ export class LoadingScreen {
};
private create = () => {
const preload = getLocalPreload('internalAPI.js');
this.view = new BrowserView({webPreferences: {
preload,
// Workaround for this issue: https://github.com/electron/electron/issues/30993
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
transparent: true,
}});
const localURL = 'mattermost-desktop://renderer/loadingScreen.html';
this.view = new WebContentsView({webPreferences: {preload: getLocalPreload('internalAPI.js')}});
performanceMonitor.registerView('LoadingScreen', this.view.webContents);
this.view.webContents.loadURL(localURL);
this.view.webContents.loadURL('mattermost-desktop://renderer/loadingScreen.html');
};
private handleAnimationFinished = () => {
@@ -97,7 +83,9 @@ export class LoadingScreen {
if (this.view && this.state !== LoadingScreenState.HIDDEN) {
this.state = LoadingScreenState.HIDDEN;
MainWindow.get()?.removeBrowserView(this.view);
MainWindow.get()?.contentView.removeChildView(this.view);
this.view.webContents.close();
delete this.view;
}
if (process.env.NODE_ENV === 'test') {

View File

@@ -6,7 +6,8 @@
import {ModalView} from './modalView';
jest.mock('electron', () => ({
BrowserView: jest.fn().mockImplementation(() => ({
WebContentsView: jest.fn().mockImplementation(() => ({
setBackgroundColor: jest.fn(),
webContents: {
loadURL: jest.fn(),
once: jest.fn(),
@@ -34,7 +35,12 @@ jest.mock('main/performanceMonitor', () => ({
describe('main/views/modalView', () => {
describe('show', () => {
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn()};
const window = {
contentView: {
addChildView: jest.fn(),
removeChildView: jest.fn(),
},
};
const onResolve = jest.fn();
const onReject = jest.fn();
let modalView;
@@ -56,15 +62,15 @@ describe('main/views/modalView', () => {
it('should add to window', () => {
modalView.show();
expect(window.addBrowserView).toBeCalledWith(modalView.view);
expect(window.contentView.addChildView).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);
expect(window.contentView.removeChildView).toBeCalledWith(modalView.view);
expect(window.contentView.addChildView).toBeCalledWith(modalView.view);
});
it('should delay call to focus when the modal is loading', () => {
@@ -87,7 +93,12 @@ describe('main/views/modalView', () => {
});
describe('hide', () => {
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn()};
const window = {
contentView: {
addChildView: jest.fn(),
removeChildView: jest.fn(),
},
};
const onResolve = jest.fn();
const onReject = jest.fn();
let modalView;
@@ -111,7 +122,7 @@ describe('main/views/modalView', () => {
it('should remove browser view and destroy web contents on hide', () => {
modalView.hide();
expect(modalView.view.webContents.close).toBeCalled();
expect(window.removeBrowserView).toBeCalledWith(modalView.view);
expect(window.contentView.removeChildView).toBeCalledWith(modalView.view);
});
it('should close dev tools when open', () => {

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import type {BrowserWindow} from 'electron';
import {BrowserView} from 'electron';
import {WebContentsView} from 'electron';
import {Logger} from 'common/log';
import performanceMonitor from 'main/performanceMonitor';
@@ -20,7 +20,7 @@ export class ModalView<T, T2> {
key: string;
html: string;
data: T;
view: BrowserView;
view: WebContentsView;
onReject: (value: T2) => void;
onResolve: (value: T2) => void;
window: BrowserWindow;
@@ -36,14 +36,8 @@ export class ModalView<T, T2> {
this.data = data;
this.log = new Logger('ModalView', key);
this.log.info(`preloading with ${preload}`);
this.view = new BrowserView({webPreferences: {
preload,
// Workaround for this issue: https://github.com/electron/electron/issues/30993
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
transparent: true,
}});
this.view = new WebContentsView({webPreferences: {preload}});
this.view.setBackgroundColor('#00000000');
this.onReject = onReject;
this.onResolve = onResolve;
this.window = currentWindow;
@@ -64,11 +58,11 @@ export class ModalView<T, T2> {
show = (win?: BrowserWindow, withDevTools?: boolean) => {
if (this.windowAttached) {
// we'll reatach
this.windowAttached.removeBrowserView(this.view);
this.windowAttached.contentView.removeChildView(this.view);
}
this.windowAttached = win || this.window;
this.windowAttached.addBrowserView(this.view);
this.windowAttached.contentView.addChildView(this.view);
// Linux sometimes doesn't have the bound initialized correctly initially, so we wait to set them
const setBoundsFunction = () => {
@@ -100,8 +94,8 @@ export class ModalView<T, T2> {
if (this.view.webContents.isDevToolsOpened()) {
this.view.webContents.closeDevTools();
}
this.windowAttached.removeBrowserView(this.view);
performanceMonitor.unregisterView(this.view.webContents.id);
this.windowAttached.contentView.removeChildView(this.view);
this.view.webContents.close();
delete this.windowAttached;

View File

@@ -15,7 +15,7 @@ jest.mock('main/utils', () => ({
}));
jest.mock('electron', () => ({
BrowserView: jest.fn().mockImplementation(() => ({
WebContentsView: jest.fn().mockImplementation(() => ({
webContents: {
loadURL: jest.fn(),
focus: jest.fn(),

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import type {IpcMainEvent} from 'electron';
import {BrowserView, ipcMain} from 'electron';
import {WebContentsView, ipcMain} from 'electron';
import ServerViewState from 'app/serverViewState';
import AppState from 'common/appState';
@@ -32,7 +32,7 @@ import MainWindow from '../windows/mainWindow';
const log = new Logger('ServerDropdownView');
export class ServerDropdownView {
private view?: BrowserView;
private view?: WebContentsView;
private servers: UniqueServer[];
private hasGPOServers: boolean;
private isOpen: boolean;
@@ -75,22 +75,15 @@ export class ServerDropdownView {
private init = () => {
log.info('init');
const preload = getLocalPreload('internalAPI.js');
this.view = new BrowserView({webPreferences: {
preload,
// Workaround for this issue: https://github.com/electron/electron/issues/30993
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
transparent: true,
}});
this.view = new WebContentsView({webPreferences: {preload: getLocalPreload('internalAPI.js')}});
this.view.setBackgroundColor('#00000000');
performanceMonitor.registerView('ServerDropdownView', this.view.webContents);
this.view.webContents.loadURL('mattermost-desktop://renderer/dropdown.html');
this.setOrderedServers();
this.windowBounds = MainWindow.getBounds();
this.updateDropdown();
MainWindow.get()?.addBrowserView(this.view);
MainWindow.get()?.contentView.addChildView(this.view);
};
private updateDropdown = () => {
@@ -138,7 +131,7 @@ export class ServerDropdownView {
return;
}
this.view.setBounds(this.bounds);
MainWindow.get()?.setTopBrowserView(this.view);
MainWindow.get()?.contentView.addChildView(this.view);
this.view.webContents.focus();
MainWindow.sendToRenderer(OPEN_SERVERS_DROPDOWN);
this.isOpen = true;

View File

@@ -12,7 +12,7 @@ import PermissionsManager from 'main/permissionsManager';
import MainWindow from 'main/windows/mainWindow';
import LoadingScreen from './loadingScreen';
import {MattermostBrowserView} from './MattermostBrowserView';
import {MattermostWebContentsView} from './MattermostWebContentsView';
import {ViewManager} from './viewManager';
jest.mock('electron', () => ({
@@ -112,8 +112,8 @@ jest.mock('common/servers/serverManager', () => ({
}),
}));
jest.mock('./MattermostBrowserView', () => ({
MattermostBrowserView: jest.fn(),
jest.mock('./MattermostWebContentsView', () => ({
MattermostWebContentsView: jest.fn(),
}));
jest.mock('./modalManager', () => ({
@@ -133,7 +133,7 @@ describe('main/views/viewManager', () => {
beforeEach(() => {
viewManager.showById = jest.fn();
MainWindow.get.mockReturnValue({});
MattermostBrowserView.mockImplementation((view) => ({
MattermostWebContentsView.mockImplementation((view) => ({
on: jest.fn(),
load: loadFn,
once: onceFn,
@@ -181,7 +181,7 @@ describe('main/views/viewManager', () => {
beforeEach(() => {
viewManager.showById = jest.fn();
MainWindow.get.mockReturnValue({});
MattermostBrowserView.mockImplementation((view) => ({
MattermostWebContentsView.mockImplementation((view) => ({
on: jest.fn(),
load: jest.fn(),
once: jest.fn(),
@@ -235,7 +235,7 @@ describe('main/views/viewManager', () => {
const onceFn = jest.fn();
const loadFn = jest.fn();
const destroyFn = jest.fn();
MattermostBrowserView.mockImplementation((view) => ({
MattermostWebContentsView.mockImplementation((view) => ({
on: jest.fn(),
load: loadFn,
once: onceFn,
@@ -255,7 +255,7 @@ describe('main/views/viewManager', () => {
it('should recycle existing views', () => {
const makeSpy = jest.spyOn(viewManager, 'makeView');
const view = new MattermostBrowserView({
const view = new MattermostWebContentsView({
id: 'view1',
server: {
id: 'server1',

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import type {IpcMainEvent, IpcMainInvokeEvent} from 'electron';
import {BrowserView, dialog, ipcMain} from 'electron';
import {WebContentsView, dialog, ipcMain} from 'electron';
import isDev from 'electron-is-dev';
import ServerViewState from 'app/serverViewState';
@@ -51,7 +51,7 @@ import MainWindow from 'main/windows/mainWindow';
import type {DeveloperSettings} from 'types/settings';
import LoadingScreen from './loadingScreen';
import {MattermostBrowserView} from './MattermostBrowserView';
import {MattermostWebContentsView} from './MattermostWebContentsView';
import modalManager from './modalManager';
import {getLocalPreload, getAdjustedWindowBoundaries} from '../utils';
@@ -62,7 +62,7 @@ const URL_VIEW_HEIGHT = 20;
export class ViewManager {
private closedViews: Map<string, {srv: MattermostServer; view: MattermostView}>;
private views: Map<string, MattermostBrowserView>;
private views: Map<string, MattermostWebContentsView>;
private currentView?: string;
private urlViewCancel?: () => void;
@@ -206,23 +206,23 @@ export class ViewManager {
if (this.closedViews.has(view.id)) {
this.openClosedView(view.id, urlWithSchema);
} else {
const browserView = this.views.get(view.id);
if (!browserView) {
const webContentsView = this.views.get(view.id);
if (!webContentsView) {
log.error(`Couldn't find a view matching the id ${view.id}`);
return;
}
if (browserView.isReady() && ServerManager.getRemoteInfo(browserView.view.server.id)?.serverVersion && Utils.isVersionGreaterThanOrEqualTo(ServerManager.getRemoteInfo(browserView.view.server.id)?.serverVersion ?? '', '6.0.0')) {
const formattedServerURL = `${browserView.view.server.url.origin}${getFormattedPathName(browserView.view.server.url.pathname)}`;
if (webContentsView.isReady() && ServerManager.getRemoteInfo(webContentsView.view.server.id)?.serverVersion && Utils.isVersionGreaterThanOrEqualTo(ServerManager.getRemoteInfo(webContentsView.view.server.id)?.serverVersion ?? '', '6.0.0')) {
const formattedServerURL = `${webContentsView.view.server.url.origin}${getFormattedPathName(webContentsView.view.server.url.pathname)}`;
const pathName = `/${urlWithSchema.replace(formattedServerURL, '')}`;
browserView.sendToRenderer(BROWSER_HISTORY_PUSH, pathName);
this.deeplinkSuccess(browserView.id);
webContentsView.sendToRenderer(BROWSER_HISTORY_PUSH, pathName);
this.deeplinkSuccess(webContentsView.id);
} else {
// attempting to change parsedURL protocol results in it not being modified.
browserView.resetLoadingStatus();
browserView.load(urlWithSchema);
browserView.once(LOAD_SUCCESS, this.deeplinkSuccess);
browserView.once(LOAD_FAILED, this.deeplinkFailed);
webContentsView.resetLoadingStatus();
webContentsView.load(urlWithSchema);
webContentsView.once(LOAD_SUCCESS, this.deeplinkSuccess);
webContentsView.once(LOAD_FAILED, this.deeplinkFailed);
}
}
} else {
@@ -260,26 +260,26 @@ export class ViewManager {
this.closedViews.set(view.id, {srv, view});
return;
}
const browserView = this.makeView(srv, view, url);
this.addView(browserView);
const webContentsView = this.makeView(srv, view, url);
this.addView(webContentsView);
};
private makeView = (srv: MattermostServer, view: MattermostView, url?: string): MattermostBrowserView => {
private makeView = (srv: MattermostServer, view: MattermostView, url?: string): MattermostWebContentsView => {
const mainWindow = MainWindow.get();
if (!mainWindow) {
throw new Error('Cannot create view, no main window present');
}
const browserView = new MattermostBrowserView(view, {webPreferences: {spellcheck: Config.useSpellChecker}});
browserView.once(LOAD_SUCCESS, this.activateView);
browserView.on(LOADSCREEN_END, this.finishLoading);
browserView.on(LOAD_FAILED, this.failLoading);
browserView.on(UPDATE_TARGET_URL, this.showURLView);
browserView.load(url);
return browserView;
const webContentsView = new MattermostWebContentsView(view, {webPreferences: {spellcheck: Config.useSpellChecker}});
webContentsView.once(LOAD_SUCCESS, this.activateView);
webContentsView.on(LOADSCREEN_END, this.finishLoading);
webContentsView.on(LOAD_FAILED, this.failLoading);
webContentsView.on(UPDATE_TARGET_URL, this.showURLView);
webContentsView.load(url);
return webContentsView;
};
private addView = (view: MattermostBrowserView): void => {
private addView = (view: MattermostWebContentsView): void => {
this.views.set(view.id, view);
// Force a permission check for notifications
@@ -355,26 +355,18 @@ export class ViewManager {
}
if (url && url !== '') {
const urlString = typeof url === 'string' ? url : url.toString();
const preload = getLocalPreload('internalAPI.js');
const urlView = new BrowserView({
webPreferences: {
preload,
// Workaround for this issue: https://github.com/electron/electron/issues/30993
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
transparent: true,
}});
const urlView = new WebContentsView({webPreferences: {preload: getLocalPreload('internalAPI.js')}});
urlView.setBackgroundColor('#00000000');
const localURL = `mattermost-desktop://renderer/urlView.html?url=${encodeURIComponent(urlString)}`;
performanceMonitor.registerView('URLView', urlView.webContents);
urlView.webContents.loadURL(localURL);
MainWindow.get()?.addBrowserView(urlView);
MainWindow.get()?.contentView.addChildView(urlView);
const boundaries = this.views.get(this.currentView || '')?.getBounds() ?? MainWindow.getBounds();
const hideView = () => {
delete this.urlViewCancel;
try {
mainWindow.removeBrowserView(urlView);
mainWindow.contentView.removeChildView(urlView);
} catch (e) {
log.error('Failed to remove URL view', e);
}
@@ -427,12 +419,12 @@ export class ViewManager {
const currentViewId: string | undefined = this.views.get(this.currentView as string)?.view.id;
const current: Map<string, MattermostBrowserView> = new Map();
const current: Map<string, MattermostWebContentsView> = new Map();
for (const view of this.views.values()) {
current.set(view.view.id, view);
}
const views: Map<string, MattermostBrowserView> = new Map();
const views: Map<string, MattermostWebContentsView> = new Map();
const closed: Map<string, {srv: MattermostServer; view: MattermostView}> = new Map();
const sortedViews = ServerManager.getAllServers().flatMap((x) => ServerManager.getOrderedTabsForServer(x.id).
@@ -622,10 +614,10 @@ export class ViewManager {
this.closedViews.delete(view.id);
}
this.showById(id);
const browserView = this.views.get(id)!;
browserView.isVisible = true;
browserView.on(LOAD_SUCCESS, () => {
browserView.isVisible = false;
const webContentsView = this.views.get(id)!;
webContentsView.isVisible = true;
webContentsView.on(LOAD_SUCCESS, () => {
webContentsView.isVisible = false;
this.showById(id);
});
ipcMain.emit(OPEN_VIEW, null, view.id);

View File

@@ -35,7 +35,7 @@ import {
openScreensharePermissionsSettingsMacOS,
resetScreensharePermissionsMacOS,
} from 'main/utils';
import type {MattermostBrowserView} from 'main/views/MattermostBrowserView';
import type {MattermostWebContentsView} from 'main/views/MattermostWebContentsView';
import ViewManager from 'main/views/viewManager';
import webContentsEventManager from 'main/views/webContentEvents';
import MainWindow from 'main/windows/mainWindow';
@@ -51,7 +51,7 @@ const log = new Logger('CallsWidgetWindow');
export class CallsWidgetWindow {
private win?: BrowserWindow;
private mainView?: MattermostBrowserView;
private mainView?: MattermostWebContentsView;
private options?: CallsWidgetWindowConfig;
private missingScreensharePermissions?: boolean;
@@ -135,7 +135,7 @@ export class CallsWidgetWindow {
return u.toString();
};
private init = (view: MattermostBrowserView, options: CallsWidgetWindowConfig) => {
private init = (view: MattermostWebContentsView, options: CallsWidgetWindowConfig) => {
this.win = new BrowserWindow({
width: MINIMUM_CALLS_WIDGET_WIDTH,
height: MINIMUM_CALLS_WIDGET_HEIGHT,

View File

@@ -96,6 +96,9 @@ describe('main/windows/mainWindow', () => {
send: jest.fn(),
setWindowOpenHandler: jest.fn(),
},
contentView: {
on: jest.fn(),
},
isMaximized: jest.fn(),
isFullScreen: jest.fn(),
getBounds: jest.fn(),

View File

@@ -21,7 +21,6 @@ import {
MAIN_WINDOW_CREATED,
MAIN_WINDOW_RESIZED,
MAIN_WINDOW_FOCUSED,
VIEW_FINISHED_RESIZING,
TOGGLE_SECURE_INPUT,
EMIT_CONFIGURATION,
EXIT_FULLSCREEN,
@@ -49,18 +48,14 @@ export class MainWindow extends EventEmitter {
private savedWindowState?: Partial<SavedWindowState>;
private ready: boolean;
private isResizing: boolean;
private lastEmittedBounds?: Electron.Rectangle;
constructor() {
super();
// Create the browser window.
this.ready = false;
this.isResizing = false;
ipcMain.handle(GET_FULL_SCREEN_STATUS, () => this.win?.isFullScreen());
ipcMain.on(VIEW_FINISHED_RESIZING, this.handleViewFinishedResizing);
ipcMain.on(EMIT_CONFIGURATION, this.handleUpdateTitleBarOverlay);
ipcMain.on(EXIT_FULLSCREEN, this.handleExitFullScreen);
@@ -85,7 +80,7 @@ export class MainWindow extends EventEmitter {
titleBarStyle: 'hidden' as const,
titleBarOverlay: this.getTitleBarOverlay(),
trafficLightPosition: {x: 12, y: 12},
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
backgroundColor: '#000', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
webPreferences: {
disableBlinkFeatures: 'Auxclick',
preload: getLocalPreload('internalAPI.js'),
@@ -131,16 +126,7 @@ export class MainWindow extends EventEmitter {
this.win.on('unresponsive', this.onUnresponsive);
this.win.on('enter-full-screen', this.onEnterFullScreen);
this.win.on('leave-full-screen', this.onLeaveFullScreen);
this.win.on('will-resize', this.onWillResize);
this.win.on('resized', this.onResized);
if (process.platform === 'win32') {
// We don't want this on macOS, it's an alias of 'move'
// This is mostly a fix for Windows 11 snapping
this.win.on('moved', this.onResized);
}
if (process.platform !== 'darwin') {
this.win.on('resize', this.onResize);
}
this.win.contentView.on('bounds-changed', this.handleBoundsChanged);
this.win.webContents.on('before-input-event', this.onBeforeInputEvent);
// Should not allow the main window to generate a window of its own
@@ -457,82 +443,16 @@ export class MainWindow extends EventEmitter {
});
};
private emitBounds = (bounds?: Electron.Rectangle, force?: boolean) => {
// Workaround since the window bounds aren't updated immediately when the window is maximized for some reason
// We also don't want to force too many resizes so we throttle here
setTimeout(() => {
const newBounds = bounds ?? this.getBounds();
if (!force && newBounds?.height === this.lastEmittedBounds?.height && newBounds?.width === this.lastEmittedBounds?.width) {
return;
}
// For some reason on Linux I've seen the menu bar popup again
this.win?.setMenuBarVisibility(false);
this.emit(MAIN_WINDOW_RESIZED, newBounds);
this.lastEmittedBounds = newBounds;
}, 10);
};
private onEnterFullScreen = () => {
this.win?.webContents.send('enter-full-screen');
this.emitBounds();
};
private onLeaveFullScreen = () => {
this.win?.webContents.send('leave-full-screen');
this.emitBounds();
};
/**
* Resizing code
*/
private onWillResize = (event: Event, newBounds: Electron.Rectangle) => {
log.silly('onWillResize', newBounds);
/**
* Fixes an issue on win11 related to Snap where the first "will-resize" event would return the same bounds
* causing the "resize" event to not fire
*/
const prevBounds = this.getBounds();
if (prevBounds?.height === newBounds.height && prevBounds?.width === newBounds.width) {
log.debug('prevented resize');
event.preventDefault();
return;
}
// Workaround for macOS to stop the window from sending too many resize calls to the BrowserViews
if (process.platform === 'darwin' && this.isResizing) {
log.debug('prevented resize');
event.preventDefault();
return;
}
this.isResizing = true;
this.emitBounds(newBounds);
};
private onResize = () => {
log.silly('onResize');
// Workaround for Windows to stop the window from sending too many resize calls to the BrowserViews
if (process.platform === 'win32' && this.isResizing) {
return;
}
this.emitBounds();
};
private onResized = () => {
log.debug('onResized');
// Because this is the final window state after a resize, we force the size here
this.emitBounds(this.getBounds(), true);
this.isResizing = false;
};
private handleViewFinishedResizing = () => {
this.isResizing = false;
private handleBoundsChanged = () => {
this.emit(MAIN_WINDOW_RESIZED, this.win?.contentView.getBounds());
};
private handleExitFullScreen = () => {

View File

@@ -94,7 +94,7 @@ export default function ErrorView(props: Props) {
</li>
<li>
<FormattedMessage
id='renderer.components.errorView.troubleshooting.browserView.canReachFromBrowserWindow'
id='renderer.components.errorView.troubleshooting.webContentsView.canReachFromBrowserWindow'
defaultMessage='You can reach <link>{url}</link> from a browser window.'
values={{
url: props.url,

View File

@@ -36,12 +36,6 @@ function WelcomeScreen({
useEffect(() => {
setShowContent(true);
// Let the main process know when the window has finished resizing
// This is to reduce the amount of white box that happens when expand the BrowserView
window.addEventListener('resize', () => {
window.desktop.viewFinishedResizing();
});
}, []);
const slides = useMemo(() => [

View File

@@ -95,7 +95,6 @@ declare global {
openWindowsCameraPreferences: () => void;
openWindowsMicrophonePreferences: () => void;
getMediaAccessStatus: (mediaType: 'microphone' | 'camera' | 'screen') => Promise<'not-determined' | 'granted' | 'denied' | 'restricted' | 'unknown'>;
viewFinishedResizing: () => void;
modals: {
cancelModal: <T>(data?: T) => void;