From a730d87dd1b1b1c2f44a96cbfbdd7d6e28978a56 Mon Sep 17 00:00:00 2001 From: Marcel Schaeben Date: Mon, 8 May 2023 19:15:49 +0200 Subject: [PATCH] add tests for WindowManager and use mocha-electron for testing so we have access to the Electron APIs --- ...plate__of_mocha_javascript_test_runner.xml | 1 + package.json | 3 +- src/main/index.ts | 3 - src/main/windowManager.ts | 14 +-- test/main/utils.ts | 18 +++ test/main/window-manager.unit.test.ts | 105 ++++++++++++++++++ test/main/winery-manager.unit.test.ts | 17 +-- test/tsconfig.json | 2 +- 8 files changed, 132 insertions(+), 31 deletions(-) create mode 100644 test/main/utils.ts create mode 100644 test/main/window-manager.unit.test.ts diff --git a/.idea/runConfigurations/_template__of_mocha_javascript_test_runner.xml b/.idea/runConfigurations/_template__of_mocha_javascript_test_runner.xml index 56e8ffd..2295145 100644 --- a/.idea/runConfigurations/_template__of_mocha_javascript_test_runner.xml +++ b/.idea/runConfigurations/_template__of_mocha_javascript_test_runner.xml @@ -2,6 +2,7 @@ project + $PROJECT_DIR$/node_modules/electron-mocha true bdd diff --git a/package.json b/package.json index f2d62d8..850a62f 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "ci:dist": "electron-builder", "release": "electron-builder", "test:winery-launcher": "mvn -pl winery-launcher test", - "test:electron": "mocha -r ts-node/register test/**/*.unit.test.ts test/**/*.integration.test.ts", + "test:electron": "electron-mocha -r ts-node/register test/**/*.unit.test.ts test/**/*.integration.test.ts", "test:electron:coverage": "nyc npm run test:electron", "test": "npm-run-all test:winery-launcher test:electron" }, @@ -115,6 +115,7 @@ "css-loader": "^6.7.3", "electron": "^24.1.2", "electron-builder": "^24.2.1", + "electron-mocha": "^12.0.0", "eslint": "^8.39.0", "eslint-plugin-tsdoc": "^0.2.17", "execa": "^7.1.1", diff --git a/src/main/index.ts b/src/main/index.ts index fc0c353..3d4c7df 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -78,9 +78,6 @@ function isValidRepository(repositoryPath: string) { function checkUrlType(url: URL): NavigationUrlType { const parsedMainWindowUrl = new URL(mainWindowUrl) - console.log(url.pathname) - console.log(wineryProcess.toscaManagerUrl.pathname) - if (url.href.startsWith(parsedMainWindowUrl.href)) { return "mainWindow" } else if ( diff --git a/src/main/windowManager.ts b/src/main/windowManager.ts index fbae899..b648c01 100644 --- a/src/main/windowManager.ts +++ b/src/main/windowManager.ts @@ -19,19 +19,13 @@ export class WindowManager extends EventEmitter { private topologyModelerWindowSet = new Set() constructor(private urlTypeChecker: (url: URL) => NavigationUrlType) { - if (!urlTypeChecker) { - throw new Error("Could not initialize Window Manager: Need to pass in an URL type checker!") - } super() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - app['bw'] = BrowserWindow } get mainWindow() { return this._mainWindow } - get toscaManagerWindows() { return Array.from(this.toscaManagerWindowSet) } - get topologyModelerWindows() { return Array.from(this.topologyModelerWindowSet) } - get wineryWindows() { return [...this.toscaManagerWindows, ...this.topologyModelerWindows] } + get toscaManagerWindows() { return Object.freeze(Array.from(this.toscaManagerWindowSet)) } + get topologyModelerWindows() { return Object.freeze(Array.from(this.topologyModelerWindowSet)) } + get wineryWindows() { return Object.freeze([...this.toscaManagerWindows, ...this.topologyModelerWindows]) } /** * Opens the main "workspace selection" window. Makes sure it is created as needed and that there is only one main @@ -178,8 +172,6 @@ export class WindowManager extends EventEmitter { private wineryWindowOpenHandler(details: HandlerDetails): ReturnType { const parsedUrl = new URL(details.url) const urlType = this.urlTypeChecker(parsedUrl) - console.log(urlType) - console.log(details.url) this.openWindowFor(parsedUrl).catch() diff --git a/test/main/utils.ts b/test/main/utils.ts new file mode 100644 index 0000000..33db622 --- /dev/null +++ b/test/main/utils.ts @@ -0,0 +1,18 @@ +import {mainWindowUrl, wineryApiPath} from "../../src/main/resources"; +import {createLogger, transports} from "winston"; +import {PassThrough} from "stream"; +import {match} from "sinon"; + +export const PORT = 8000 +const wineryApiUrl = new URL(wineryApiPath, `http://localhost:${PORT}`).toString() +export const wineryApiUrlMatcher = match((url: URL) => url.toString() === wineryApiUrl) +export const mainWindowUrlMatcher = match((url: URL) => url.toString() === mainWindowUrl) + +// Helper function: create da test dummy logger to listen for "logged" events +export const createTestLogger = () => { + const testTransport = new transports.Stream({stream: new PassThrough()}) + const testLogger = createLogger({ + transports: [testTransport] + }) + return {testTransport, testLogger}; +}; \ No newline at end of file diff --git a/test/main/window-manager.unit.test.ts b/test/main/window-manager.unit.test.ts new file mode 100644 index 0000000..e5617b9 --- /dev/null +++ b/test/main/window-manager.unit.test.ts @@ -0,0 +1,105 @@ +import {expect} from 'chai'; +import {BrowserWindow, shell} from 'electron'; +import sinon, {SinonStub} from 'sinon'; +import {NavigationUrlType, WindowManager} from "../../src/main/windowManager"; +import {mainWindowUrlMatcher} from "./utils"; +import {mainWindowUrl} from "../../src/main/resources"; + +type LoadURLStub = SinonStub<[string, Electron.LoadURLOptions?], Promise> + +describe('WindowManager', () => { + let windowManager: WindowManager; + let urlTypeCheckerStub: SinonStub<[URL], NavigationUrlType>; + + beforeEach(() => { + urlTypeCheckerStub = sinon.stub<[URL]>() + windowManager = new WindowManager(urlTypeCheckerStub) + + // make sure electron does not try to load any urls during tests (which would open an error dialog) + sinon + .stub(BrowserWindow.prototype, 'loadURL') + .withArgs(mainWindowUrlMatcher) + .resolves() + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('openMainWindow', () => { + it('should create a new main window when called for the first time', () => { + expect(windowManager.mainWindow).to.be.null; + windowManager.openMainWindow(); + expect(windowManager.mainWindow).to.be.instanceOf(BrowserWindow); + }); + + it('should not create a new main window when called multiple times', () => { + windowManager.openMainWindow(); + const firstWindow = windowManager.mainWindow; + windowManager.openMainWindow(); + const secondWindow = windowManager.mainWindow; + expect(firstWindow).to.equal(secondWindow); + }); + }); + + describe('openWindowFor', () => { + it('should create and load a TOSCA Manager window when called with a TOSCA Manager URL', async () => { + const url = "about:blank" + urlTypeCheckerStub.returns("toscaManager"); + + const window = await windowManager.openWindowFor(new URL(url)) + + expect((window.loadURL as LoadURLStub).calledOnceWith(url.toString())).to.be.true; + expect(windowManager.toscaManagerWindows.length).to.equal(1) + expect(windowManager.toscaManagerWindows).to.contain(window) + }); + + it('should create and load a new Topology Modeler window with the specified URL', async () => { + const url = "about:blank" + urlTypeCheckerStub.returns("topologyModeler"); + const window = await windowManager.openWindowFor(new URL(url)) + + expect((window.loadURL as LoadURLStub).calledOnceWith(url.toString())).to.be.true; + expect(windowManager.topologyModelerWindows.length).to.equal(1) + expect(windowManager.topologyModelerWindows).to.contain(window) + }); + + it('should open external links in the user\'s web browser', async () => { + const externalUrl = new URL('http://example.com'); + urlTypeCheckerStub.returns("external"); + const shellOpenExternalStub = sinon.stub(shell, 'openExternal') + .withArgs(externalUrl.toString()) + .resolves() + + await windowManager.openWindowFor(externalUrl); + expect(shellOpenExternalStub.calledOnceWith(externalUrl.toString())).to.be.true; + }); + + it('should throw an error if the specified URL is the main window URL', async () => { + urlTypeCheckerStub.returns("mainWindow"); + try { + await windowManager.openWindowFor(new URL(mainWindowUrl)) + expect.fail("openWindowFor should fail when trying to open the mainWindowUrl") + } catch (e) { /* empty */ } + }); + }); + + describe('closeAllWineryWindows', () => { + it('should close all winery windows', () => { + const destroySpy = sinon.spy(BrowserWindow.prototype, 'destroy'); + + // Open two Tosca Manager windows and one Topology Modeler window + urlTypeCheckerStub.returns("toscaManager"); + windowManager.openWindowFor(new URL('about:blank')); + windowManager.openWindowFor(new URL('about:blank')); + + urlTypeCheckerStub.returns("topologyModeler"); + windowManager.openWindowFor(new URL('about:blank')); + + expect(windowManager.wineryWindows.length).to.equal(3) + + windowManager.closeAllWineryWindows(); + expect(windowManager.wineryWindows).to.be.empty + }); + }); +}); \ No newline at end of file diff --git a/test/main/winery-manager.unit.test.ts b/test/main/winery-manager.unit.test.ts index 3524486..8ba9628 100644 --- a/test/main/winery-manager.unit.test.ts +++ b/test/main/winery-manager.unit.test.ts @@ -7,13 +7,9 @@ import child_process, {ChildProcess} from "child_process"; import {Duplex, PassThrough, Writable} from "stream"; import {expect} from "chai"; import {WineryManager} from "../../src/main/wineryManager"; -import {createLogger, LogEntry, transports} from "winston"; +import {LogEntry} from "winston"; import * as fse from "fs-extra"; -import {wineryApiPath} from "../../src/main/resources"; - -const PORT = 8000 -const wineryApiUrl = new URL(wineryApiPath, `http://localhost:${PORT}`).toString() -const wineryApiUrlMatcher = match((url: URL) => url.toString() === wineryApiUrl) +import {createTestLogger, PORT, wineryApiUrlMatcher} from "./utils"; class MockChildProcess extends ChildProcess { constructor( @@ -27,15 +23,6 @@ class MockChildProcess extends ChildProcess { } -// Helper function: create da test dummy logger to listen for "logged" events -const createTestLogger = () => { - const testTransport = new transports.Stream({stream: new PassThrough()}) - const testLogger = createLogger({ - transports: [testTransport] - }) - return {testTransport, testLogger}; -}; - describe('Winery Manager Unit Tests', () => { let fetchStub: SinonStub; let writeFileStub: SinonStub; diff --git a/test/tsconfig.json b/test/tsconfig.json index 648f077..c7613ca 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -7,7 +7,7 @@ "esModuleInterop": true }, "include": [ - "./**/*.test.ts" + "./**/*.ts" ], "exclude": [ "../node_modules"