Responding to ipcMain events within Spectron

I created an electron application that initiates a launcher window (in a renderer process) first, which then starts multiple background services. Once these background services are successfully started, it sends the message "services-running" on its ipcRenderer back to the main process. The main process responds by closing the launcher window and opening the main application window. The event is handled by

ipcMain.on('services-running',...)

I have already unit tested all the handlers individually and now I want to conduct integration tests on the events that pass through ipcMain.

Here is the current structure of my integration test:

import { Application } from 'spectron';
import * as electron from "electron";
import { expect } from 'chai';
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';

let app: Application;

global.before(() => {
    app = new Application({
        path: "" + electron,
        args: ["app/main.js"],
        env: {
            ELECTRON_ENABLE_LOGGING: true,
            ELECTRON_ENABLE_STACK_DUMPING: true,
            NODE_ENV: "integrationtest"
        },
        startTimeout: 20000,
        chromeDriverLogPath: '../chromedriverlog.txt'
    });

    chai.use(chaiAsPromised);
    chai.should();
});

describe('Application', () => {

    before('Start Application', () => {
        return app.start();
    });

    after(() => {
        if(app && app.isRunning()){
            return app.stop();
        }
    });

    it('should initiate the launcher', async  () => {
        await app.client.waitUntilWindowLoaded();
        return app.client.getTitle().should.eventually.equal('Launcher');
    });

    it('should start all services without timing out', async (done) => {
        console.log('subscribed');
        app.electron.remote.ipcMain.on('services-running', () => {
            done();
        });
    });

});

The initial test passes successfully. However, the second test fails after reaching the timeout despite the event being triggered (as indicated by the 'subscribed' log output).

According to the documentation, the nodeIntegration needs to be enabled in order to access the complete electron API with spectron. While all my renderer processes have {nodeIntegration: true} in their respective webPreferences, I believe this setting doesn't apply to the main process since it's a node process itself.

My primary concern is about binding to ipcMain events and incorporating them into my assertions. Additionally, how can I determine when the launcher window closes and the main window opens?

Furthermore, I have some confusion regarding the spectron API:

  1. In the spectron.d.ts file, the electron property of the Application is defined as type Electron.AllElectron, which is a MainInterface containing the ipcMain property directly. My understanding leads me to believe that accessing ipcMain should be via app.electron.ipcMain (however, this returns undefined). Where does 'remote' come from and why is it not visible in the spectron.d.ts file?

  2. All methods in the SpectronClient return Promise<void>. In JavaScript examples, they chain client statements like below:

return app.client
  .waitUntilWindowLoaded()
  .getTitle().should.equal('Launcher');

This chaining method doesn't work in TypeScript due to inability to chain to a Promise<void>. How does this functionality work in JavaScript?

Answer №1

When faced with challenges, I approached each one independently. By transitioning everything to classes and utilizing fields/constructor injection to insert dependencies into my classes, I was able to mock them effectively, even elements originating from electron.

export class LauncherRenderer implements Renderer {

    protected mongo: MongoProcess;
    protected logger: Logger;
    protected ipc: IpcRenderer;

    protected STATUS_LABEL: string = 'status-text';

    constructor() {
        this.ipc = ipcRenderer;

        this.mongo = new MongoProcess(this.ipc);

        this.logger = new Logger('launcher', this.ipc);
    }

Within the class, I consistently utilize this.ipc when subscribing to events. For unit tests, I have a specialized FakeIpc class:

import { EventEmitter } from 'events';

export class FakeIpc {

    public emitter: EventEmitter = new EventEmitter();

    public send(channel: string, message?: any): void { }

    public on(event: string, listener: () => void): void {
        this.emitter.on(event, listener);
    }

    public emit(event: string): void {
        this.emitter.emit(event);
    }
}

During the setup of Unit tests for LauncherRenderer, I inject the FakeIpc into the renderer:

