对 Spectron 中的 ipcMain 事件做出反应
Reacting to ipcMain events in Spectron
我有一个电子应用程序,它首先启动一个启动器 window(在渲染器进程中),启动多个后台服务。在这些后台服务成功启动后,它将 "services-running"
在其 ipcRenderer
上发送回主进程,主进程又通过关闭启动器 window 并启动主应用程序 window.该事件当然是由 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 弹出主要 window 之前我可以在 shell 上看到 subscribed
,但在达到超时后,第二个测试最终将失败,因此事件肯定会被触发。
我在文档中读到需要启用 nodeIntegration
才能使用 spectron 访问全电子 api,我所有的渲染器进程都以 {nodeIntegration: true}
在各自的 webPreferences
。但是由于我对主进程感兴趣,所以我认为这不适用(或者至少我认为不应该,因为主进程本身就是一个节点进程)。
所以我的主要问题是,我将如何绑定到 ipcMain
事件并将它们包含在我的断言中。另外,我怎么知道启动器 window 何时关闭以及 "main" window 何时打开?
作为奖励,我对 spectron api 有一些理解上的问题。
如果我查看 spectron.d.ts
,Application
的 electron
属性 是 Electron.AllElectron
类型,而 Electron.AllElectron
又是一个MainInterface
直接有ipcMain
属性。所以在我的理解中访问 ipcMain
应该是 app.electron.ipcMain
(这是未定义的),那个遥控器来自哪里以及为什么它在 spectron.d.ts
.
[=58 中不可见=]
SpectronClient
上的方法都是returnPromise<void>
。所以我必须 await
或 then
那些。如果我查看 javascript 示例,它们会链接客户端语句:
return app.client
.waitUntilWindowLoaded()
.getTitle().should.equal('Launcher');
这在打字稿中不起作用,因为你不能链接到 Promise<void>
显然,......这在 js 中如何工作?
所以我分别解决了问题。我将所有内容迁移到 classes 并使用字段/构造函数注入将所有依赖项放入我的 classes 中,这样我就可以模拟它们,包括来自 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);
}
在 class 内,我将在订阅事件时始终使用 this.ipc
。对于单元测试,我有一个 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);
}
}
在为 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,或者使用 public trigger
方法来拍摄 ipc 事件并测试我的 class 是否正确反应它。
对于集成测试,我认识到我不应该关心事件等内部结构(这是在单元测试中完成的),只关心那些结果(windows 关闭和打开)。像这样:
it('should start the launcher', async () => {
await app.client.waitUntilWindowLoaded();
const title: string = await app.client.getTitle();
expect(title).toEqual('Launcher');
});
在接下来的测试中,我等到启动器消失并打开一个新的 window,这样事件就一定成功了,否则就不会发生。
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
只是一个辅助方法,它等待承诺在达到超时后解决或终止测试。
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));
}
我有一个电子应用程序,它首先启动一个启动器 window(在渲染器进程中),启动多个后台服务。在这些后台服务成功启动后,它将 "services-running"
在其 ipcRenderer
上发送回主进程,主进程又通过关闭启动器 window 并启动主应用程序 window.该事件当然是由 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 弹出主要 window 之前我可以在 shell 上看到 subscribed
,但在达到超时后,第二个测试最终将失败,因此事件肯定会被触发。
我在文档中读到需要启用 nodeIntegration
才能使用 spectron 访问全电子 api,我所有的渲染器进程都以 {nodeIntegration: true}
在各自的 webPreferences
。但是由于我对主进程感兴趣,所以我认为这不适用(或者至少我认为不应该,因为主进程本身就是一个节点进程)。
所以我的主要问题是,我将如何绑定到 ipcMain
事件并将它们包含在我的断言中。另外,我怎么知道启动器 window 何时关闭以及 "main" window 何时打开?
作为奖励,我对 spectron api 有一些理解上的问题。
如果我查看
[=58 中不可见=]spectron.d.ts
,Application
的electron
属性 是Electron.AllElectron
类型,而Electron.AllElectron
又是一个MainInterface
直接有ipcMain
属性。所以在我的理解中访问ipcMain
应该是app.electron.ipcMain
(这是未定义的),那个遥控器来自哪里以及为什么它在spectron.d.ts
.SpectronClient
上的方法都是returnPromise<void>
。所以我必须await
或then
那些。如果我查看 javascript 示例,它们会链接客户端语句:
return app.client
.waitUntilWindowLoaded()
.getTitle().should.equal('Launcher');
这在打字稿中不起作用,因为你不能链接到 Promise<void>
显然,......这在 js 中如何工作?
所以我分别解决了问题。我将所有内容迁移到 classes 并使用字段/构造函数注入将所有依赖项放入我的 classes 中,这样我就可以模拟它们,包括来自 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);
}
在 class 内,我将在订阅事件时始终使用 this.ipc
。对于单元测试,我有一个 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);
}
}
在为 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,或者使用 public trigger
方法来拍摄 ipc 事件并测试我的 class 是否正确反应它。
对于集成测试,我认识到我不应该关心事件等内部结构(这是在单元测试中完成的),只关心那些结果(windows 关闭和打开)。像这样:
it('should start the launcher', async () => {
await app.client.waitUntilWindowLoaded();
const title: string = await app.client.getTitle();
expect(title).toEqual('Launcher');
});
在接下来的测试中,我等到启动器消失并打开一个新的 window,这样事件就一定成功了,否则就不会发生。
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
只是一个辅助方法,它等待承诺在达到超时后解决或终止测试。
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));
}