├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ ├── ci.yml
│ └── playwright.yml
├── .gitignore
├── README.md
├── app
├── index.js
└── renderer
│ ├── loading.html
│ ├── main.html
│ ├── main.js
│ ├── preload.js
│ ├── window.html
│ └── window.js
├── index.d.ts
├── index.js
├── lib
├── electron-windows.js
└── helper.js
├── package.json
├── playwright.config.js
├── sceenshot.png
├── start.js
└── test
├── e2e
└── electron-windows.e2e.test.js
├── electron-windows.test.js
└── mocha.opts
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/.*
2 | **/node_modules
3 | **/test
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | extends: 'eslint-config-egg',
5 | // babel-eslint (deprecated) is now @babel/eslint-parser
6 | parser: '@babel/eslint-parser',
7 | parserOptions: {
8 | ecmaVersion: 2018,
9 | requireConfigFile: false,
10 | },
11 | rules: {
12 | 'valid-jsdoc': 0,
13 | 'no-script-url': 0,
14 | 'no-multi-spaces': 0,
15 | 'default-case': 0,
16 | 'no-case-declarations': 0,
17 | 'one-var-declaration-per-line': 0,
18 | 'no-restricted-syntax': 0,
19 | 'jsdoc/require-param': 0,
20 | 'jsdoc/check-param-names': 0,
21 | 'jsdoc/require-param-description': 0,
22 | 'arrow-parens': 0,
23 | 'prefer-promise-reject-errors': 0,
24 | 'no-control-regex': 0,
25 | 'no-use-before-define': 0,
26 | 'array-callback-return': 0,
27 | 'no-bitwise': 0,
28 | 'no-self-compare': 0,
29 | 'one-var': 0
30 | },
31 | globals: {
32 | window: true,
33 | document: true,
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | push:
7 | branches:
8 | - '**'
9 |
10 | jobs:
11 | Runner:
12 | timeout-minutes: 10
13 | runs-on: ${{ matrix.os }}
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | os: [ ubuntu-latest ]
18 | node-version: [ 16 ]
19 | steps:
20 | - name: Checkout Git Source
21 | uses: actions/checkout@v3
22 |
23 | - name: Setup Node.js
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 |
28 | - name: Install dependencies
29 | run: |
30 | npm i npm@6 -g
31 | npm i
32 |
33 | - name: Continuous integration
34 | run: |
35 | npm run lint
36 | npm run test
37 |
38 | - name: Code coverage
39 | uses: codecov/codecov-action@v3.0.0
40 | with:
41 | token: ${{ secrets.CODECOV_TOKEN }}
42 |
--------------------------------------------------------------------------------
/.github/workflows/playwright.yml:
--------------------------------------------------------------------------------
1 | name: E2E Tests
2 | on:
3 | push:
4 | branches: [ master ]
5 | pull_request:
6 | branches: [ master ]
7 | jobs:
8 | test:
9 | timeout-minutes: 10
10 | strategy:
11 | matrix:
12 | os: [ macos-latest, windows-latest ]
13 | runs-on: ${{ matrix.os }}
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | - uses: actions/setup-node@v4
18 | with:
19 | node-version: 20
20 |
21 | - name: Install dependencies
22 | run: npm i
23 |
24 | - name: Install Playwright Browser
25 | run: npx playwright install --with-deps chromium
26 |
27 | - name: Run Playwright tests
28 | run: npm run test:e2e
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | .nyc_output
4 | coverage
5 | file.txt
6 | *.gz
7 | *.sw*
8 | *.un~
9 |
10 | # Playwright
11 | /test-results/
12 | /playwright-report/
13 | /blob-report/
14 | /playwright/.cache/
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # electron-windows
2 |
3 | [![NPM version][npm-image]][npm-url]
4 | [![CI][CI-image]][CI-url]
5 | [![Test coverage][codecov-image]][codecov-url]
6 | [![node version][node-image]][node-url]
7 | [![npm download][download-image]][download-url]
8 |
9 | [npm-image]: https://img.shields.io/npm/v/electron-windows.svg
10 | [npm-url]: https://npmjs.org/package/electron-windows
11 | [CI-image]: https://github.com/electron-modules/electron-windows/actions/workflows/ci.yml/badge.svg
12 | [CI-url]: https://github.com/electron-modules/electron-windows/actions/workflows/ci.yml
13 | [codecov-image]: https://img.shields.io/codecov/c/github/electron-modules/electron-windows.svg?logo=codecov
14 | [codecov-url]: https://codecov.io/gh/electron-modules/electron-windows
15 | [node-image]: https://img.shields.io/badge/node.js-%3E=_8-green.svg
16 | [node-url]: http://nodejs.org/download/
17 | [download-image]: https://img.shields.io/npm/dm/electron-windows.svg
18 | [download-url]: https://npmjs.org/package/electron-windows
19 |
20 | > Manage multiple windows of Electron gracefully and provides powerful features.
21 |
22 |
23 |
24 | ## Contributors
25 |
26 | |[
xudafeng](https://github.com/xudafeng)
|[
sriting](https://github.com/sriting)
|[
snapre](https://github.com/snapre)
|[
ColaDaddyz](https://github.com/ColaDaddyz)
|[
z0gSh1u](https://github.com/z0gSh1u)
|[
zlyi](https://github.com/zlyi)
|
27 | | :---: | :---: | :---: | :---: | :---: | :---: |
28 | [
moshangqi](https://github.com/moshangqi)
29 |
30 | This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Fri Apr 25 2025 11:40:35 GMT+0800`.
31 |
32 |
33 |
34 | ## Installment
35 |
36 | ```bash
37 | $ npm i electron-windows --save
38 | ```
39 |
40 | ## Demo
41 |
42 | 
43 |
44 | ## APIs
45 |
46 | ### init
47 |
48 | ```javascript
49 | const WindowManager = require('electron-windows');
50 | const windowManager = new WindowManager();
51 | ```
52 |
53 | ### create
54 |
55 | ```javascript
56 | const { app } = require('electron');
57 | const winRef = windowManager.create({
58 | name: 'window1',
59 | loadingView: {
60 | url: '',
61 | },
62 | browserWindow: {
63 | width: 800,
64 | height: 600,
65 | titleBarStyle: 'hidden',
66 | title: 'demo',
67 | show: false,
68 | webPreferences: {
69 | nodeIntegration: app.isDev,
70 | webSecurity: true,
71 | webviewTag: true,
72 | },
73 | },
74 | openDevTools: true,
75 | storageKey: 'storage-filename', // optional. The name of file. Support storage of window state
76 | storagePath: app.getPath('userData'), // optional. The path of file, only used when storageKey is not empty
77 | });
78 | ```
79 |
80 | ## TODO
81 |
82 | - [ ] support storage of window configuration
83 | - [ ] clone pointed window
84 |
85 | ## License
86 |
87 | The MIT License (MIT)
88 |
--------------------------------------------------------------------------------
/app/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const url = require('url');
4 | const path = require('path');
5 | const _ = require('lodash');
6 | const WindowManager = require('..');
7 | const { ipcMain, screen, BrowserWindow } = require('electron');
8 | const storage = require('electron-json-storage-alt');
9 |
10 | const loadingUrl = url.format({
11 | pathname: path.join(__dirname, 'renderer', 'loading.html'),
12 | protocol: 'file:',
13 | });
14 |
15 | const mainUrl = url.format({
16 | pathname: path.join(__dirname, 'renderer', 'main.html'),
17 | protocol: 'file:',
18 | });
19 |
20 | const windowUrl = url.format({
21 | pathname: path.join(__dirname, 'renderer', 'window.html'),
22 | protocol: 'file:',
23 | });
24 |
25 | const getRandomPostion = () => {
26 | const { workAreaSize } = screen.getPrimaryDisplay();
27 | const { width: screenWidth, height: screenHeight } = workAreaSize;
28 | const x = parseInt(_.random(screenWidth / 16, screenWidth / 2), 10);
29 | const y = parseInt(_.random(screenHeight / 16, screenHeight / 2), 10);
30 | return {
31 | x,
32 | y,
33 | };
34 | };
35 |
36 | class App {
37 | windowManager = new WindowManager({
38 | onStorageSave: (key, data) => {
39 | storage.set(key, data);
40 | },
41 | onStorageReadSync(key) {
42 | return storage.getSync(key);
43 | },
44 | });
45 |
46 | init() {
47 | const mainWindow = this.windowManager.create({
48 | name: 'main',
49 | storageKey: 'main-window-storage',
50 | loadingView: {
51 | url: loadingUrl,
52 | },
53 | browserWindow: {
54 | webPreferences: {
55 | enableRemoteModule: false,
56 | // `preload` uses Node's `fs` module, so `nodeIntegration` should be enabled.
57 | nodeIntegration: true,
58 | webSecurity: true,
59 | webviewTag: true,
60 | preload: path.join(__dirname, 'renderer', 'preload.js'),
61 | },
62 | },
63 | openDevTools: false,
64 | });
65 | mainWindow.loadURL(mainUrl);
66 | this.mainWindow = mainWindow;
67 | this.bindIPC();
68 | }
69 |
70 | bindIPC() {
71 | ipcMain.on('new-window', () => {
72 | const postion = getRandomPostion();
73 | const window = this.windowManager.create({
74 | name: Date.now(),
75 | loadingView: {
76 | url: loadingUrl,
77 | },
78 | browserWindow: {
79 | x: postion.x,
80 | y: postion.y,
81 | webPreferences: {
82 | nodeIntegration: true,
83 | webSecurity: true,
84 | webviewTag: true,
85 | preload: path.join(__dirname, 'renderer', 'preload.js'),
86 | },
87 | },
88 | });
89 |
90 | window.loadURL(windowUrl);
91 | console.log(this.windowManager);
92 | });
93 |
94 | ipcMain.on('new-online-window', () => {
95 | const postion = getRandomPostion();
96 | const window = this.windowManager.create({
97 | name: Date.now(),
98 | loadingView: {
99 | url: loadingUrl,
100 | },
101 | browserWindow: {
102 | x: postion.x,
103 | y: postion.y,
104 | webPreferences: {
105 | nodeIntegration: true,
106 | webSecurity: true,
107 | webviewTag: true,
108 | preload: path.join(__dirname, 'renderer', 'preload.js'),
109 | },
110 | },
111 | });
112 |
113 | window.loadURL('https://www.github.com');
114 | console.log(this.windowManager);
115 | });
116 |
117 | ipcMain.on('close-window', (_) => {
118 | const window = BrowserWindow.fromWebContents(_.sender);
119 | window.close();
120 | console.log(this.windowManager);
121 | });
122 |
123 | ipcMain.on('blur-window', (_) => {
124 | const window = BrowserWindow.fromWebContents(_.sender);
125 | window.blur();
126 | console.log(window);
127 | });
128 |
129 | ipcMain.on('open-devtools', (_) => {
130 | const window = BrowserWindow.fromWebContents(_.sender);
131 | window.openDevTools();
132 | });
133 | }
134 | }
135 |
136 | module.exports = App;
137 |
--------------------------------------------------------------------------------
/app/renderer/loading.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Loading
7 |
28 |
29 |
30 |
31 |
32 | loading...
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/renderer/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | main
7 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/renderer/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | document.querySelector('#new').addEventListener('click', () => {
4 | window._electron_bridge.send('new-window');
5 | }, false);
6 |
7 | document.querySelector('#new-online').addEventListener('click', () => {
8 | window._electron_bridge.send('new-online-window');
9 | }, false);
10 |
11 | document.querySelector('#play-game').addEventListener('click', () => {
12 | const className = 'custom-iframe';
13 | const elem = document.querySelector(`.${className}`);
14 | if (elem) return;
15 | const iframe = document.createElement('iframe');
16 | iframe.src = 'https://xudafeng.github.io';
17 | iframe.className = className;
18 | document.body.appendChild(iframe);
19 | }, false);
20 |
21 | document.querySelector('#debug').addEventListener('click', () => {
22 | window._electron_bridge.send('open-devtools');
23 | }, false);
24 |
--------------------------------------------------------------------------------
/app/renderer/preload.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { writeFile, readFileSync } = require('fs');
4 | const { ipcRenderer, desktopCapturer, contextBridge } = require('electron');
5 |
6 | contextBridge.exposeInMainWorld(
7 | '_electron_bridge',
8 | {
9 | send: (channel, args) => {
10 | ipcRenderer.send(channel, args);
11 | },
12 | writeFile,
13 | readFileSync,
14 | desktopCapturer,
15 | }
16 | );
17 |
--------------------------------------------------------------------------------
/app/renderer/window.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/renderer/window.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | document.querySelector('#close').addEventListener('click', () => {
4 | window._electron_bridge.send('close-window');
5 | }, false);
6 |
7 | document.querySelector('#blur').addEventListener('click', () => {
8 | window._electron_bridge.send('blur-window');
9 | }, false);
10 |
11 | document.querySelector('#write').addEventListener('click', () => {
12 | const data = 'write local file successfully';
13 | const fileName = 'file.txt';
14 | window._electron_bridge.writeFile(fileName, data, (err) => {
15 | if (err) {
16 | console.log(err);
17 | } else {
18 | console.log(window._electron_bridge.readFileSync(fileName, 'utf8'));
19 | }
20 | });
21 | }, false);
22 |
23 | document.querySelector('#debug').addEventListener('click', () => {
24 | window._electron_bridge.send('open-devtools');
25 | }, false);
26 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Type Definitions for electron-windows
3 | *
4 | * https://github.com/electron-modules/electron-windows
5 | */
6 | import type { BrowserWindow, BrowserWindowConstructorOptions } from 'electron'
7 |
8 | /**
9 | * The options used when creating a new window.
10 | */
11 | interface ICreateOptions {
12 | /**
13 | * The name of the window. Default is `'anonymous'`.
14 | */
15 | name?: string
16 | /**
17 | * The loading view of the window.
18 | */
19 | loadingView?: { url?: string; [key: string]: any }
20 | /**
21 | * The options for the BrowserWindow. Same as Electron's `BrowserWindow` constructor options.
22 | */
23 | browserWindow?: BrowserWindowConstructorOptions
24 | /**
25 | * Whether to open DevTools. Default is false.
26 | */
27 | openDevTools?: boolean
28 | /**
29 | * Whether to prevent the origin close event. Default is false.
30 | */
31 | preventOriginClose?: boolean
32 | /**
33 | * Whether to prevent the origin navigate event. Default is false.
34 | */
35 | preventOriginNavigate?: boolean
36 | /**
37 | * The storage key for the window state. State management is based on `electron-window-state`.
38 | */
39 | storageKey?: string
40 | /**
41 | * The storage path for the window state. State management is based on `electron-window-state`.
42 | */
43 | storagePath?: string
44 | /**
45 | * The global user agent string used for window's `loadURL`.
46 | */
47 | globalUserAgent?: string
48 |
49 | // Allow other properties
50 | [key: string]: any
51 | }
52 |
53 | /**
54 | * Window enriched with state management features.
55 | */
56 | interface StatefulWindow extends BrowserWindow {
57 | /**
58 | * The storage key for the window state. State management is based on `electron-window-state`.
59 | */
60 | storageKey?: string
61 | /**
62 | * The state from storage. State management is based on `electron-window-state`.
63 | */
64 | stateFromStorage?: {
65 | manage: (win: BrowserWindow) => void
66 | x?: number
67 | y?: number
68 | width?: number
69 | height?: number
70 | [key: string]: any
71 | }
72 | /**
73 | * (PRESERVED) Keeps the name of the window.
74 | */
75 | _name?: string
76 | /**
77 | * (PRESERVED) Keeps the very original `loadURL` method without UA modification.
78 | */
79 | _loadURL?: (url: string, options?: Electron.LoadURLOptions) => void // Added based on usage in _setGlobalUserAgent
80 | }
81 |
82 | /**
83 | * A union type of `StatefulWindow` and common `BrowserWindow`.
84 | */
85 | type Window = StatefulWindow | BrowserWindow
86 |
87 | /**
88 | * Manage multiple windows of Electron gracefully and provides powerful features.
89 | */
90 | declare class WindowsManager {
91 | constructor(options?: Record)
92 |
93 | /**
94 | * The options used when creating a new window.
95 | */
96 | _createOptions: ICreateOptions
97 |
98 | /**
99 | * The options used when `WindowsManager` is constructed.
100 | */
101 | options: Record
102 |
103 | /**
104 | * The windows managed by the `WindowsManager`.
105 | */
106 | windows: { [id: number]: Window }
107 |
108 | /**
109 | * Creates a new window managed by the `WindowsManager`.
110 | * @param options Configuration options for the new window.
111 | * @returns The created `Window` instance (with added state properties if `storageKey` is provided).
112 | */
113 | create(options: ICreateOptions): Window
114 |
115 | /**
116 | * Retrieves a managed window by its assigned name.
117 | * @param name The name assigned to the window during creation.
118 | * @returns The found `Window` instance or undefined.
119 | */
120 | get(name: string): Window | undefined
121 |
122 | /**
123 | * Retrieves a managed window by its ID.
124 | * @param id The ID of the window.
125 | * @returns The found `Window` instance or undefined.
126 | */
127 | getById(id: number): Window | undefined
128 |
129 | /**
130 | * Retrieves all windows currently managed by this instance.
131 | * @returns An object where keys are window IDs and values are `Window` instances.
132 | */
133 | getAll(): { [id: number]: Window }
134 |
135 | /**
136 | * Clone a window.
137 | * @todo This is NOT IMPLEMENTED yet. Now it just returns the same window.
138 | * @param window The `Window` instance to clone.
139 | * @returns The cloned `Window` instance.
140 | */
141 | clone(window: Window): Window
142 |
143 | /**
144 | * The global user agent string.
145 | */
146 | static GLOBAL_USER_AGENT?: string
147 |
148 | /**
149 | * Sets a global user agent string to be used for all subsequent `loadURL` calls within managed windows.
150 | * @param ua The user agent string.
151 | */
152 | static setGlobalUserAgent(ua: string): void
153 | }
154 |
155 | export = WindowsManager
156 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = require('./lib/electron-windows');
4 |
--------------------------------------------------------------------------------
/lib/electron-windows.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const _ = require('lodash');
4 | const { BrowserWindow, BrowserView } = require('electron');
5 | const windowStateKeeper = require('electron-window-state');
6 |
7 | class WindowsManager {
8 | constructor(options = {}) {
9 | this.options = options;
10 | this.windows = {};
11 | }
12 |
13 | create(options) {
14 | this._createOptions = options;
15 | let stateFromStorage = {};
16 | const {
17 | name = 'anonymous',
18 | loadingView = {},
19 | browserWindow: browserWindowOptions = {},
20 | openDevTools = false,
21 | preventOriginClose = false,
22 | storageKey = undefined,
23 | storagePath = undefined,
24 | } = options;
25 | if (storageKey) {
26 | const initWindowState = {
27 | defaultWidth: browserWindowOptions.width,
28 | defaultHeight: browserWindowOptions.height,
29 | file: storageKey + '.json',
30 | };
31 | if (storagePath) {
32 | initWindowState.path = storagePath;
33 | }
34 | stateFromStorage = windowStateKeeper(initWindowState);
35 | }
36 | const window = new BrowserWindow(Object.assign({
37 | acceptFirstMouse: true,
38 | }, browserWindowOptions, _.pick(stateFromStorage, [
39 | 'x',
40 | 'y',
41 | 'width',
42 | 'height',
43 | ])));
44 | this._setGlobalUserAgent(window, options);
45 | window._name = name;
46 | if (loadingView.url) {
47 | this._setLoadingView(window);
48 | }
49 | window.on('close', (event) => {
50 | if (preventOriginClose) {
51 | event.preventDefault();
52 | return;
53 | }
54 | delete this.windows[window.id];
55 | });
56 | window.webContents.on('dom-ready', () => {
57 | if (openDevTools) {
58 | window.openDevTools();
59 | }
60 | });
61 | this.windows[window.id] = window;
62 | if (storageKey) {
63 | window.stateFromStorage = stateFromStorage;
64 | window.storageKey = storageKey;
65 | // register listener on BrowserWindow for window resize events, store state after window close
66 | stateFromStorage.manage(window);
67 | }
68 | return window;
69 | }
70 |
71 | _setGlobalUserAgent(window, options) {
72 | const {
73 | globalUserAgent,
74 | } = options;
75 | const ua = globalUserAgent || WindowsManager.GLOBAL_USER_AGENT;
76 | if (!ua) return;
77 | window._loadURL = window.loadURL;
78 | window.loadURL = (url, options = {}) => {
79 | options.userAgent = ua;
80 | window._loadURL(url, options);
81 | };
82 | }
83 |
84 | _setLoadingView(window) {
85 | const {
86 | loadingView: loadingViewOptions,
87 | preventOriginNavigate = false,
88 | } = this._createOptions;
89 | let _loadingView = new BrowserView();
90 |
91 | if (window.isDestroyed()) {
92 | return;
93 | }
94 |
95 | const loadLoadingView = () => {
96 | const [ viewWidth, viewHeight ] = window.getSize();
97 | window.setBrowserView(_loadingView);
98 | _loadingView.setBounds({
99 | x: 0,
100 | y: 0,
101 | width: viewWidth,
102 | height: viewHeight,
103 | });
104 | _loadingView.webContents.loadURL(loadingViewOptions.url);
105 | };
106 |
107 | const onFailure = () => {
108 | if (_loadingView.webContents && !_loadingView.webContents.isDestroyed()) {
109 | _loadingView.webContents.destroy();
110 | }
111 | if (window.isDestroyed()) {
112 | return;
113 | }
114 |
115 | if (window) {
116 | window.removeBrowserView(_loadingView);
117 | }
118 | };
119 |
120 | loadLoadingView();
121 |
122 | window.on('resize', _.debounce(() => {
123 | if (_loadingView.webContents && !_loadingView.webContents.isDestroyed()) {
124 | if (window.isDestroyed()) {
125 | return;
126 | }
127 | const [ viewWidth, viewHeight ] = window.getSize();
128 | _loadingView.setBounds({ x: 0, y: 0, width: viewWidth, height: viewHeight });
129 | }
130 | }, 500));
131 |
132 | window.webContents.on('will-navigate', (e) => {
133 | if (preventOriginNavigate) {
134 | e.preventDefault();
135 | return;
136 | }
137 | if (window.isDestroyed()) {
138 | return;
139 | }
140 | if (_loadingView.webContents && !_loadingView.webContents.isDestroyed()) {
141 | window.setBrowserView(_loadingView);
142 | } else { // if loadingView has been destroyed
143 | _loadingView = new BrowserView();
144 | loadLoadingView();
145 | }
146 | });
147 | window.webContents.on('dom-ready', onFailure);
148 | window.webContents.on('crashed', onFailure);
149 | window.webContents.on('unresponsive', onFailure);
150 | window.webContents.on('did-fail-load', onFailure);
151 | }
152 |
153 | get(name) {
154 | return Object.values(this.windows)
155 | .find(item => item._name === name);
156 | }
157 |
158 | getById(id) {
159 | return Object.values(this.windows)
160 | .find(item => item.id === id);
161 | }
162 |
163 | getAll() {
164 | return this.windows;
165 | }
166 |
167 | clone(window) {
168 | return window;
169 | }
170 | }
171 |
172 | WindowsManager.setGlobalUserAgent = (ua) => {
173 | WindowsManager.GLOBAL_USER_AGENT = ua;
174 | };
175 |
176 | module.exports = WindowsManager;
177 |
--------------------------------------------------------------------------------
/lib/helper.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {};
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "electron-windows",
3 | "version": "34.0.0",
4 | "description": "Manage multiple windows of Electron gracefully and provides powerful features.",
5 | "keywords": [
6 | "electron",
7 | "window",
8 | "windows"
9 | ],
10 | "files": [
11 | "lib",
12 | "index.d.ts"
13 | ],
14 | "main": "index.js",
15 | "types": "./index.d.ts",
16 | "repository": {
17 | "type": "git",
18 | "url": "git://github.com/electron-modules/electron-windows.git"
19 | },
20 | "dependencies": {
21 | "electron": "34",
22 | "electron-window-state": "^5.0.3",
23 | "lodash": "4"
24 | },
25 | "devDependencies": {
26 | "@babel/eslint-parser": "^7.27.1",
27 | "@playwright/test": "^1.52.0",
28 | "@types/node": "^22.15.17",
29 | "electron-json-storage-alt": "18",
30 | "eslint": "7",
31 | "eslint-config-egg": "12",
32 | "eslint-plugin-mocha": "~10.0.0",
33 | "git-contributor": "*",
34 | "husky": "*",
35 | "mocha": "*",
36 | "nyc": "*"
37 | },
38 | "scripts": {
39 | "dev": "electron ./start.js",
40 | "test": "nyc --reporter=lcov --reporter=text mocha",
41 | "test:e2e": "npx playwright test",
42 | "lint": "eslint . --fix",
43 | "contributor": "git-contributor"
44 | },
45 | "husky": {
46 | "hooks": {
47 | "pre-commit": "npm run lint"
48 | }
49 | },
50 | "license": "MIT"
51 | }
--------------------------------------------------------------------------------
/playwright.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | const { defineConfig, devices } = require('@playwright/test');
3 |
4 | const isCI = !!process.env.CI;
5 |
6 | module.exports = defineConfig({
7 | testDir: './test/e2e',
8 | fullyParallel: false,
9 | forbidOnly: isCI,
10 | retries: 0,
11 | workers: 1,
12 | reporter: 'null',
13 | use: {
14 | trace: 'off',
15 | },
16 | projects: [
17 | {
18 | name: 'chromium',
19 | use: { ...devices['Desktop Chrome'] },
20 | },
21 | ],
22 | });
23 |
--------------------------------------------------------------------------------
/sceenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/electron-modules/electron-windows/a4229c5afb32a8ed3c124fd9592b0f7852063258/sceenshot.png
--------------------------------------------------------------------------------
/start.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { app: electronApp } = require('electron');
4 |
5 | const App = require('./app');
6 |
7 | electronApp.on('ready', async () => {
8 | const app = new App();
9 | app.init();
10 | });
11 |
--------------------------------------------------------------------------------
/test/e2e/electron-windows.e2e.test.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { test, expect, _electron as electron } from '@playwright/test';
3 | import { last as _last } from 'lodash';
4 | import * as path from 'path';
5 |
6 | const wait = (ms = 2000) => new Promise((resolve) => setTimeout(resolve, ms));
7 |
8 | /**
9 | * Gets the last element of array.
10 | * @template T
11 | * @param {T[] | null | undefined} arr
12 | * @returns {T}
13 | */
14 | const last = (arr) => {
15 | const lastElement = _last(arr);
16 | expect(lastElement).not.toBeUndefined();
17 | // @ts-ignore
18 | return lastElement;
19 | };
20 |
21 | test.describe('test/e2e/electron-windows.e2e.test.js', () => {
22 | /** @type { import('@playwright/test').ElectronApplication } */
23 | let electronApp;
24 |
25 | test.beforeEach(async () => {
26 | electronApp = await electron.launch({ args: ['start.js'], cwd: path.join(__dirname, '../..') });
27 | });
28 |
29 | test('loadingView option is working', async () => {
30 | const window = await electronApp.firstWindow();
31 |
32 | expect(await window.title()).toBe('Loading');
33 | expect(window.url()).toMatch(/renderer\/loading\.html$/);
34 | });
35 |
36 | test('new window can be correctly created', async () => {
37 | await wait();
38 | const window = await electronApp.firstWindow();
39 | await window.click('button#new');
40 | await wait();
41 | const windows = electronApp.windows();
42 |
43 | expect(windows.length).toBeGreaterThan(1);
44 | expect(last(windows).url()).toMatch(/renderer\/window\.html$/);
45 | });
46 |
47 | test('new online window can be correctly created', async () => {
48 | await wait();
49 | const window = await electronApp.firstWindow();
50 | await window.click('button#new-online');
51 | await wait();
52 | const windows = electronApp.windows();
53 |
54 | expect(windows.length).toBeGreaterThan(1);
55 | expect(last(windows).url()).toMatch(/https(.+)?github\.com/);
56 | });
57 |
58 | test('window can be correctly closed', async () => {
59 | await wait();
60 | const window = await electronApp.firstWindow();
61 | await window.click('button#new');
62 | await wait();
63 | const newWindow = last(electronApp.windows());
64 | await newWindow.click('button#close');
65 | await wait();
66 | const windows = electronApp.windows();
67 |
68 | expect(windows).toHaveLength(1);
69 | expect(await windows[0].title()).toBe('main');
70 | });
71 |
72 | test.afterEach(async () => {
73 | await electronApp?.close();
74 | // @ts-ignore
75 | electronApp = null;
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/test/electron-windows.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require('assert');
4 |
5 | const WindowManager = require('..');
6 |
7 | describe('./test/electron-windows.test.js', () => {
8 | it('should be ok', () => {
9 | assert(WindowManager);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --reporter spec
2 |
--------------------------------------------------------------------------------