【问题标题】:Reacting to ipcMain events in Spectron对 Spectron 中的 ipcMain 事件作出反应
【发布时间】:2019-11-15 06:14:00
【问题描述】:

我有一个电子应用程序,它首先启动一个启动器窗口(在渲染器进程中),该窗口启动多个后台服务。在这些后台服务成功启动后,它会将其ipcRenderer 上的"services-running" 发送回主进程,主进程反过来通过关闭启动器窗口并启动主应用程序窗口来响应该事件。活动当然是ipcMain.on('services-running',...)收到的

我分别对所有处理程序进行了单元测试,所以这些都很好,现在我想集成测试通过ipcMain 的事件。

这是我的集成测试目前的样子:

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 start the launcher', async  () => {
        await app.client.waitUntilWindowLoaded();
        return app.client.getTitle().should.eventually.equal('Launcher');
    });

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

});

第一个测试工作正常。虽然在主窗口弹出之前我可以在 shell 上看到subscribed,但在达到超时后第二次测试最终会失败,因此肯定会触发该事件。

我在文档中读到需要启用nodeIntegration 才能使用spectron 访问完整的电子API,我所有的渲染器进程都以{nodeIntegration: true} 在它们各自的webPreferences 中启动。但由于我对主进程感兴趣,我认为这不适用(或者至少我认为不应该,因为主进程本身就是一个节点进程)。

所以我的主要问题是,我将如何绑定到 ipcMain 事件并将其包含在我的断言中。另外我怎么知道启动器窗口何时关闭并且“主”窗口已打开?

作为奖励,我对 spectron api 有一些理解问题。

  1. 如果我查看spectron.d.tsApplicationelectron 属性是Electron.AllElectron 类型,而MainInterface 又是MainInterface 并直接具有ipcMain 属性。因此,据我了解,访问ipcMain 应该是app.electron.ipcMain(未定义),远程来自哪里以及为什么它在spectron.d.ts 中不可见。

  2. SpectronClient 上的方法都返回 Promise<void>。所以我必须awaitthen 那些。如果我查看它们链接客户端语句的 javascript 示例:

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

这在 typescript 中不起作用,因为您显然无法链接到 Promise<void>,......这在 js 中如何工作?

【问题讨论】:

  • 你有什么运气吗?我也在尝试收听 ipcMain 事件...
  • @mottosson 我提出了我的解决方法作为答案。我认为这是一个关于如何处理模拟内部库内容的非常通用的解决方案。从那以后一直在使用这种方法,在其他一些其他语言的项目中也是如此,目前它仍然有效。

标签: typescript electron spectron ipcmain


【解决方案1】:

所以我分别解决了这些问题。我将所有内容迁移到类并使用字段/构造函数注入将所有依赖项放入我的类中,因此我可以模拟它们,包括来自电子的东西。

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);
    }

在课程中,订阅事件时我将始终使用this.ipc。对于单元测试,我有一个 FakeIpc 类:

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);
    }
}

在为LauncherRenderer 设置单元测试时,我将FakeIpc 注入渲染器:

 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;
    });

这样我可以在订阅完成后监视 ipc,或者使用公共 trigger 方法来拍摄 ipc 事件并测试我的班级是否对它做出正确反应。

对于集成测试,我认识到我不应该关心内部事件,如事件(这是在单元测试中完成的),只关心那些结果(窗口关闭和打开)。像这样:

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

在下一个测试中,我会等到启动器消失并打开一个新窗口,这样事件肯定会起作用,否则就不会发生。

    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');
    });

waitForPredicate 只是一个辅助方法,它等待 promise 解决或在达到超时后终止测试。

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) {
                // performance.now() would be nicer, but that doesn't work in jasmin tests
                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;
            }
            // timeout
            rej();
        });
}

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

【讨论】:

  • 太好了,非常感谢!只是一个关于 waitForPredicate 的问题。它看起来像一个整洁的实用程序。你如何实施它?您是否以某种方式使用 setInterval() 来检查谓词?
  • @mottosson 我添加了它:)
  • 超级!再次感谢! =)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-27
  • 1970-01-01
  • 1970-01-01
  • 2021-12-26
  • 2015-01-27
  • 2021-12-27
相关资源
最近更新 更多