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: {