diff --git a/.vscode/launch.json b/.vscode/launch.json
index 94ccd04c..4489230f 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -10,29 +10,6 @@
"webRoot": "${workspaceRoot}/packages/desktopjs-openfin/dist",
"sourceMaps": true
},
- {
- "name": "Debug Electron Main",
- "type": "node",
- "request": "launch",
- "cwd": "${workspaceRoot}/examples/web",
- "runtimeExecutable": "electron.cmd", // For windows
- //"runtimeExecutable": "electron" // Other platforms
- "program": "${workspaceRoot}/examples/electron/electron.js"
- },
- {
- "name": "Debug Electron Renderer",
- "type": "chrome",
- "request": "launch",
- "runtimeExecutable": "electron.cmd", // For windows
- //"runtimeExecutable": "electron" // Other platforms
- "runtimeArgs": [
- "${workspaceRoot}/examples/electron",
- "--enable-logging",
- "--remote-debugging-port=9222"
- ],
- "sourceMaps": true,
- "webRoot": "${workspaceRoot}/dist"
- },
{
"type": "node",
"request": "launch",
@@ -44,4 +21,4 @@
"smartStep": true
}
]
-}
\ No newline at end of file
+}
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 04d0ced8..3166b0ec 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -80,13 +80,6 @@ export default [
'license-header/header': ['error', LICENSE_HEADER] // Update the license header rule to use the array
},
},
- {
- // Override for Electron source file allowing require
- files: ['packages/desktopjs-electron/src/electron.ts'],
- rules: {
- '@typescript-eslint/no-require-imports': 'off'
- }
- },
{
// Override for spec files
files: ['**/*.spec.ts'],
diff --git a/examples/README.md b/examples/README.md
index 6720d15a..43a89cd6 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,5 +1,3 @@
-[Electron](electron/README.md)
-
[OpenFin](openfin/README.md)
[Web](web/README.md)
diff --git a/examples/electron/README.md b/examples/electron/README.md
deleted file mode 100644
index fe0bfeb9..00000000
--- a/examples/electron/README.md
+++ /dev/null
@@ -1,99 +0,0 @@
-desktopJS and [Electron](https://electron.atom.io/ "Electron")
-===============================================================
-
-It is possible to use desktopJS within the main electron node process to author
-container portable bootstraps or within the renderer to leverage desktop functionality
-from your application code.
-
-To launch the example manually, run electron within the examples/web directory providing
-the electron directory as the argument. It is necessary to launch from the examples
-directory to ensure assets used from examples/web are also within the scope of the
-node process.
-
-
- [examples/web] $ electron ../electron --enable-logging
-
-
-### Electron main
-
-From your bootstrap, use the same consistent api as you would from your application.
-
-```
-const desktopJS = require('@morgan-stanley/desktopjs');
-const djsElectron = require('@morgan-stanley/desktopjs-electron');
-const app = electron.app;
-
-let mainWindow;
-
-function createWindow() {
- let container = desktopJS.resolveContainer();
- container.createWindow('http://localhost:8000').then(win => mainWindow = win);
-}
-
-app.on("ready", createWindow);
-```
-
-### Electron renderer
-
-index.html
-
-```
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-app.js
-
-```
-var container = desktopJS.resolveContainer();
-
-var hostName = document.getElementById('hostName');
-var btnOpenWindow = document.getElementById('button-open-window');
-var childWindow;
-
-document.addEventListener("DOMContentLoaded", function (event) {
- hostName.innerHTML = container.hostType + " " + container.uuid
-});
-
-btnOpenWindow.onclick = function () {
- container.createWindow("child.html",
- {
- resizable: true,
- x: 10, y: 10,
- width: 500, height: 300,
- minWidth: 200, minHeight: 100, maxWidth: 800, maxHeight: 500,
- taskbar: true, icon: "assets/img/application.png",
- minimizable: true, maximizable: true,
- alwaysOnTop: false, center: false,
- }).then(win => childWindow = win);
-};
-```
-
-In order to show notifications while hosted in Electron, it is necessary for you as a
-developer to provide a polyfill of showNotification. This allows you the flexibility
-to use the node module of your choice for displaying notifications.
-
-Here is an example polyfill using electron-notify.
-
-```
-desktopJS.Electron.ElectronContainer.prototype.showNotification = function(title, options) {
- notifier = (this.isRemote)
- ? this.electron.require("electron-notify")
- : require("electron-notify");
-
- notifier.notify({
- title: title,
- text: options.body
- });
-};
-```
diff --git a/examples/electron/electron.js b/examples/electron/electron.js
deleted file mode 100644
index 46693ce2..00000000
--- a/examples/electron/electron.js
+++ /dev/null
@@ -1,40 +0,0 @@
-const desktopJS = require('@morgan-stanley/desktopjs');
-const djsElectron = require('@morgan-stanley/desktopjs-electron');
-const electron = require('electron');
-const app = electron.app;
-
-let mainWindow;
-let snapAssist;
-
-function createWindow() {
- let container = desktopJS.resolveContainer({node: true});
-
- desktopJS.ContainerWindow.addListener("window-created", (e) => container.log("info", "Window created - static (ContainerWindow): " + e.windowId + ", " + e.windowName));
- desktopJS.ContainerWindow.addListener("window-joinGroup", (e) => container.log("info", "grouped " + JSON.stringify(e)));
- desktopJS.ContainerWindow.addListener("window-leaveGroup", (e) => container.log("info", "ungrouped" + JSON.stringify(e)));
-
- snapAssist = new desktopJS.SnapAssistWindowManager(container,
- {
- windowStateTracking: desktopJS.WindowStateTracking.Main | desktopJS.WindowStateTracking.Group
- });
-
- container.createWindow('http://localhost:8000', { name: "desktopJS", main: true }).then(win => mainWindow = win);
-
- let trayIcon = electron.nativeImage.createFromPath(__dirname + '\\..\\web\\favicon.ico');
- container.addTrayIcon({ icon: trayIcon, text: 'ContainerPOC' }, () => {
- mainWindow.isShowing().then((showing) => {
- if (showing) {
- mainWindow.hide();
- } else {
- mainWindow.show();
- }
- });
- }, [{ label: "Exit", click: (menuItem) => app.quit() }]);
-
- container.ipc.subscribe("stock.selected", function (event, message) {
- container.log("info", "Message received: " + message.symbol);
- });
-}
-
-app.on("ready", createWindow);
-
diff --git a/examples/electron/package.json b/examples/electron/package.json
deleted file mode 100644
index d4679fdc..00000000
--- a/examples/electron/package.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "name": "desktopJS",
- "version": "0.0.0",
- "description": "desktopJS Demo",
- "main": "electron.js",
- "private": true,
- "dependencies": {
- "@morgan-stanley/desktopjs": "file:../../packages/desktopjs",
- "@morgan-stanley/desktopjs-electron": "file:../../packages/desktopjs-electron"
- }
-}
diff --git a/examples/web/assets/js/app.js b/examples/web/assets/js/app.js
index 3ee522d8..d89c0150 100644
--- a/examples/web/assets/js/app.js
+++ b/examples/web/assets/js/app.js
@@ -46,19 +46,6 @@ desktopJS.Default.DefaultContainerWindow.prototype.getSnapshot = function () {
};
*/
-/*
-// Provide polyfill for electron notifications. here is an example using electron-notify
-desktopJS.Electron.ElectronContainer.prototype.showNotification = function (title, options) {
- notifier = (this.isRemote) ? this.electron.require("electron-notify") : require("electron-notify");
-
- notifier.notify({
- title: title,
- text: options.body,
- onClickFunc: function () { options["notification"].onclick(); }
- });
-};
-*/
-
document.addEventListener("DOMContentLoaded", function (event) {
updatefps();
diff --git a/examples/web/index.html b/examples/web/index.html
index 1f8bcd37..941e4802 100644
--- a/examples/web/index.html
+++ b/examples/web/index.html
@@ -7,7 +7,6 @@
desktopJS
-
diff --git a/package-lock.json b/package-lock.json
index d6d8dc30..1faf850e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,9 +8,7 @@
"license": "Apache-2.0",
"workspaces": [
"packages/desktopjs",
- "packages/desktopjs-electron",
- "packages/desktopjs-openfin",
- "examples/electron"
+ "packages/desktopjs-openfin"
],
"dependencies": {
"@rollup/rollup-linux-x64-gnu": "4.62.3"
@@ -48,27 +46,24 @@
"examples/electron": {
"name": "desktopJS",
"version": "0.0.0",
+ "extraneous": true,
"dependencies": {
"@morgan-stanley/desktopjs": "file:../../packages/desktopjs",
"@morgan-stanley/desktopjs-electron": "file:../../packages/desktopjs-electron"
}
},
- "examples/electron/node_modules/@morgan-stanley/desktopjs": {
- "resolved": "examples/packages/desktopjs",
- "link": true
- },
- "examples/electron/node_modules/@morgan-stanley/desktopjs-electron": {
- "resolved": "examples/file:packages/desktopjs-electron",
- "link": true
- },
"examples/electron/packages/desktopjs": {
"extraneous": true
},
"examples/electron/packages/desktopjs-electron": {
"extraneous": true
},
- "examples/file:packages/desktopjs-electron": {},
- "examples/packages/desktopjs": {},
+ "examples/file:packages/desktopjs-electron": {
+ "extraneous": true
+ },
+ "examples/packages/desktopjs": {
+ "extraneous": true
+ },
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@@ -135,6 +130,7 @@
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -708,6 +704,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
},
@@ -731,6 +728,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -2866,10 +2864,6 @@
"resolved": "packages/desktopjs",
"link": true
},
- "node_modules/@morgan-stanley/desktopjs-electron": {
- "resolved": "packages/desktopjs-electron",
- "link": true
- },
"node_modules/@morgan-stanley/desktopjs-openfin": {
"resolved": "packages/desktopjs-openfin",
"link": true
@@ -3201,9 +3195,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -3346,6 +3337,7 @@
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -3813,6 +3805,7 @@
"integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/types": "8.65.0",
@@ -4636,6 +4629,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5090,6 +5084,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001718",
"electron-to-chromium": "^1.5.160",
@@ -5626,10 +5621,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/desktopJS": {
- "resolved": "examples/electron",
- "link": true
- },
"node_modules/detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -5962,6 +5953,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -9548,6 +9540,7 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -10964,6 +10957,7 @@
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -11011,9 +11005,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -12086,6 +12077,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -12272,6 +12264,7 @@
"integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.27.0 || ^0.28.0",
"fdir": "^6.5.0",
@@ -12834,15 +12827,12 @@
"packages/desktopjs-electron": {
"name": "@morgan-stanley/desktopjs-electron",
"version": "4.0.2",
+ "extraneous": true,
"license": "Apache-2.0",
"devDependencies": {
"@morgan-stanley/desktopjs": "file:../../packages/desktopjs"
}
},
- "packages/desktopjs-electron/node_modules/@morgan-stanley/desktopjs": {
- "resolved": "packages/packages/desktopjs",
- "link": true
- },
"packages/desktopjs-electron/packages/desktopjs": {
"extraneous": true
},
diff --git a/package.json b/package.json
index 0d38ab43..6bd2d39b 100644
--- a/package.json
+++ b/package.json
@@ -59,9 +59,7 @@
},
"workspaces": [
"packages/desktopjs",
- "packages/desktopjs-electron",
- "packages/desktopjs-openfin",
- "examples/electron"
+ "packages/desktopjs-openfin"
],
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "^4.41.1"
diff --git a/packages/desktopjs-electron/README.md b/packages/desktopjs-electron/README.md
deleted file mode 100644
index 780323ac..00000000
--- a/packages/desktopjs-electron/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# @morgan-stanley/desktopjs-electron
-
-[desktopJS](https://github.com/MorganStanley/desktopJS) container implementation for Electron
\ No newline at end of file
diff --git a/packages/desktopjs-electron/jest.config.js b/packages/desktopjs-electron/jest.config.js
deleted file mode 100644
index c833001b..00000000
--- a/packages/desktopjs-electron/jest.config.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/** @type {import('jest').Config} */
-const config = {
- preset: 'ts-jest',
- testEnvironment: 'jsdom',
- testMatch: [
- '/tests/**/*.spec.ts'
- ],
- setupFilesAfterEnv: [
- '/tests/setup.ts'
- ],
- transform: {
- '^.+\\.tsx?$': ['ts-jest', {
- tsconfig: '/../../tsconfig.test.json'
- }]
- },
- collectCoverageFrom: [
- '/src/**/*.ts',
- '!/src/**/*.d.ts'
- ],
- coverageDirectory: '/build/coverage',
-
-};
-
-// eslint-disable-next-line no-undef
-module.exports = config;
diff --git a/packages/desktopjs-electron/package.json b/packages/desktopjs-electron/package.json
deleted file mode 100644
index 21f66a84..00000000
--- a/packages/desktopjs-electron/package.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "name": "@morgan-stanley/desktopjs-electron",
- "title": "desktopJS-electron",
- "description": "desktopJS container implementation for Electron",
- "version": "4.0.2",
- "publishConfig": {
- "access": "public"
- },
- "main": "./dist/desktopjs-electron.umd.js",
- "types": "./dist/desktopjs-electron.d.ts",
- "files": [
- "dist"
- ],
- "scripts": {
- "clean": "rimraf dist build",
- "build": "vite build",
- "test": "jest --coverage",
- "test:watch": "jest --watch",
- "deploy": "npm publish --provenance --access public"
- },
- "license": "Apache-2.0",
- "author": "Morgan Stanley",
- "repository": {
- "type": "git",
- "url": "git+https://github.com/morganstanley/desktopJS.git"
- },
- "bugs": {
- "url": "https://github.com/MorganStanley/desktopJS/issues"
- },
- "keywords": [
- "container",
- "desktop wrapper",
- "desktop",
- "electron"
- ],
- "homepage": "https://github.com/MorganStanley/desktopJS/tree/main#readme",
- "devDependencies": {
- "@morgan-stanley/desktopjs": "file:../../packages/desktopjs"
- }
-}
diff --git a/packages/desktopjs-electron/src/electron.ts b/packages/desktopjs-electron/src/electron.ts
deleted file mode 100644
index c9634a68..00000000
--- a/packages/desktopjs-electron/src/electron.ts
+++ /dev/null
@@ -1,770 +0,0 @@
-/*
- * Morgan Stanley makes this available to you under the Apache License,
- * Version 2.0 (the "License"). You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * See the NOTICE file distributed with this work for additional information
- * regarding copyright ownership. Unless required by applicable law or agreed
- * to in writing, software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions
- * and limitations under the License.
- */
-
-/* eslint-disable @typescript-eslint/no-var-requires */
-/**
- * @module @morgan-stanley/desktopjs-electron
- */
-
-import {
- registerContainer, ContainerWindow, PersistedWindowLayout, Rectangle, Container, WebContainerBase,
- ScreenManager, Display, Point, ObjectTransform, PropertyMap, NotificationOptions, ContainerNotification,
- TrayIconDetails, MenuItem, Guid, MessageBus, MessageBusSubscription, MessageBusOptions, GlobalShortcutManager,
- EventArgs, WindowEventArgs
-} from "@morgan-stanley/desktopjs";
-
-registerContainer("Electron", {
- condition: () => {
- try {
- return typeof require !== "undefined" && (require("electron") || require("electron").remote);
- } catch (e) {
- return false;
- }
- },
- create: (options) => new ElectronContainer(null, null, null, options)
-});
-
-class InternalMessageType {
- public static readonly initialize: string = "desktopJS.window-initialize";
- public static readonly getGroup: string = "desktopJS.window-getGroup";
- public static readonly joinGroup: string = "desktopJS.window-joinGroup";
- public static readonly leaveGroup: string = "desktopJS.window-leaveGroup";
- public static readonly getOptions: string = "desktopJS.window-getOptions";
-}
-
-const windowEventMap = {};
-
-/**
- * @augments ContainerWindow
- */
-export class ElectronContainerWindow extends ContainerWindow {
- private readonly container: ElectronContainer;
- private readonly window: Window;
-
- public constructor(wrap: any, container: ElectronContainer, win?: Window) {
- super(wrap);
- this.container = container;
- this.window = win;
- }
-
- private get isRemote(): boolean {
- return !!this.container.internalIpc.send;
- }
-
- public get id(): string {
- return this.innerWindow.id || this.innerWindow.guestId;
- }
-
- public get name(): string {
- return this.innerWindow.name;
- }
-
- public async load(url: string, options?: any) {
- if (options) {
- this.innerWindow.loadURL(url, options);
- } else {
- this.innerWindow.loadURL(url);
- }
- }
-
- public async focus() {
- this.innerWindow.focus();
- }
-
- public async show() {
- this.innerWindow.show();
- }
-
- public async hide() {
- this.innerWindow.hide();
- }
-
- public async close() {
- this.innerWindow.close();
- }
-
- public async maximize() {
- this.innerWindow.maximize();
- }
-
- public async minimize() {
- this.innerWindow.minimize();
- }
-
- public async restore() {
- this.innerWindow.restore();
- }
-
- public async isShowing() {
- return this.innerWindow.isVisible();
- }
-
- public async getSnapshot() {
- return new Promise((resolve, reject) => {
- this.innerWindow.capturePage((snapshot) => {
- resolve("data:image/png;base64," + snapshot.toPNG().toString("base64"));
- });
- });
- }
-
- public async flash(enable: boolean, options?: any) {
- this.innerWindow.flashFrame(enable);
- }
-
- public async getParent(): Promise {
- return this.innerWindow.getParentWindow();
- }
-
- public async setParent(parent: ContainerWindow) {
- this.innerWindow.setParentWindow(parent.innerWindow);
- }
-
- public async getBounds() {
- const { x, y, width, height } = this.innerWindow.getBounds();
- return new Rectangle(x, y, width, height);
- }
-
- public async setBounds(bounds: Rectangle) {
- this.innerWindow.setBounds(bounds);
- }
-
- public get allowGrouping() {
- return true;
- }
-
- public async getGroup(): Promise {
- const ids = (this.isRemote)
- ? (this.container.internalIpc).sendSync(InternalMessageType.getGroup, { source: this.id })
- : (this.container).windowManager.getGroup(this.innerWindow);
-
- return ids.map(id => (id === this.id) ? this : this.container.wrapWindow(this.container.browserWindow.fromId(id)));
- }
-
- public async joinGroup(target: ContainerWindow) {
- if (!target || target.id === this.id) {
- return;
- }
-
- if (this.isRemote) {
- (this.container.internalIpc).send(InternalMessageType.joinGroup, { source: this.id, target: target.id });
- } else {
- (this.container).windowManager.groupWindows(target.innerWindow, this.innerWindow);
- }
- }
-
- public async leaveGroup() {
- if (this.isRemote) {
- (this.container.internalIpc).send(InternalMessageType.leaveGroup, { source: this.id });
- } else {
- (this.container).windowManager.ungroupWindows(this.innerWindow);
- }
- }
-
- public async bringToFront() {
- this.innerWindow.moveTop();
- }
-
- public async getOptions() {
- const options = (this.isRemote)
- ? (this.container.internalIpc).sendSync(InternalMessageType.getOptions, { source: this.id })
- : this.innerWindow[Container.windowOptionsPropertyKey];
-
- return options;
- }
-
- public async getState() {
- if (this.innerWindow && this.innerWindow.webContents) {
- return this.innerWindow.webContents.executeJavaScript("window.getState ? window.getState() : undefined");
- }
- }
-
- public async setState(state: any) {
- await this.innerWindow?.webContents?.executeJavaScript(`if (window.setState) { window.setState(JSON.parse(\`${JSON.stringify(state)}\`)); }`);
-
- this.emit("state-changed", { name: "state-changed", sender: this, state: state });
- ContainerWindow.emit("state-changed", { name: "state-changed", windowId: this.id, state: state } );
- }
-
- protected attachListener(eventName: string, listener: (...args: any[]) => void): void {
- if (eventName === "beforeunload") {
- const win = this.window || window;
- if (win && this.id === this.container.getCurrentWindow().id) {
- win.addEventListener("beforeunload", listener);
- } else {
- throw new Error("Event handler for 'beforeunload' can only be added on current window");
- }
- } else {
- this.innerWindow.addListener(windowEventMap[eventName] || eventName, listener);
- }
- }
-
- protected detachListener(eventName: string, listener: (...args: any[]) => void): void {
- this.innerWindow.removeListener(windowEventMap[eventName] || eventName, listener);
- }
-
- public get nativeWindow(): Window {
- // For Electron we can only return in and for current renderer
- return this.window || window;
- }
-}
-
-/**
- * @augments MessageBus
- */
-export class ElectronMessageBus implements MessageBus {
- public ipc: any;
- private browserWindow: any;
-
- public constructor(ipc: any, browserWindow: any) {
- this.ipc = ipc;
- this.browserWindow = browserWindow;
- }
-
- public async subscribe(topic: string, listener: (event: any, message: T) => void, options?: MessageBusOptions) {
- const subscription = new MessageBusSubscription(topic, (event: any, message: any) => {
- listener({ topic: topic }, message);
- });
- this.ipc.on(topic, subscription.listener);
- return subscription;
- }
-
- public async unsubscribe(subscription: MessageBusSubscription) {
- const { topic, listener, options } = subscription;
- return this.ipc.removeListener(topic, listener);
- }
-
- public async publish(topic: string, message: T, options?: MessageBusOptions) {
- // If publisher is targeting a window, do not send to main
- if (!options?.name) {
- if ((this.ipc).send !== undefined) {
- // Publish to main from renderer (send is not available on ipcMain)
- (this.ipc).send(topic, message);
- } else {
- // we are in main so invoke listener directly
- this.ipc.listeners(topic).forEach(cb => cb({ topic }, message));
- }
- }
-
- // Broadcast to all windows or to the individual targeted window
- if (this.browserWindow?.getAllWindows) {
- for (const window of this.browserWindow.getAllWindows()) {
- if (!(options?.name) || options.name === window.name) {
- window.webContents.send(topic, message);
- }
- }
- }
- }
-}
-
-/**
- * @extends ContainerBase
- */
-export class ElectronContainer extends WebContainerBase {
- protected isRemote: boolean = true;
- protected electron: any;
- protected app: any;
- public browserWindow: any;
- protected tray: any;
- protected menu: any;
- public internalIpc: any;
- private windowManager: ElectronWindowManager;
- private nodeIntegration: boolean;
-
- /**
- * Gets or sets whether to replace the native web Notification API with a wrapper around showNotification.
- * @type {boolean}
- * @default true
- */
- public static replaceNotificationApi: boolean = true;
-
- public static readonly windowOptionsMap: PropertyMap = {
- taskbar: { target: "skipTaskbar", convert: (value: any, from: any, to: any) => { return !value; } },
- node: {
- target: "webPreferences", convert: (value: any, from: any, to: any) => {
- return Object.assign(to.webPreferences || {}, { nodeIntegration: value });
- }
- }
- };
-
- public windowOptionsMap: PropertyMap = ElectronContainer.windowOptionsMap;
-
- public constructor(electron?: any, ipc?: any, win?: any, options?: any) {
- super(win);
- this.hostType = "Electron";
-
- try {
- if (electron) {
- this.electron = electron;
- } else {
- // Check if we are in renderer or main by first accessing remote, if undefined switch to main
- this.electron = require("electron").remote;
- if (typeof this.electron === "undefined") {
- this.electron = require("electron");
- this.isRemote = false;
- }
- }
-
- this.app = this.electron.app;
- this.browserWindow = this.electron.BrowserWindow;
- this.tray = this.electron.Tray;
- this.menu = this.electron.Menu;
-
- this.internalIpc = ipc || ((this.isRemote) ? require("electron").ipcRenderer : this.electron.ipcMain);
- this.ipc = this.createMessageBus();
- this.nodeIntegration = null;
- this.setOptions(options);
- } catch (e) {
- // eslint-disable-next-line no-console
- console.error(e);
- }
-
- this.screen = new ElectronDisplayManager(this.electron);
-
- this.globalShortcut = new ElectronGlobalShortcutManager(this.electron);
- }
-
- public setOptions(options: any) {
- try {
- if ((!this.isRemote || (options && typeof options.isRemote !== "undefined" && !options.isRemote)) && !this.windowManager) {
- this.windowManager = new ElectronWindowManager(this.app, this.internalIpc, this.browserWindow);
-
- this.app.on("browser-window-created", (event, window) => {
- setImmediate(() => {
- Container.emit("window-created", { name: "window-created", windowId: window.webContents.id });
- ContainerWindow.emit("window-created", { name: "window-created", windowId: window.webContents.id });
- });
- });
- }
-
- if (options && options.autoStartOnLogin) {
- this.app.setLoginItemSettings({
- openAtLogin: options.autoStartOnLogin
- });
- }
-
- let replaceNotificationApi = ElectronContainer.replaceNotificationApi;
- if (options && typeof options.replaceNotificationApi !== "undefined") {
- replaceNotificationApi = options.replaceNotificationApi;
- }
-
- if (replaceNotificationApi) {
- this.registerNotificationsApi();
- }
-
- if (options && options.node) {
- this.nodeIntegration = options.node;
- }
- } catch (e) {
- // eslint-disable-next-line no-console
- console.error(e);
- }
- }
-
- public async getOptions(): Promise {
- try {
- return { autoStartOnLogin: await this.isAutoStartEnabledAtLogin() };
- } catch(error) {
- throw new Error("Error getting Container options. " + error);
- }
- }
-
- private async isAutoStartEnabledAtLogin(): Promise {
- const config = this.app.getLoginItemSettings();
- return config.openAtLogin;
- }
-
- protected createMessageBus() : MessageBus {
- return new ElectronMessageBus(this.internalIpc, this.browserWindow);
- }
-
- protected registerNotificationsApi() {
- if (typeof this.globalWindow !== "undefined" && this.globalWindow) {
- // Define owningContainer for closure to inner class
- // eslint-disable-next-line @typescript-eslint/no-this-alias
- const owningContainer: ElectronContainer = this;
-
- this.globalWindow["Notification"] = class ElectronNotification extends ContainerNotification {
- constructor(title: string, options?: NotificationOptions) {
- super(title, options);
- options["notification"] = this;
- owningContainer.showNotification(this.title, this.options);
- }
- };
- }
- }
-
- public async getInfo(): Promise {
- return `Electron/${this.electron.process.versions.electron} Chrome/${this.electron.process.versions.chrome}`;
- }
-
- public getMainWindow(): ContainerWindow {
- for (const window of this.browserWindow.getAllWindows()) {
- if (window[Container.windowOptionsPropertyKey] && window[Container.windowOptionsPropertyKey].main) {
- return this.wrapWindow(window);
- }
- }
-
- // No windows were marked as main so fallback to the first window created as being main
- const win = this.browserWindow.fromId(1);
- return win ? this.wrapWindow(win) : undefined;
- }
-
- public getCurrentWindow(): ContainerWindow {
- return this.wrapWindow(this.electron.getCurrentWindow());
- }
-
- protected getWindowOptions(options?: any): any {
- // If we have any container level node default, apply it here if no preference is specified in the options
- if (this.nodeIntegration != null && !("node" in options)) {
- options.node = this.nodeIntegration;
- }
-
- return ObjectTransform.transformProperties(options, this.windowOptionsMap);
- }
-
- public wrapWindow(containerWindow: any): ElectronContainerWindow {
- return new ElectronContainerWindow(containerWindow, this);
- }
-
- public async createWindow(url: string, options?: any): Promise {
- const newOptions = this.getWindowOptions(options);
- const electronWindow: any = new this.browserWindow(newOptions);
- const windowName = newOptions.name || Guid.newGuid();
-
- /*
- If we are in the renderer process, we need to ipc to the main process
- to set the window name. Otherwise it can not be seen from other renderer
- processes.
-
- This requires the main process to have desktopJS container listening
- either via desktopJS.resolveContainer() or new desktopJS.Electron.ElectronContainer();
- If it is not listening, this ipc call will hang indefinitely. If we are in the
- main process we can just directly set the name.
- */
- if (this.isRemote) {
- electronWindow["name"] = (this.internalIpc).sendSync(InternalMessageType.initialize, { id: electronWindow.id, name: windowName, options: newOptions });
- electronWindow[Container.windowOptionsPropertyKey] = options;
- } else {
- this.windowManager.initializeWindow(electronWindow, windowName, newOptions);
- }
-
- electronWindow.loadURL(this.ensureAbsoluteUrl(url));
-
- const newWindow = this.wrapWindow(electronWindow);
- this.emit("window-created", { sender: this, name: "window-created", window: newWindow, windowId: electronWindow.id, windowName: windowName });
- return newWindow;
- }
-
- public showNotification(title: string, options?: NotificationOptions) {
- const Notification = this.electron.Notification;
- const notify = new Notification(Object.assign(options || {}, { title: title }));
- if (options["onClick"]) {
- notify.addListener("click", options["onClick"]);
- }
- if (options["notification"]) {
- notify.once("show", () => notify.addListener("click", options["notification"]["onclick"]));
- }
-
- notify.show();
- }
-
- public addTrayIcon(details: TrayIconDetails, listener: () => void, menuItems?: MenuItem[]) {
- const tray = new this.tray(details.icon);
-
- if (details.text) {
- tray.setToolTip(details.text);
- }
-
- if (menuItems) {
- tray.setContextMenu(this.menu.buildFromTemplate(menuItems));
- }
-
- if (listener) {
- tray.on("click", listener);
- }
- }
-
- protected async closeAllWindows(excludeSelf?: boolean) {
- for (const window of this.browserWindow.getAllWindows()) {
- if (!excludeSelf || window !== this.electron.getCurrentWindow()) {
- window.close();
- }
- }
- }
-
- public async getAllWindows(): Promise {
- return this.browserWindow.getAllWindows().map(window => this.wrapWindow(window));
- }
-
- public async getWindowById(id: string): Promise {
- const win = this.browserWindow.fromId(id);
- return win ? this.wrapWindow(win) : null;
- }
-
- public async getWindowByName(name: string): Promise {
- const win = this.browserWindow.getAllWindows().find(window => window.name === name);
- return win ? this.wrapWindow(win) : null;
- }
-
- public async buildLayout() {
- const layout = new PersistedWindowLayout();
- const mainWindow = this.getMainWindow().innerWindow;
- const promises: Promise[] = [];
-
- const windows = await this.getAllWindows();
- windows.forEach(window => {
- const options = window.innerWindow[Container.windowOptionsPropertyKey];
- if (options && "persist" in options && !options.persist) {
- return;
- }
-
- promises.push((async () => {
- layout.windows.push(
- {
- id: window.id,
- name: window.name,
- url: window.innerWindow.webContents.getURL(),
- main: (mainWindow === window.innerWindow),
- state: await window.getState(),
- options: options,
- bounds: window.innerWindow.getBounds(),
- group: (await window.getGroup()).map(win => win.id)
- }
- );
- })());
- });
-
- await Promise.all(promises);
- return layout;
- }
-}
-
-export class ElectronWindowManager {
- private app: any;
- private ipc: NodeJS.EventEmitter;
- private browserWindow: any;
-
- private lastBounds: Map = new Map(); // BrowserWindow.id -> Rectangle
- private ignoredWindows: number[] = []; // Array of BrowserWindow.id
-
- public constructor(app?: any, ipc?: any, browserWindow?: any) {
- this.app = app || require("electron").app;
- this.ipc = ipc || require("electron").ipcMain;
- this.browserWindow = browserWindow || require("electron").BrowserWindow;
-
- this.ipc.on(InternalMessageType.initialize, (event: any, message: any) => {
- const { id, name, options } = message;
- const win = this.browserWindow.fromId(id);
- this.initializeWindow(win, name, options);
- event.returnValue = name;
- });
-
- this.ipc.on(InternalMessageType.joinGroup, (event: any, message: any) => {
- const { "source": sourceId, "target": targetId } = message;
- const source = this.browserWindow.fromId(sourceId);
- const target = this.browserWindow.fromId(targetId);
-
- this.groupWindows(target, source);
- });
-
- this.ipc.on(InternalMessageType.leaveGroup, (event: any, message: any) => {
- const { "source": sourceId } = message;
- this.ungroupWindows(this.browserWindow.fromId(sourceId));
- });
-
- this.ipc.on(InternalMessageType.getGroup, (event: any, message: any) => {
- const { "source": sourceId } = message;
- event.returnValue = this.getGroup(this.browserWindow.fromId(sourceId));
- });
-
- this.ipc.on(InternalMessageType.getOptions, (event: any, message: any) => {
- const { "source": sourceId } = message;
- event.returnValue = this.browserWindow.fromId(sourceId)[Container.windowOptionsPropertyKey];
- });
- }
-
- public initializeWindow(win: any, name: string, options: any) {
- win.name = name;
- win[Container.windowOptionsPropertyKey] = options;
-
- if (options && options.main && (!("quitOnClose" in options) || options.quitOnClose)) {
- win.on("closed", () => {
- this.app.quit();
- });
- }
- }
-
- private registerWindowEvents(win: any) {
- if (win && !win.moveHandler) {
- this.lastBounds.set(win.id, win.getBounds());
- win.moveHandler = (e) => this.handleMove(win);
- win.on("move", win.moveHandler);
- }
- }
-
- private unregisterWindowEvents(win: any) {
- if (win && win.moveHandler) {
- this.lastBounds.delete(win.id);
- win.removeListener("move", win.moveHandler);
- delete win.moveHandler;
- }
- }
-
- public getGroup(window: any): any[] {
- return (window.group)
- ? this.browserWindow.getAllWindows().filter(win => { return (win.group === window.group); }).map(win => win.id)
- : [];
- }
-
- public groupWindows(target: any, ...windows: any[]) {
- for (const win of windows) {
- win.group = target.group || (target.group = Guid.newGuid());
- this.registerWindowEvents(win);
-
- ContainerWindow.emit("window-joinGroup", { name: "window-joinGroup", windowId: win.id, targetWindowId: target.id });
- }
-
- this.registerWindowEvents(target);
- }
-
- public ungroupWindows(...windows: any[]) {
- // Unhook and clear group of all provided windows
- for (const win of windows) {
- this.unregisterWindowEvents(win);
- win.group = null;
- ContainerWindow.emit("window-leaveGroup", { name: "window-leaveGroup", windowId: win.id });
- }
-
- // Group all windows by group and for any group consisting of one window unhook and clear the group
- this.groupBy(this.browserWindow.getAllWindows(), "group")?.filter(group => group.key && group.values && group.values.length === 1).forEach(group => {
- for (const win of group.values) {
- this.unregisterWindowEvents(win);
- win.group = null;
- }
- });
- }
-
- private handleMove(win: any): void {
- // Grab the last bounds we had and the current and then store the current
- const oldBounds = this.lastBounds.get(win.id);
- const newBounds = win.getBounds();
- this.lastBounds.set(win.id, newBounds);
-
- // If the height or width change this is a resize and we should just exit out
- if (oldBounds.width !== newBounds.width || oldBounds.height !== newBounds.height) {
- return;
- }
-
- // Prevent cycles
- if (this.ignoredWindows.indexOf(win.id) >= 0) {
- return;
- }
-
- if (win.group) {
- // Get all windows other windows in same group
- const groupedWindows = this.browserWindow.getAllWindows().filter(window => { return (window.group === win.group && window.id !== win.id); });
-
- if (groupedWindows && groupedWindows.length > 0) {
- const diff = { x: oldBounds.x - newBounds.x, y: oldBounds.y - newBounds.y };
-
- groupedWindows.forEach(groupedWindow => {
- const targetBounds = groupedWindow.getBounds();
- this.ignoredWindows.push(groupedWindow.id);
- groupedWindow.setBounds({ x: targetBounds.x - diff.x, y: targetBounds.y - diff.y, width: targetBounds.width, height: targetBounds.height }, true);
- this.ignoredWindows.splice(this.ignoredWindows.indexOf(groupedWindow.id), 1);
- });
- }
- }
- }
-
- private groupBy(array: T[], groupBy: any): { key: any, values: T[] }[] {
- return array?.reduce((accumulator, current) => {
- const key = groupBy instanceof Function ? groupBy(current) : current[groupBy];
- const group = accumulator.find((r) => r && r.key === key);
-
- if (group) {
- group.values.push(current);
- } else {
- accumulator.push({ key: key, values: [current] });
- }
-
- return accumulator;
- }, []);
- }
-}
-
-/** @private */
-class ElectronDisplayManager implements ScreenManager {
- private readonly electron: any;
-
- public constructor(electron: any) {
- this.electron = electron;
- }
-
- createDisplay(monitorDetails: any) {
- const display = new Display();
- display.id = monitorDetails.id;
- display.scaleFactor = monitorDetails.scaleFactor;
-
- display.bounds = new Rectangle(monitorDetails.bounds.x,
- monitorDetails.bounds.y,
- monitorDetails.bounds.width,
- monitorDetails.bounds.height);
-
- display.workArea = new Rectangle(monitorDetails.workArea.x,
- monitorDetails.workArea.y,
- monitorDetails.workArea.width,
- monitorDetails.workArea.height);
-
- return display;
- }
-
- public async getPrimaryDisplay() {
- return this.createDisplay(this.electron.screen.getPrimaryDisplay());
- }
-
- public async getAllDisplays() {
- return this.electron.screen.getAllDisplays().map(this.createDisplay.bind(this));
- }
-
- public async getMousePosition(): Promise {
- return this.electron.screen.getCursorScreenPoint();
- }
-}
-
-/** @private */
-class ElectronGlobalShortcutManager extends GlobalShortcutManager {
- private readonly electron: any;
-
- public constructor(electron: any) {
- super();
- this.electron = electron;
- }
-
- public async register(shortcut: string, callback: () => void) {
- this.electron.globalShortcut.register(shortcut, callback);
- }
-
- public async isRegistered(shortcut: string): Promise {
- return this.electron.globalShortcut.isRegistered(shortcut);
- }
-
- public async unregister(shortcut: string) {
- this.electron.globalShortcut.unregister(shortcut);
- }
-
- public async unregisterAll() {
- this.electron.globalShortcut.unregisterAll();
- }
-}
\ No newline at end of file
diff --git a/packages/desktopjs-electron/tests/electron.spec.ts b/packages/desktopjs-electron/tests/electron.spec.ts
deleted file mode 100644
index 9012846d..00000000
--- a/packages/desktopjs-electron/tests/electron.spec.ts
+++ /dev/null
@@ -1,1274 +0,0 @@
-/*
- * Morgan Stanley makes this available to you under the Apache License,
- * Version 2.0 (the "License"). You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * See the NOTICE file distributed with this work for additional information
- * regarding copyright ownership. Unless required by applicable law or agreed
- * to in writing, software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions
- * and limitations under the License.
- */
-
-/* eslint-disable @typescript-eslint/no-empty-function */
-import { ElectronContainer, ElectronContainerWindow, ElectronMessageBus, ElectronWindowManager } from "../src/electron";
-import { ContainerWindow, Container } from "@morgan-stanley/desktopjs";
-
-class MockEventEmitter {
- private eventListeners: { [key: string]: any[] } = {};
-
- public addListener(eventName: string, listener: any): void {
- (this.eventListeners[eventName] = this.eventListeners[eventName] || []).push(listener);
- }
-
- public removeListener(eventName: string, listener: any): void { }
-
- public on(eventName: string, listener: any): void {
- this.addListener(eventName, listener);
- }
-
- public listeners(eventName: string): ((event: any) => void)[] {
- return this.eventListeners[eventName] || [];
- }
-
- public emit(eventName: string, ...args: any[]): void {
- const listeners = this.listeners(eventName);
- for (const listener of listeners) {
- listener(...args);
- }
- }
-}
-
-class MockIpc extends MockEventEmitter {
- public send(channel: string, ...args: any[]): void { }
- public sendSync(channel: string, ...args: any[]): any { }
- public sendTo(windowId: number, channel: string, ...args: any[]): void { }
-}
-
-class MockBrowserWindow extends MockEventEmitter {
- public static fromId(id: number): MockBrowserWindow {
- return new MockBrowserWindow();
- }
-
- public id: number = Math.floor(Math.random() * 1000000);
- public name: string = "MockBrowserWindow";
- public group: string | null = null; // Add group property for window grouping tests
-
- public loadURL(url: string, options?: any): Promise { return Promise.resolve(); }
- public focus(): void { }
- public show(): void { }
- public hide(): void { }
- public close(): void { }
- public minimize(): void { }
- public maximize(): void { }
- public restore(): void { }
- public isVisible(): boolean { return true; }
- public capturePage(): Promise { return Promise.resolve(); }
- public getBounds(): any { return { x: 0, y: 1, width: 2, height: 3 }; }
- public setBounds(bounds: any): void {
- // Emit move event when bounds are set
- this.emit("move", null);
- }
- public flashFrame(flag: boolean): void { }
- public stopFlashing(): void { }
- public getParentWindow(): any { return null; }
- public setParentWindow(parent: any): void { }
- public moveTop(): void { }
- public getNativeWindow(): any { return {}; }
- public webContents: any = {
- executeJavaScript: (code: string, userGesture?: boolean, callback?: (result: any) => void) => {
- if (callback) {
- callback(null);
- }
- return Promise.resolve();
- },
- on: (event: string, listener: any) => { },
- send: (channel: string, ...args: any[]) => { }
- };
-}
-
-describe("ElectronContainerWindow", () => {
- let innerWindow: any;
- let win: any;
-
- beforeEach(() => {
- innerWindow = new MockBrowserWindow();
-
- // Create a mock container
- const container = {
- electron: {
- ipcRenderer: {
- sendSync: jest.fn().mockReturnValue({ name: "name" })
- }
- }
- };
-
- // Create the window with our mocks
- win = {
- innerWindow: innerWindow,
- id: "id",
- name: "name",
- container: container,
- load: jest.fn(),
- focus: jest.fn(),
- show: jest.fn(),
- hide: jest.fn(),
- close: jest.fn(),
- minimize: jest.fn(),
- maximize: jest.fn(),
- restore: jest.fn(),
- isShowing: jest.fn(),
- getSnapshot: jest.fn(),
- getBounds: jest.fn().mockReturnValue({ x: 0, y: 0, width: 0, height: 0 }),
- setBounds: jest.fn(),
- flash: jest.fn(),
- stopFlashing: jest.fn(),
- getParent: jest.fn(),
- setParent: jest.fn(),
- getOptions: jest.fn(),
- getState: jest.fn(),
- setState: jest.fn(),
- moveTop: jest.fn(),
- bringToFront: () => {
- win.moveTop();
- },
- addListener: jest.fn(),
- removeListener: jest.fn(),
- nativeWindow: innerWindow
- };
- });
-
- it("Wrapped window is retrievable", () => {
- expect(win.innerWindow).toBeDefined();
- });
-
- it("id returns underlying id", () => {
- expect(win.id).toEqual("id");
- });
-
- it("name returns underlying name", () => {
- expect(win.name).toEqual("name");
- });
-
- it("bringToFront invokes underlying moveTop", () => {
- win.bringToFront();
- expect(win.moveTop).toHaveBeenCalled();
- });
-
- describe("Window members", () => {
- it.skip("load", () => {
- // Skip this test for now due to issues with the mock implementation
- });
-
- it("load with options", () => {
- win.load = (url, options) => {
- innerWindow.loadURL(url, options);
- };
-
- jest.spyOn(innerWindow, "loadURL");
- win.load("url", { a: "value" });
- expect(innerWindow.loadURL).toHaveBeenCalledWith("url", { a: "value" });
- });
-
- it("focus", () => {
- win.focus = () => {
- innerWindow.focus();
- };
-
- jest.spyOn(innerWindow, "focus");
- win.focus();
- expect(innerWindow.focus).toHaveBeenCalled();
- });
-
- it("show", () => {
- win.show = () => {
- innerWindow.show();
- };
-
- jest.spyOn(innerWindow, "show");
- win.show();
- expect(innerWindow.show).toHaveBeenCalled();
- });
-
- it("hide", () => {
- win.hide = () => {
- innerWindow.hide();
- };
-
- jest.spyOn(innerWindow, "hide");
- win.hide();
- expect(innerWindow.hide).toHaveBeenCalled();
- });
-
- it("close", () => {
- win.close = () => {
- innerWindow.close();
- };
-
- jest.spyOn(innerWindow, "close");
- win.close();
- expect(innerWindow.close).toHaveBeenCalled();
- });
-
- it("minimize", () => {
- win.minimize = () => {
- innerWindow.minimize();
- };
-
- jest.spyOn(innerWindow, "minimize");
- win.minimize();
- expect(innerWindow.minimize).toHaveBeenCalled();
- });
-
- it("maximize", () => {
- win.maximize = () => {
- innerWindow.maximize();
- };
-
- jest.spyOn(innerWindow, "maximize");
- win.maximize();
- expect(innerWindow.maximize).toHaveBeenCalled();
- });
-
- it("restore", () => {
- win.restore = () => {
- innerWindow.restore();
- };
-
- jest.spyOn(innerWindow, "restore");
- win.restore();
- expect(innerWindow.restore).toHaveBeenCalled();
- });
-
- it("isShowing", () => {
- win.isShowing = () => {
- return innerWindow.isVisible();
- };
-
- jest.spyOn(innerWindow, "isVisible").mockReturnValue(true);
- const showing = win.isShowing();
- expect(showing).toBe(true);
- expect(innerWindow.isVisible).toHaveBeenCalled();
- });
-
- it("getSnapshot", () => {
- win.getSnapshot = (callback) => {
- innerWindow.capturePage(callback);
- };
-
- jest.spyOn(innerWindow, "capturePage").mockImplementation(callback => {
- callback("snapshot");
- });
- win.getSnapshot(snapshot => {
- expect(snapshot).toEqual("snapshot");
- });
- expect(innerWindow.capturePage).toHaveBeenCalled();
- });
-
- it("getBounds retrieves underlying window position", () => {
- win.getBounds = () => {
- return innerWindow.getBounds();
- };
-
- jest.spyOn(innerWindow, "getBounds").mockReturnValue({ x: 1, y: 2, width: 3, height: 4 });
- const bounds = win.getBounds();
- expect(bounds).toEqual({ x: 1, y: 2, width: 3, height: 4 });
- expect(innerWindow.getBounds).toHaveBeenCalled();
- });
-
- it("setBounds sets underlying window position", () => {
- win.setBounds = (bounds) => {
- innerWindow.setBounds(bounds);
- };
-
- jest.spyOn(innerWindow, "setBounds");
- win.setBounds({ x: 1, y: 2, width: 3, height: 4 });
- expect(innerWindow.setBounds).toHaveBeenCalledWith({ x: 1, y: 2, width: 3, height: 4 });
- });
-
- it("flash enable invokes underlying flash", () => {
- win.flash = (enable) => {
- innerWindow.flashFrame(enable);
- };
-
- jest.spyOn(innerWindow, "flashFrame");
- win.flash(true);
- expect(innerWindow.flashFrame).toHaveBeenCalledWith(true);
- });
-
- it("flash disable invokes underlying stopFlashing", () => {
- win.flash = (enable) => {
- innerWindow.flashFrame(enable);
- };
-
- jest.spyOn(innerWindow, "flashFrame");
- win.flash(false);
- expect(innerWindow.flashFrame).toHaveBeenCalledWith(false);
- });
-
- it("getParent calls underlying getParentWindow", () => {
- win.getParent = () => {
- return innerWindow.getParentWindow();
- };
-
- const parent = new MockBrowserWindow();
- jest.spyOn(innerWindow, "getParentWindow").mockReturnValue(parent);
- win.getParent();
- expect(innerWindow.getParentWindow).toHaveBeenCalled();
- });
-
- it("setParent calls underlying setParentWindow", () => {
- win.setParent = (parent: any) => {
- innerWindow.setParentWindow(parent?.innerWindow || null);
- };
-
- jest.spyOn(innerWindow, "setParentWindow");
- const parent = { innerWindow: new MockBrowserWindow() };
- win.setParent(parent as any);
- expect(innerWindow.setParentWindow).toHaveBeenCalledWith(parent.innerWindow);
- });
-
- it("getOptions sends synchronous ipc message", () => {
- // Create a proper mock for the container
- win.container = {
- electron: {
- ipcRenderer: {
- sendSync: jest.fn().mockReturnValue({ a: "value" })
- }
- }
- };
-
- // Update the mock implementation to call sendSync directly
- win.getOptions = () => {
- return win.container.electron.ipcRenderer.sendSync("desktopJS.window-getOptions", { id: innerWindow.id });
- };
-
- const options = win.getOptions();
- expect(options).toEqual({ a: "value" });
- expect(win.container.electron.ipcRenderer.sendSync).toHaveBeenCalledWith("desktopJS.window-getOptions", { id: innerWindow.id });
- });
-
- it("removeListener calls underlying Electron window removeListener", () => {
- // Update the mock implementation to call removeListener directly
- win.removeListener = (event, listener) => {
- innerWindow.removeListener(event, listener);
- };
-
- jest.spyOn(innerWindow, "removeListener");
- const listener = () => {};
- win.removeListener("event", listener);
- expect(innerWindow.removeListener).toHaveBeenCalledWith("event", listener);
- });
-
- it("nativeWindow returns window", () => {
- expect(win.nativeWindow).toBe(innerWindow);
- });
- });
-});
-
-describe("ElectronContainer", () => {
- let electron: any;
- let container: any;
- const globalWindow: any = {};
-
- beforeEach(() => {
- electron = {
- BrowserWindow: {
- getAllWindows(): MockBrowserWindow[] { return []; },
- fromId(id: string): MockBrowserWindow { return null; }
- },
- app: {
- getLoginItemSettings: jest.fn().mockReturnValue({ openAtLogin: true }),
- setLoginItemSettings: jest.fn()
- },
- ipcMain: {
- on: jest.fn()
- },
- ipcRenderer: {
- sendSync: jest.fn(),
- send: jest.fn()
- },
- remote: {
- app: {
- getLoginItemSettings: jest.fn().mockReturnValue({ openAtLogin: true }),
- setLoginItemSettings: jest.fn()
- }
- },
- dialog: {
- showMessageBox: jest.fn()
- },
- Tray: jest.fn(),
- Menu: jest.fn(),
- nativeImage: {
- createFromDataURL: jest.fn()
- },
- webContents: {
- getFocusedWebContents: jest.fn()
- },
- require: (type: string) => { return {} },
- getCurrentWindow: () => { return new MockBrowserWindow(); },
- process: { versions: { electron: "1", chrome: "2" } },
- };
-
- // Create a mock container instead of a real one
- container = {
- hostType: "Electron",
- electron: electron,
- app: electron.app,
- browserWindow: electron.BrowserWindow,
- internalIpc: new MockIpc(),
-
- getInfo: async () => {
- return "Electron/1 Chrome/2";
- },
-
- getMainWindow: () => {
- const win = new MockBrowserWindow();
- win.name = "main";
- return new ElectronContainerWindow(win);
- },
-
- getCurrentWindow: () => {
- return new ElectronContainerWindow(electron.getCurrentWindow());
- },
-
- createWindow: jest.fn().mockImplementation((url, options) => {
- const win = new MockBrowserWindow();
- return new ElectronContainerWindow(win);
- }),
-
- addTrayIcon: jest.fn(),
-
- showNotification: jest.fn(),
-
- getOptions: async () => {
- return { autoStartOnLogin: true };
- },
-
- setOptions: jest.fn(),
-
- saveLayoutToStorage: jest.fn(),
-
- // eslint-disable-next-line no-console
- console: { error: jest.fn() }
- };
- });
-
- it("hostType is Electron", () => {
- expect(container.hostType).toEqual("Electron");
- });
-
- it("getInfo invokes underlying version info", async () => {
- const info = await container.getInfo();
- expect(info).toEqual("Electron/1 Chrome/2");
- });
-
- it("error during creation", () => {
- // eslint-disable-next-line no-console
- console.error = jest.fn();
-
- // Create a mock ElectronContainer constructor that throws an error
- const mockElectronContainer = {
- constructor: () => {
- throw new Error("Test error");
- }
- };
-
- // Call the error handler directly
- try {
- mockElectronContainer.constructor();
- } catch (e) {
- // eslint-disable-next-line no-console
- console.error(e);
- }
-
- expect(console.error).toHaveBeenCalledTimes(1);
- });
-
- it("electron members are copied", () => {
- expect(container.electron).toBeDefined();
- expect(container.app).toBeDefined();
- expect(container.browserWindow).toBeDefined();
- });
-
- it.skip("getMainWindow returns wrapped window marked as main", () => {});
-
- it.skip("getMainWindow with no defined main returns first wrapped window", () => {});
-
- it.skip("getCurrentWindow returns wrapped inner getCurrentWindow", () => {});
-
- it.skip("createWindow", async () => {});
-
- it.skip("createWindow fires window-created", async () => {});
-
- it.skip("addTrayIcon", () => {});
-
- describe("notifications", () => {
- beforeEach(() => {
- // Mock Notification in the global window
- globalWindow.Notification = {
- requestPermission: jest.fn().mockImplementation(callback => {
- callback("granted");
- return Promise.resolve("granted");
- })
- };
-
- // Mock the electron Notification
- electron.Notification = jest.fn().mockImplementation(() => {
- return {
- show: jest.fn()
- };
- });
-
- // Mock the container showNotification method
- container.showNotification = jest.fn();
- });
-
- it("showNotification delegates to electron notification", () => {
- const notificationOptions = { onClick: () => {}, notification: {} };
- container.showNotification("title", notificationOptions);
- expect(container.showNotification).toHaveBeenCalledWith("title", notificationOptions);
- });
-
- it("requestPermission granted", async () => {
- await globalWindow.Notification.requestPermission(permission => {
- expect(permission).toEqual("granted");
- });
- expect(globalWindow.Notification.requestPermission).toHaveBeenCalled();
- });
-
- it("notification api delegates to showNotification", () => {
- // Mock the Notification constructor
- globalWindow.Notification = jest.fn();
-
- // Call the constructor
- new globalWindow.Notification("title", { body: "Test message" });
-
- // Verify it was called with the right arguments
- expect(globalWindow.Notification).toHaveBeenCalledWith("title", { body: "Test message" });
- });
- });
-
- describe("LoginItemSettings", () => {
- beforeEach(() => {
- // Reset the mocks for each test
- electron.app.setLoginItemSettings = jest.fn();
- electron.app.getLoginItemSettings = jest.fn().mockReturnValue({ openAtLogin: true });
-
- // Update container methods for LoginItemSettings tests
- container.setOptions = (options) => {
- try {
- if (options && options.autoStartOnLogin !== undefined) {
- electron.app.setLoginItemSettings({ openAtLogin: options.autoStartOnLogin });
- }
- } catch (e) {
- // eslint-disable-next-line no-console
- console.error(e);
- }
- };
-
- container.getOptions = async () => {
- try {
- const loginSettings = electron.app.getLoginItemSettings();
- return { autoStartOnLogin: loginSettings.openAtLogin };
- } catch (e) {
- throw new Error(`Error getting Container options. Error: ${e.message}`);
- }
- };
- });
-
- it("options autoStartOnLogin to setLoginItemSettings", () => {
- electron.app.setLoginItemSettings = jest.fn();
- // Create a new container with options
- const newContainer = {
- app: electron.app,
- setOptions: container.setOptions
- };
-
- // Call setOptions directly to simulate constructor behavior
- newContainer.setOptions({ autoStartOnLogin: true });
- expect(electron.app.setLoginItemSettings).toHaveBeenCalledWith({ openAtLogin: true });
- });
-
- it("options missing autoStartOnLogin does not invoke setLoginItemSettings", () => {
- electron.app.setLoginItemSettings = jest.fn();
- // Create a new container with options
- const newContainer = {
- app: electron.app,
- setOptions: container.setOptions
- };
-
- // Call setOptions directly to simulate constructor behavior
- newContainer.setOptions({});
- expect(electron.app.setLoginItemSettings).not.toHaveBeenCalled();
- });
-
- it("setOptions allows the auto startup settings to be turned on", () => {
- electron.app.setLoginItemSettings = jest.fn();
- container.setOptions({ autoStartOnLogin: true });
- expect(electron.app.setLoginItemSettings).toHaveBeenCalledWith({ openAtLogin: true });
- });
-
- it("setOptions errors out on setLoginItemSettings", () => {
- // eslint-disable-next-line no-console
- console.error = jest.fn();
- electron.app.setLoginItemSettings = jest.fn().mockImplementation(() => {
- throw new Error("something went wrong");
- });
- container.setOptions({ autoStartOnLogin: true });
- expect(electron.app.setLoginItemSettings).toHaveBeenCalledWith({ openAtLogin: true });
- expect(console.error).toHaveBeenCalledTimes(1);
- });
-
- it("getOptions returns autoStartOnLogin status", async () => {
- electron.app.getLoginItemSettings = jest.fn().mockReturnValue({ openAtLogin: true });
- const result = await container.getOptions();
- expect(electron.app.getLoginItemSettings).toHaveBeenCalled();
- expect(result.autoStartOnLogin).toEqual(true);
- });
-
- it("getOptions error out while fetching auto start info", async () => {
- electron.app.getLoginItemSettings = jest.fn().mockImplementation(() => {
- throw new Error("something went wrong");
- });
- await expect(container.getOptions()).rejects.toThrow("Error getting Container options. Error: something went wrong");
- expect(electron.app.getLoginItemSettings).toHaveBeenCalled();
- });
- });
-
- describe("window management", () => {
- let win1: MockBrowserWindow;
- let win2: MockBrowserWindow;
-
- beforeEach(() => {
- win1 = new MockBrowserWindow();
- win2 = new MockBrowserWindow();
-
- jest.spyOn(win1, "close").mockImplementation(() => {});
- jest.spyOn(win2, "close").mockImplementation(() => {});
-
- electron.BrowserWindow = {
- getAllWindows(): MockBrowserWindow[] { return [win1, win2]; },
- fromId(): any { return null; }
- };
-
- // Skip creating a real container and mock the methods we need
- container = {
- getAllWindows: async () => {
- return electron.BrowserWindow.getAllWindows().map(win => ({ innerWindow: win }));
- },
- getWindowById: async (id: string) => {
- return { innerWindow: new MockBrowserWindow() };
- },
- getWindowByName: async (name: string) => {
- return name === "existingWindow" ? { innerWindow: new MockBrowserWindow() } : null;
- },
- closeAllWindows: async (skipSelf: boolean = false) => {
- const windows = electron.BrowserWindow.getAllWindows();
- for (let i = skipSelf ? 1 : 0; i < windows.length; i++) {
- windows[i].close();
- }
- },
- saveLayout: async (name: string) => {
- const layout = { windows: [{ name: "win1" }] };
- container.saveLayoutToStorage(name, layout);
- return layout;
- },
- saveLayoutToStorage: jest.fn(),
- buildLayout: () => {
- return {
- windows: [
- { name: "win1", id: "1", url: "url", bounds: { x: 0, y: 1, width: 2, height: 3 } }
- ]
- };
- }
- };
- });
-
- it("getAllWindows returns wrapped native windows", async () => {
- const wins = await container.getAllWindows();
- expect(wins).not.toBeNull();
- expect(wins.length).toEqual(2);
- expect(wins[0].innerWindow).toBeDefined();
- });
-
- describe("getWindow", () => {
- it("getWindowById returns wrapped window", async () => {
- const win = await container.getWindowById("1");
- expect(win).toBeDefined();
- });
-
- it("getWindowById with unknown id returns null", async () => {
- // Override the mock implementation for this test
- container.getWindowById = async () => null;
- const win = await container.getWindowById("DoesNotExist");
- expect(win).toBeNull();
- });
-
- it("getWindowByName returns wrapped window", async () => {
- const win = await container.getWindowByName("existingWindow");
- expect(win).toBeDefined();
- });
-
- it("getWindowByName with unknown name returns null", async () => {
- const win = await container.getWindowByName("DoesNotExist");
- expect(win).toBeNull();
- });
- });
-
- it("closeAllWindows excluding self skips current window", async () => {
- await container.closeAllWindows(true);
-
- expect(win1.close).not.toHaveBeenCalled();
- expect(win2.close).toHaveBeenCalled();
- });
-
- it("closeAllWindows including self closes all", async () => {
- await container.closeAllWindows(false);
-
- expect(win1.close).toHaveBeenCalled();
- expect(win2.close).toHaveBeenCalled();
- });
-
- it("saveLayout invokes underlying saveLayoutToStorage", async () => {
- await container.saveLayout("Test");
- expect(container.saveLayoutToStorage).toHaveBeenCalledWith("Test", expect.any(Object));
- });
-
- it("buildLayout skips windows with persist false", () => {
- const layout = container.buildLayout();
- expect(layout.windows.length).toEqual(1);
- expect(layout.windows[0].name).toEqual("win1");
- });
- });
-});
-
-describe("ElectronMessageBus", () => {
- it("subscribe invokes underlying subscribe", async () => {
- const mockIpc = new MockIpc();
- const callback = jest.fn();
- const bus = new ElectronMessageBus(mockIpc);
-
- jest.spyOn(mockIpc, "on").mockImplementation(() => {});
- await bus.subscribe("topic", callback);
-
- expect(mockIpc.on).toHaveBeenCalledWith("topic", expect.any(Function));
- });
-
- it("subscribe listener attached", async () => {
- const mockIpc = new MockIpc();
- const callback = jest.fn();
- const bus = new ElectronMessageBus(mockIpc);
-
- // Mock the on method to capture the handler
- jest.spyOn(mockIpc, "on").mockImplementation((topic, handler) => {
- // Manually call the handler to simulate an event
- handler({}, "test-message");
- });
-
- await bus.subscribe("topic", callback);
-
- // The callback should be called with an event object and the message
- expect(callback).toHaveBeenCalledWith({ topic: "topic" }, "test-message");
- });
-
- it("unsubscribe invokes underlying unsubscribe", async () => {
- const mockIpc = new MockIpc();
- const callback = jest.fn();
- const bus = new ElectronMessageBus(mockIpc);
-
- // First subscribe to get a subscription object
- jest.spyOn(mockIpc, "on").mockImplementation(() => {});
- const subscription = await bus.subscribe("topic", callback);
-
- // Then test unsubscribe
- jest.spyOn(mockIpc, "removeListener").mockImplementation(() => {});
- await bus.unsubscribe(subscription);
-
- expect(mockIpc.removeListener).toHaveBeenCalledWith("topic", expect.any(Function));
- });
-
- it("publish invokes underling publish", async () => {
- const mockIpc = new MockIpc();
- const message = { data: "data" };
- jest.spyOn(mockIpc, "send").mockImplementation(() => {});
- const bus = new ElectronMessageBus(mockIpc);
- await bus.publish("topic", message);
- expect(mockIpc.send).toHaveBeenCalledWith("topic", message);
- });
-
- it("publish in main invokes callback in main", async () => {
- // Skip this test since it's difficult to mock properly
- // The actual functionality is tested in the integration tests
- });
-
- it("publish with optional name invokes underling send", async () => {
- const mockIpc = new MockIpc();
- const message = { data: "data" };
- jest.spyOn(mockIpc, "send").mockImplementation(() => {});
- const bus = new ElectronMessageBus(mockIpc);
-
- // Create a mock options object with a name property
- const options = { name: "name" };
-
- await bus.publish("topic", message, options);
-
- // The implementation should use the name from options
- // We're not testing the actual concatenation logic here since that's implementation-specific
- // Just verifying that send was called
- expect(mockIpc.send).not.toHaveBeenCalled(); // Should not call send when targeting a specific window
- });
-});
-
-describe("ElectronWindowManager", () => {
- describe("groupWindows", () => {
- let target: MockBrowserWindow;
- let win1: MockBrowserWindow;
- let win2: MockBrowserWindow;
- let mgr: any;
-
- beforeEach(() => {
- target = new MockBrowserWindow();
- win1 = new MockBrowserWindow();
- win2 = new MockBrowserWindow();
-
- target.id = 1;
- win1.id = 2;
- win2.id = 3;
-
- // Set group to null initially
- target.group = null;
- win1.group = null;
- win2.group = null;
-
- jest.spyOn(target, "on").mockImplementation(() => {});
- jest.spyOn(win1, "on").mockImplementation(() => {});
- jest.spyOn(win2, "on").mockImplementation(() => {});
-
- mgr = {
- groupWindows: (target: any, ...windows: any[]) => {
- const group = target.group || `group-${target.id}`;
- target.group = group;
-
- for (const win of windows) {
- win.group = group;
- }
-
- // Attach move handlers
- target.on("move", () => {});
- for (const win of windows) {
- win.on("move", () => {});
- }
- },
- browserWindow: {
- getAllWindows: () => [target, win1, win2]
- }
- };
- });
-
- it("assigns group property", () => {
- expect(target.group).toBeNull();
- expect(win1.group).toBeNull();
- expect(win2.group).toBeNull();
-
- mgr.groupWindows(target, win1, win2);
-
- expect(target.group).toBe(`group-${target.id}`);
- expect(win1.group).toBe(target.group);
- expect(win2.group).toBe(target.group);
- });
-
- it("copies from existing target group", () => {
- target.group = "existing-group";
- mgr.groupWindows(target, win1, win2);
-
- expect(win1.group).toBe("existing-group");
- expect(win2.group).toBe("existing-group");
- });
-
- it("attaches move handler", () => {
- mgr.groupWindows(target, win1, win2);
-
- expect(target.on).toHaveBeenCalledWith("move", expect.any(Function));
- expect(win1.on).toHaveBeenCalledWith("move", expect.any(Function));
- expect(win2.on).toHaveBeenCalledWith("move", expect.any(Function));
- });
-
- it("resize is ignored", () => {
- mgr.groupWindows(target, win1, win2);
- jest.spyOn(mgr.browserWindow, "getAllWindows").mockReturnValue([]);
- win1.setBounds({ x: 0, y: 1, width: 10, height: 10});
-
- expect(mgr.browserWindow.getAllWindows).toHaveBeenCalledTimes(0);
- });
-
- it("move updates other grouped window bounds", () => {
- // Mock the getBounds and setBounds methods
- const mockBounds = { x: 0, y: 1, width: 2, height: 3 };
-
- jest.spyOn(target, "getBounds").mockReturnValue({...mockBounds});
- jest.spyOn(win1, "getBounds").mockReturnValue({...mockBounds});
- jest.spyOn(win2, "getBounds").mockReturnValue({...mockBounds});
-
- jest.spyOn(target, "setBounds").mockImplementation((bounds) => {
- Object.assign(mockBounds, bounds);
- target.emit("move", null);
- });
-
- jest.spyOn(win2, "setBounds").mockImplementation((bounds) => {
- Object.assign(mockBounds, bounds);
- });
-
- mgr.groupWindows(target, win1, win2);
-
- // Update win1's bounds to trigger the move event
- const newBounds = { x: 10, y: 1, width: 2, height: 3 };
- win1.setBounds(newBounds);
-
- // Since we're mocking, we need to manually update the bounds
- Object.assign(mockBounds, newBounds);
-
- // Now check that the other windows' bounds were updated
- expect(mockBounds.x).toEqual(10);
- });
- });
-
- describe("ungroupWindows", () => {
- let target: MockBrowserWindow;
- let win1: MockBrowserWindow;
- let win2: MockBrowserWindow;
- let mgr: any;
-
- beforeEach(() => {
- target = new MockBrowserWindow();
- win1 = new MockBrowserWindow();
- win2 = new MockBrowserWindow();
-
- mgr = {
- ungroupWindows: (...windows: any[]) => {
- for (const win of windows) {
- win.group = null;
- win.removeListener("move", expect.any(Function));
- }
- },
- browserWindow: {
- getAllWindows: () => [target, win1, win2]
- }
- };
-
- // Set up spies
- jest.spyOn(target, "removeListener").mockImplementation(() => {});
- });
-
- it("clears group properties", () => {
- // Set initial group properties
- target.group = "group1";
- win1.group = "group1";
- win2.group = "group1";
-
- expect(target.group).toBeDefined();
- expect(win1.group).toBeDefined();
- expect(win2.group).toBeDefined();
-
- jest.spyOn(mgr.browserWindow, "getAllWindows").mockReturnValue([target, win1, win2]);
- mgr.ungroupWindows(win1, win2);
-
- expect(win1.group).toBeNull();
- expect(win2.group).toBeNull();
- });
-
- it("unhooks orphanded grouped window", () => {
- mgr.ungroupWindows(target, win1, win2);
- expect(target.group).toBeNull();
- expect(target.removeListener).toHaveBeenCalledWith("move", expect.any(Function));
- });
- });
-
- describe("main process", () => {
- let mgr: any;
- let ipc: MockIpc;
-
- beforeEach(() => {
- ipc = new MockIpc();
-
- // Call the on method to register handlers
- ipc.on = jest.fn();
-
- // Create the manager which should register the handlers
- mgr = {
- app: { quit: jest.fn() },
- ipc: ipc,
- browserWindow: {
- fromId: jest.fn(),
- getAllWindows: jest.fn()
- },
- initializeWindow: function(win: any, name: string, options: any) {
- win.name = name;
- if (options && options.main) {
- win.on("closed", () => this.app.quit());
- }
- },
- groupWindows: jest.fn(),
- ungroupWindows: jest.fn()
- };
-
- // Manually call the setup function that would normally be called by the constructor
- const setupEventHandlers = () => {
- ipc.on("desktopJS.window-initialize", jest.fn());
- ipc.on("desktopJS.window-joinGroup", jest.fn());
- ipc.on("desktopJS.window-leaveGroup", jest.fn());
- ipc.on("desktopJS.window-getGroup", jest.fn());
- ipc.on("desktopJS.window-getOptions", jest.fn());
- };
-
- setupEventHandlers();
-
- // Setup handlers for tests
- ipc.initializeHandler = (event: any, options: any) => {
- const win = mgr.browserWindow.fromId(options.id);
- if (win) {
- win.name = options.name;
- event.returnValue = win.name;
- }
- };
-
- ipc.joinGroupHandler = (event: any, options: any) => {
- const source = mgr.browserWindow.fromId(options.source);
- const target = mgr.browserWindow.fromId(options.target);
- if (source && target) {
- mgr.groupWindows(target, source);
- }
- };
-
- ipc.leaveGroupHandler = (event: any, options: any) => {
- const win = mgr.browserWindow.fromId(options.source);
- if (win) {
- mgr.ungroupWindows(win);
- }
- };
-
- ipc.getGroupHandler = (event: any, options: any) => {
- const win = mgr.browserWindow.fromId(options.id);
- if (win && win.group) {
- const windows = mgr.browserWindow.getAllWindows();
- event.returnValue = windows
- .filter((w: any) => w.group === win.group)
- .map((w: any) => w.id);
- } else {
- event.returnValue = [];
- }
- };
- });
-
- it("subscribed to ipc", () => {
- expect(ipc.on).toHaveBeenCalledTimes(5);
- expect(ipc.on).toHaveBeenCalledWith("desktopJS.window-initialize", expect.any(Function));
- expect(ipc.on).toHaveBeenCalledWith("desktopJS.window-joinGroup", expect.any(Function));
- expect(ipc.on).toHaveBeenCalledWith("desktopJS.window-leaveGroup", expect.any(Function));
- expect(ipc.on).toHaveBeenCalledWith("desktopJS.window-getGroup", expect.any(Function));
- expect(ipc.on).toHaveBeenCalledWith("desktopJS.window-getOptions", expect.any(Function));
- });
-
- it("initializeWindow on non-main does not attach to close", () => {
- const win = new MockBrowserWindow();
- jest.spyOn(win, "on").mockImplementation(() => {});
- mgr.initializeWindow(win, "name", {});
- expect(win.on).not.toHaveBeenCalled();
- });
-
- it("initializeWindow attaches to close on main window and invokes quit", () => {
- const win = new MockBrowserWindow();
- jest.spyOn(win, "on").mockImplementation((event, callback) => {
- if (event === "closed") {
- callback();
- }
- });
-
- mgr.initializeWindow(win, "name", { main: true });
-
- expect(win.on).toHaveBeenCalledWith("closed", expect.any(Function));
- expect(mgr.app.quit).toHaveBeenCalled();
- });
-
- describe("main ipc handlers", () => {
- it("setname sets and returns supplied name", () => {
- const win = new MockBrowserWindow();
- mgr.browserWindow.fromId.mockReturnValue(win);
-
- const event: any = {};
- ipc.initializeHandler(event, { id: 1, name: "NewName" });
-
- expect(win.name).toEqual("NewName");
- expect(event.returnValue).toEqual("NewName");
- });
-
- it("joinGroup invokes groupWindows", () => {
- const source = new MockBrowserWindow();
- source.id = 1;
- const target = new MockBrowserWindow();
- target.id = 2;
-
- mgr.browserWindow.fromId.mockImplementation(id => id === source.id ? source : target);
-
- ipc.joinGroupHandler({}, { source: 1, target: 2 });
-
- expect(mgr.groupWindows).toHaveBeenCalledWith(target, source);
- });
-
- it("leaveGroup invokes ungroupWindows", () => {
- const win = new MockBrowserWindow();
- mgr.browserWindow.fromId.mockReturnValue(win);
-
- ipc.leaveGroupHandler({}, { source: 1 });
-
- expect(mgr.ungroupWindows).toHaveBeenCalledWith(win);
- });
-
- it("getGroup returns matching windows by group", () => {
- const win1 = new MockBrowserWindow();
- win1.id = 1;
- const win2 = new MockBrowserWindow();
- win2.id = 2;
- const win3 = new MockBrowserWindow();
- win3.id = 3;
-
- win1.group = "group";
- win2.group = "group";
- win3.group = "different-group";
-
- mgr.browserWindow.fromId.mockReturnValue(win1);
- mgr.browserWindow.getAllWindows.mockReturnValue([win1, win2, win3]);
-
- const event: any = {};
- ipc.getGroupHandler(event, { id: 1 });
-
- expect(event.returnValue).toEqual([1, 2]);
- });
-
- it("getGroup returns returns empty array when no matching groups", () => {
- const win1 = new MockBrowserWindow();
- win1.id = 1;
- const win2 = new MockBrowserWindow();
- win2.id = 2;
-
- // No groups set
-
- mgr.browserWindow.fromId.mockReturnValue(win1);
- mgr.browserWindow.getAllWindows.mockReturnValue([win1, win2]);
-
- const event: any = {};
- ipc.getGroupHandler(event, { id: 1 });
-
- expect(event.returnValue).toEqual([]);
- });
- });
- });
-});
-
-describe("ElectronDisplayManager", () => {
- let electron: any;
- let screen: any;
- let displayManager: any;
-
- beforeEach(() => {
- // Create mock electron and screen objects with Jest
- electron = { ipc: jest.fn() };
- screen = {
- getPrimaryDisplay: jest.fn(),
- getAllDisplays: jest.fn(),
- getCursorScreenPoint: jest.fn().mockReturnValue({ x: 1, y: 2 })
- };
-
- Object.defineProperty(electron, "screen", { value: screen });
-
- // Create a mock display manager
- displayManager = {
- screen,
- getPrimaryMonitor: () => {
- return screen.getPrimaryDisplay();
- },
- getAllDisplays: () => {
- return screen.getAllDisplays();
- },
- getMousePosition: () => {
- return screen.getCursorScreenPoint();
- }
- };
- });
-
- it("screen to be defined", () => {
- expect(displayManager.screen).toBeDefined();
- });
-
- it("getPrimaryMonitor", () => {
- displayManager.getPrimaryMonitor();
- expect(screen.getPrimaryDisplay).toHaveBeenCalled();
- });
-
- it("getAllDisplays", () => {
- displayManager.getAllDisplays();
- expect(screen.getAllDisplays).toHaveBeenCalled();
- });
-
- it("getMousePosition", () => {
- const position = displayManager.getMousePosition();
- expect(position).toEqual({ x: 1, y: 2 });
- expect(screen.getCursorScreenPoint).toHaveBeenCalled();
- });
-});
-
-describe("ElectronGlobalShortcutManager", () => {
- describe("invokes underlying Electron", () => {
- let electron: any;
- let container: any;
-
- beforeEach(() => {
- electron = {
- globalShortcut: {
- register: jest.fn(),
- unregister: jest.fn(),
- isRegistered: jest.fn(),
- unregisterAll: jest.fn()
- }
- };
-
- // Create a mock container with the methods we need to test
- container = {
- registerShortcut: (shortcut: any) => {
- electron.globalShortcut.register(shortcut.key, () => {});
- },
- unregisterShortcut: (key: string) => {
- electron.globalShortcut.unregister(key);
- },
- isShortcutRegistered: (key: string) => {
- electron.globalShortcut.isRegistered(key);
- },
- unregisterAllShortcuts: () => {
- electron.globalShortcut.unregisterAll();
- }
- };
- });
-
- it("register", () => {
- container.registerShortcut({ key: "CTRL+SHIFT+A", invocationId: "test" });
- expect(electron.globalShortcut.register).toHaveBeenCalledWith("CTRL+SHIFT+A", expect.any(Function));
- });
-
- it("unregister", () => {
- container.unregisterShortcut("CTRL+SHIFT+A");
- expect(electron.globalShortcut.unregister).toHaveBeenCalledWith("CTRL+SHIFT+A");
- });
-
- it("isRegistered", () => {
- container.isShortcutRegistered("CTRL+SHIFT+A");
- expect(electron.globalShortcut.isRegistered).toHaveBeenCalledWith("CTRL+SHIFT+A");
- });
-
- it("unregisterAll", () => {
- container.unregisterAllShortcuts();
- expect(electron.globalShortcut.unregisterAll).toHaveBeenCalled();
- });
- });
-});
\ No newline at end of file
diff --git a/packages/desktopjs-electron/tests/setup.ts b/packages/desktopjs-electron/tests/setup.ts
deleted file mode 100644
index 20f33412..00000000
--- a/packages/desktopjs-electron/tests/setup.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Morgan Stanley makes this available to you under the Apache License,
- * Version 2.0 (the "License"). You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * See the NOTICE file distributed with this work for additional information
- * regarding copyright ownership. Unless required by applicable law or agreed
- * to in writing, software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions
- * and limitations under the License.
- */
-
-/**
- * Jest Setup File for desktopJS-electron
- *
- * This file is executed after Jest is initialized but before tests are run.
- * It configures the testing environment with:
- * 1. Custom matchers for testing promises and rejections
- * 2. TypeScript type definitions for the custom matchers
- * 3. Global test utilities needed across multiple test files
- *
- * This setup is consistent across all desktopJS packages to ensure
- * uniform testing behavior.
- */
-
-import 'jest';
diff --git a/packages/desktopjs-electron/tsconfig.json b/packages/desktopjs-electron/tsconfig.json
deleted file mode 100644
index 11a0a35f..00000000
--- a/packages/desktopjs-electron/tsconfig.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "extends": "../../tsconfig.json",
- "compilerOptions": {
- "outDir": "build",
- },
- "include": [
- "src/**/*.ts"
- ],
- "exclude": [
- "**/*.tmpl.ts",
- "**/*.spec.ts"
- ]
-}
\ No newline at end of file
diff --git a/packages/desktopjs-electron/vite.config.ts b/packages/desktopjs-electron/vite.config.ts
deleted file mode 100644
index cc6799ba..00000000
--- a/packages/desktopjs-electron/vite.config.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Morgan Stanley makes this available to you under the Apache License,
- * Version 2.0 (the "License"). You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * See the NOTICE file distributed with this work for additional information
- * regarding copyright ownership. Unless required by applicable law or agreed
- * to in writing, software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions
- * and limitations under the License.
- */
-
-import { defineConfig } from 'vite';
-import { resolve } from 'path';
-import dts from 'vite-plugin-dts';
-
-const sharedConfig = {
- plugins: [
- dts({
- include: ['src'],
- exclude: ['**/*.spec.ts', '**/*.test.ts'],
- rollupTypes: true,
- aliasesExclude: ['@morgan-stanley/desktopjs']
- })
- ],
- build: {
- sourcemap: true,
- lib: {
- entry: resolve(__dirname, 'src/electron.ts'),
- name: 'desktopJSElectron',
- },
- rollupOptions: {
- external: ['electron', '@morgan-stanley/desktopjs'],
- output: {
- globals: {
- electron: 'electron',
- '@morgan-stanley/desktopjs': 'desktopJS'
- },
- sourcemapPathTransform: (relativeSourcePath: string) => {
- return relativeSourcePath.replace(/^\.\.[\\/]src[\\/]/, './');
- }
- }
- }
- }
-};
-
-export default defineConfig(({ mode }) => {
- return {
- ...sharedConfig,
- build: {
- ...sharedConfig.build,
- lib: {
- ...sharedConfig.build.lib,
- formats: ['umd'],
- fileName: (format) => `desktopjs-electron.${format}.js`
- },
- minify: false,
- outDir: 'dist'
- },
- test: {
- include: ['tests/**/*.spec.ts'],
- coverage: {
- reporter: ['text', 'json', 'html'],
- reportsDirectory: './build/coverage'
- },
- }
- };
-});
diff --git a/packages/desktopjs/package.json b/packages/desktopjs/package.json
index 452eea66..108afbab 100644
--- a/packages/desktopjs/package.json
+++ b/packages/desktopjs/package.json
@@ -34,8 +34,7 @@
"desktop wrapper",
"desktop",
"openfin",
- "hadouken",
- "electron"
+ "hadouken"
],
"homepage": "https://github.com/MorganStanley/desktopJS#readme"
}
diff --git a/packages/desktopjs/src/registry.ts b/packages/desktopjs/src/registry.ts
index 617b4527..a5e55063 100644
--- a/packages/desktopjs/src/registry.ts
+++ b/packages/desktopjs/src/registry.ts
@@ -41,7 +41,7 @@ export function clearRegistry() {
}
/** Register a container type in the registry.
- * @param {string} id Unique identifier of the container type (eg. Electron).
+ * @param {string} id Unique identifier of the container type (eg. OpenFin).
* @param {ContainerRegistration} registration Registration details.
*/
export function registerContainer(id: string, registration: ContainerRegistration) {
diff --git a/packages/desktopjs/src/shortcut.ts b/packages/desktopjs/src/shortcut.ts
index b4503022..21486cb4 100644
--- a/packages/desktopjs/src/shortcut.ts
+++ b/packages/desktopjs/src/shortcut.ts
@@ -21,18 +21,18 @@
*/
export abstract class GlobalShortcutManager {
/** Registers a global shortcut.
- * @param shortcut {string} [Accelerator]{@link https://electronjs.org/docs/api/accelerator}
+ * @param shortcut {string} Accelerator string describing the key combination (e.g. "CmdOrCtrl+Shift+A")
*/
public abstract register(shortcut: string, callback: () => void): Promise;
/** Checks if a given shortcut has been registered.
- * @param shortcut {string} [Accelerator]{@link https://electronjs.org/docs/api/accelerator}
+ * @param shortcut {string} Accelerator string describing the key combination (e.g. "CmdOrCtrl+Shift+A")
* @returns {Promise} A Promise that resolves to a boolean whether the shortcut is already registered.
*/
public abstract isRegistered(shortcut: string): Promise;
/** Removes a previously registered shortcut.
- * @param shortcut {string} [Accelerator]{@link https://electronjs.org/docs/api/accelerator}
+ * @param shortcut {string} Accelerator string describing the key combination (e.g. "CmdOrCtrl+Shift+A")
*/
public abstract unregister(shortcut: string): Promise;
diff --git a/packages/desktopjs/src/window.ts b/packages/desktopjs/src/window.ts
index 0e021d3f..87e56a9e 100644
--- a/packages/desktopjs/src/window.ts
+++ b/packages/desktopjs/src/window.ts
@@ -453,7 +453,7 @@ export class SnapAssistWindowManager extends GroupWindowManager {
win.addListener("disabled-frame-bounds-changed", () => this.onMoved(win));
win.addListener("frame-enabled", () => win.innerWindow.disableFrame());
} else {
- // Electron windows specific moved handler
+ // Container-specific moved handler for native windows that expose hookWindowMessage
if (win.innerWindow && win.innerWindow.hookWindowMessage) {
win.innerWindow.hookWindowMessage(0x0232, () => this.onMoved(win)); // WM_EXITSIZEMOVE
}
diff --git a/packages/desktopjs/tests/unit/window.spec.ts b/packages/desktopjs/tests/unit/window.spec.ts
index 0656a56b..ea6d2039 100644
--- a/packages/desktopjs/tests/unit/window.spec.ts
+++ b/packages/desktopjs/tests/unit/window.spec.ts
@@ -312,7 +312,7 @@ describe("SnapAssistWindowManager", () => {
expect(win.innerWindow.disableFrame).toHaveBeenCalled();
});
- it ("onAttached hooks on Electron wndproc when available", () => {
+ it ("onAttached hooks native wndproc via hookWindowMessage when available", () => {
const win = {
innerWindow: {
hookWindowMessage: jest.fn()
diff --git a/typedoc.json b/typedoc.json
index 41dfbd41..cb1985a2 100644
--- a/typedoc.json
+++ b/typedoc.json
@@ -1,7 +1,6 @@
{
"entryPoints": [
"./packages/desktopjs/src/desktop.ts",
- "./packages/desktopjs-electron/src/electron.ts",
"./packages/desktopjs-openfin/src/openfin.ts"
],
"name": "desktopJS",
diff --git a/vite.config.ts b/vite.config.ts
index a5365260..ae57be2e 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -14,14 +14,7 @@ export default defineConfig({
formats: ['umd'],
fileName: (format, entryName) => `${entryName}.${format}.js`,
},
- rollupOptions: {
- external: ['electron'],
- output: {
- globals: {
- electron: 'electron',
- },
- },
- },
+ rollupOptions: {},
},
test: {
coverage: {