fix: 🐛 migrate IPC to service-based window for reliable testing
This resolves a longstanding issue where IPC tests rely on the real window.postMessage and MessageChannel APIs. These are evented and asynchronous, resulting in nondeterministic tests. This commit updates tests with synchronous mocked APIs.
This commit is contained in:
4 files changed
+105
-45
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import Service from '@ember/service';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { Promise } from 'rsvp';
|
||||
|
||||
/**
|
||||
@@ -58,7 +58,7 @@ export class IPCRequest {
|
||||
this.method = method;
|
||||
this.payload = payload;
|
||||
this.origin = window?.location?.origin;
|
||||
this.#channel = new MessageChannel();
|
||||
this.#channel = new window.MessageChannel();
|
||||
this.#promise = new Promise((resolve, reject) => {
|
||||
this.#channel.port1.onmessage = (event) => {
|
||||
this.close();
|
||||
@@ -124,18 +124,9 @@ export class IPCRequest {
|
||||
* }
|
||||
*/
|
||||
export default class IpcService extends Service {
|
||||
// =attributes
|
||||
// =services
|
||||
|
||||
/**
|
||||
* Looks up the window object indirectly.
|
||||
* @type {Window}
|
||||
*/
|
||||
get window() {
|
||||
// The Ember way of accessing globals...
|
||||
const document = getOwner(this).lookup('service:-document').documentElement;
|
||||
// defaultView === window, but without using globals directly
|
||||
return document.parentNode.defaultView;
|
||||
}
|
||||
@service('browser/window') window;
|
||||
|
||||
// =methods
|
||||
|
||||
|
||||
@@ -19,15 +19,17 @@ import {
|
||||
// authenticateSession,
|
||||
invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
import { setupBrowserFakes } from 'ember-browser-services/test-support';
|
||||
import WindowMockIPC from '../helpers/window-mock-ipc';
|
||||
import config from '../../config/environment';
|
||||
|
||||
module('Acceptance | origin', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupBrowserFakes(hooks, { window: true });
|
||||
setupMirage(hooks);
|
||||
|
||||
const currentOrigin = window.location.origin;
|
||||
let mockIPC;
|
||||
let messageHandler;
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
@@ -104,40 +106,12 @@ module('Acceptance | origin', function (hooks) {
|
||||
urls.projects = `${urls.scopes.global}/projects`;
|
||||
urls.targets = `${urls.projects}/targets`;
|
||||
|
||||
class MockIPC {
|
||||
origin = null;
|
||||
|
||||
invoke(method, payload) {
|
||||
return this[method](payload);
|
||||
}
|
||||
|
||||
getOrigin() {
|
||||
return this.origin;
|
||||
}
|
||||
|
||||
setOrigin(origin) {
|
||||
this.origin = origin;
|
||||
return this.origin;
|
||||
}
|
||||
|
||||
resetOrigin() {}
|
||||
}
|
||||
|
||||
mockIPC = new MockIPC();
|
||||
messageHandler = async function (event) {
|
||||
if (event.origin !== currentOrigin) return;
|
||||
const { method, payload } = event.data;
|
||||
if (method) {
|
||||
const response = await mockIPC.invoke(method, payload);
|
||||
event.ports[0].postMessage(response);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
// Mock the postMessage interface used by IPC.
|
||||
this.owner.register('service:browser/window', WindowMockIPC);
|
||||
mockIPC = this.owner.lookup('service:browser/window').mockIPC;
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
window.removeEventListener('message', messageHandler);
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import Service from '@ember/service';
|
||||
|
||||
class MockIPC {
|
||||
origin = null;
|
||||
|
||||
invoke(method, payload) {
|
||||
return this[method](payload);
|
||||
}
|
||||
|
||||
getOrigin() {
|
||||
return this.origin;
|
||||
}
|
||||
|
||||
setOrigin(origin) {
|
||||
this.origin = origin;
|
||||
return this.origin;
|
||||
}
|
||||
|
||||
resetOrigin() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock window service with support for simplified postMessage and
|
||||
* MessageChannel APIs. This mock implements just enough functionality
|
||||
* to support the IPC mechanism.
|
||||
*
|
||||
* This mock is necessary to guarantee synchronous and deterministic
|
||||
* operation. The window's own API is evented and asychronous, which
|
||||
* cannot be relied on for testing purposes.
|
||||
*
|
||||
* @todo refactor mock postMessage and MessageChannel for reusability
|
||||
* outside of tests
|
||||
*/
|
||||
export default class extends Service {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
mockIPC = new MockIPC();
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
MessageChannel = class MessageChannel {
|
||||
constructor() {
|
||||
this.port1.postMessage = this.port1.postMessage.bind(this);
|
||||
this.port2.postMessage = this.port2.postMessage.bind(this);
|
||||
}
|
||||
port1 = {
|
||||
postMessage() {},
|
||||
close() {},
|
||||
};
|
||||
port2 = {
|
||||
postMessage(data) {
|
||||
this.port1.onmessage({ data });
|
||||
},
|
||||
close() {},
|
||||
};
|
||||
};
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
postMessage(data, origin, ports) {
|
||||
if (origin !== window.location.origin) return;
|
||||
const { method, payload } = data;
|
||||
if (method) {
|
||||
const response = this.mockIPC.invoke(method, payload);
|
||||
ports[0].postMessage(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,23 @@ import sinon from 'sinon';
|
||||
module('Unit | Service | ipc', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
class MessageChannelMock {
|
||||
constructor() {
|
||||
this.port1.postMessage = this.port1.postMessage.bind(this);
|
||||
this.port2.postMessage = this.port2.postMessage.bind(this);
|
||||
}
|
||||
port1 = {
|
||||
postMessage() {},
|
||||
close() {},
|
||||
};
|
||||
port2 = {
|
||||
postMessage(data) {
|
||||
this.port1.onmessage({ data });
|
||||
},
|
||||
close() {},
|
||||
};
|
||||
}
|
||||
|
||||
hooks.beforeEach(() => {
|
||||
// IPC requests talk to the main process indirectly via window.postMessage
|
||||
// proxy layer (mediated by preload.js).
|
||||
@@ -31,6 +48,7 @@ module('Unit | Service | ipc', function (hooks) {
|
||||
assert.equal(request.method, 'hello');
|
||||
assert.equal(request.payload, 'world');
|
||||
},
|
||||
MessageChannel: MessageChannelMock,
|
||||
};
|
||||
new IPCRequest('hello', 'world', myWindow);
|
||||
});
|
||||
@@ -45,6 +63,7 @@ module('Unit | Service | ipc', function (hooks) {
|
||||
test: 42,
|
||||
});
|
||||
},
|
||||
MessageChannel: MessageChannelMock,
|
||||
};
|
||||
const request = new IPCRequest('hello', 'world', myWindow);
|
||||
const response = await request;
|
||||
@@ -65,6 +84,7 @@ module('Unit | Service | ipc', function (hooks) {
|
||||
// Post back an error on the response port
|
||||
port.postMessage(new Error(stringifiedPayload));
|
||||
},
|
||||
MessageChannel: MessageChannelMock,
|
||||
};
|
||||
try {
|
||||
await new IPCRequest('hello', 'world', myWindow);
|
||||
|
||||
Reference in new issue
Block a user