 beforeEach(() => {
        fakeIpc = new FakeIpc();
        spyOn(fakeIpc, 'on').and.callThrough();
        spyOn(fakeIpc, 'send').and.callThrough();

        mongoMock = createSpyObj('mongoMock', ['start', 'stop', 'forceStop']);

        underTest = new LauncherRenderer();

        underTest.mongo = mongoMock;
        underTest.ipc = fakeIpc;
    });

This approach allows me to monitor the ipc for event subscriptions, or employ the public trigger method to trigger ipc events and verify if my class responds appropriately.

For integration tests, I identified that focusing on outcomes like window closures and openings – rather than internal mechanisms such as events (reserved for unit tests) – is crucial. For instance:

    it('should start the launcher', async () => {
        await app.client.waitUntilWindowLoaded();
        const title: string = await app.client.getTitle();
        expect(title).toEqual('Launcher');
    });

In the subsequent test, I wait for the launcher to disappear and a new window to open, ensuring the events have functioned correctly:

    it('should open main window after all services started within 120s', async () => {
        let handles: any = await app.client.windowHandles();

        try {
            await Utils.waitForPredicate(async () => {
                handles = await app.client.windowHandles();
                return Promise.resolve(handles.value.length === 2);
            }, 120000);
            await app.client.windowByIndex(1);
        } catch (err) {
            return Promise.reject(err);
        }

        const title: string = await app.client.getTitle();
        expect(title).toEqual('Main Window');
    });

The waitForPredicate function acts as a helper method that waits for a promise to resolve or aborts the test after a specified timeout:

public static waitForPredicate(
    predicate: () => Promise<boolean>,
    timeout: number = 10000,
    interval: number = 1000,
    expectation: boolean = true): Promise<void> {
        return new Promise<any>(async (res, rej) => {
            let currentTime: number = 0;
            while (currentTime < timeout) {
                const t0: number = Date.now();
                const readyState: boolean | void = await predicate().catch(() => rej());
                if (readyState === expectation) {
                    res();
                    return;
                }
                await Utils.sleep(interval);
                const t1: number = Date.now();
                currentTime += t1 - t0;
            }
            rej();
        });
}

public static sleep(ms: number): Promise<void> {
    if (this.skipSleep) {
        return Promise.resolve();
    }
    return new Promise<void>((res) => setTimeout(res, ms));
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

I keep receiving multiple header errors from ExpressJS even though I am positive that I am only sending a single header

Can someone please help with the issue I'm facing in the code below: router.put("/:_id", async (req: Request, res: Response) => { try { // Create the updated artist variable const artist: IArtist = req.body; const updatedArt ...

What is the reason for not hashing the password in this system?

My password hashing code using Argon2 is shown below: import { ForbiddenException, Injectable } from '@nestjs/common'; import { PrismaService } from 'src/prisma/prisma.service'; import { AuthDto } from './dto'; import * as arg ...

Template literal types in TypeScript and Visual Studio Code: An unbeatable duo

I'm encountering an issue while attempting to utilize literal types in Visual Studio Code. When following the example from the documentation, https://i.stack.imgur.com/V6njl.png eslint is flagging an error in the code (Parsing error: Type expected.e ...

Angular messaging service error TS2769: There is no overload that fits this call

Currently, I am attempting to utilize a messenger service to send products to the cart component. Within the 'Product' class, there are various product attributes stored. Although I managed to successfully log the product to the console in the ca ...

Test your unit by providing feedback to the personalized modal pop-up

Currently, I am working on a unit test within Angular where I need to evaluate the functionality of the save button. My goal is to have the 'save' option automatically selected when the user clicks on the button, and then proceed to execute the s ...

Data entered into DynamoDb using typedORM displays inaccurate Entity details

Whenever I add new entries to my local dynamoDb table using typeDORM within a lambda function, it seems to save the record with the incorrect entity information. For example, the GSI1PK GSI1: { partitionKey: 'PRO#{{primary_key}}', ...

Exploring the Behavior of Typescript Modules

When working with the module foo, calling bar.factoryMethod('Blue') will result in an instance of WidgetBlue. module foo { export class bar { factoryMethod(classname: string): WidgetBase { return new foo["Widget" + classname](); ...

Issue encountered: "TypeError: .... is not a function" arises while attempting to utilize a component function within the template

Within my component, I am attempting to dynamically provide the dimensions of my SVG viewBox by injecting them from my bootstrap in main.ts. import {Component} from 'angular2/core'; import {CircleComponent} from './circle.component'; i ...

You must use the 'new' keyword in order to invoke the class constructor

Although similar questions have been asked before, my situation differs from the typical scenarios. I have a basic base class named CObject structured as follows: export class CObject extends BaseObject { constructor() { super(); } sta ...

The index type in TypeScript's keyof function is overly broad

Sorry if this question has been addressed before, but I'm having trouble finding the right search terms. Feel free to correct my question if necessary. This is what I have: type RowData = Record<string, unknown> & {id: string}; type Column&l ...

What is the best way to assign a value to an option element for ordering purposes?

My select element is being populated with fruits from a database, using the following code: <select name="fruitsOption" id="fruitsOptionId" ngModel #fruitRef="ngModel"> <option *ngFor="let fruit of fruits">{{fruit}}</option> </selec ...

Angular: Clicking on a component triggers the reinitialization of all instances of that particular component

Imagine a page filled with project cards, each equipped with a favorite button. Clicking the button will mark the project as a favorite and change the icon accordingly. The issue arises when clicking on the favorite button causes all project cards to rese ...

Switching Theme Dynamically in a Multi-tenant Next.js + Tailwind App

I'm currently developing a Next.js + Tailwind application that supports multiple tenants and allows each tenant to easily switch styles or themes. I've been struggling with the idea of how to implement this feature without requiring a rebuild of ...

A comprehensive guide on using HttpClient in Angular

After following the tutorial on the angular site (https://angular.io/guide/http), I'm facing difficulties in achieving my desired outcome due to an error that seems unclear to me. I've stored my text file in the assets folder and created a config ...

Problem with Angular 2 Typings Paths in Typescript

Currently, I am in the process of learning how to create a Gulp build process with Angular 2 and Typescript. Following the Quick Start guide has allowed me to get everything up and running smoothly. However, I have decided to experiment with different fold ...

What is the best way to retrieve class properties within an input change listener in Angular?

I am new to Angular and have a question regarding scopes. While I couldn't find an exact match for my question in previous queries, I will try to clarify it with the code snippet below: @Component({ selector: 'item-selector&apos ...

Object.assign versus the assignment operator (i.e. =) when working with React components

Just a quick question: I've come across some answers like this one discussing the variances between Object.assign and the assignment operator (i.e. =) and grasp all the points made such as object copying versus address assignment. I'm trying to ...

Achieving selective exclusion of specific keys/values while iterating through an array and rendering them on a table using Angular

Currently facing a hurdle and seeking advice I am developing an angular application that fetches data from an API and presents it on the page The service I am utilizing is named "Api Service" which uses HTTPClient to make API calls apiservice.service.ts ...

Implementing delayed loading of Angular modules without relying on the route prefix

In my application, I am using lazy loading to load a module called lazy. The module is lazily loaded like this: { path:'lazy', loadChildren: './lazy/lazy.module#LazyModule' } Within the lazy module, there are several routes def ...

Encountering the following error message: "Received error: `../node_modules/electron/index.js:1:0 Module not found: Can't resolve 'fs'` while integrating next.js with electron template."

I am utilizing the electron template with next.js, and I am trying to import ipcRenderer in my pages/index.tsx file. Below is the crucial code snippet: ... import { ipcRenderer } from 'electron'; function Home() { useEffect(() => { ip ...