mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Serialize messages in channels to make IPC not blow up
Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
bafa7377f6
commit
077b2089b3
15
src/common/utils/tentative-parse-json.ts
Normal file
15
src/common/utils/tentative-parse-json.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { pipeline } from "@ogre-tools/fp";
|
||||||
|
import { defaultTo } from "lodash/fp";
|
||||||
|
import { withErrorSuppression } from "./with-error-suppression/with-error-suppression";
|
||||||
|
|
||||||
|
export const tentativeParseJson = (toBeParsed: any) => pipeline(
|
||||||
|
toBeParsed,
|
||||||
|
withErrorSuppression(JSON.parse),
|
||||||
|
defaultTo(toBeParsed),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
15
src/common/utils/tentative-stringify-json.ts
Normal file
15
src/common/utils/tentative-stringify-json.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { pipeline } from "@ogre-tools/fp";
|
||||||
|
import { defaultTo } from "lodash/fp";
|
||||||
|
import { withErrorSuppression } from "./with-error-suppression/with-error-suppression";
|
||||||
|
|
||||||
|
export const tentativeStringifyJson = (toBeParsed: any) => pipeline(
|
||||||
|
toBeParsed,
|
||||||
|
withErrorSuppression(JSON.stringify),
|
||||||
|
defaultTo(toBeParsed),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
@ -6,6 +6,8 @@ import { getInjectable } from "@ogre-tools/injectable";
|
|||||||
import type { IpcMainEvent } from "electron";
|
import type { IpcMainEvent } from "electron";
|
||||||
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
|
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
|
||||||
import { enlistMessageChannelListenerInjectionToken } from "../../../common/channel/enlist-message-channel-listener-injection-token";
|
import { enlistMessageChannelListenerInjectionToken } from "../../../common/channel/enlist-message-channel-listener-injection-token";
|
||||||
|
import { pipeline } from "@ogre-tools/fp";
|
||||||
|
import { tentativeParseJson } from "../../../common/utils/tentative-parse-json";
|
||||||
|
|
||||||
const enlistMessageChannelListenerInjectable = getInjectable({
|
const enlistMessageChannelListenerInjectable = getInjectable({
|
||||||
id: "enlist-message-channel-listener-for-main",
|
id: "enlist-message-channel-listener-for-main",
|
||||||
@ -14,8 +16,13 @@ const enlistMessageChannelListenerInjectable = getInjectable({
|
|||||||
const ipcMain = di.inject(ipcMainInjectable);
|
const ipcMain = di.inject(ipcMainInjectable);
|
||||||
|
|
||||||
return ({ channel, handler }) => {
|
return ({ channel, handler }) => {
|
||||||
const nativeOnCallback = (_: IpcMainEvent, message: unknown) =>
|
const nativeOnCallback = (_: IpcMainEvent, message: unknown) => {
|
||||||
handler(message);
|
pipeline(
|
||||||
|
message,
|
||||||
|
tentativeParseJson,
|
||||||
|
handler,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
ipcMain.on(channel.id, nativeOnCallback);
|
ipcMain.on(channel.id, nativeOnCallback);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
|
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
|
||||||
|
import type { EnlistMessageChannelListener } from "../../../common/channel/enlist-message-channel-listener-injection-token";
|
||||||
|
import { enlistMessageChannelListenerInjectionToken } from "../../../common/channel/enlist-message-channel-listener-injection-token";
|
||||||
|
import type { IpcMain, IpcMainEvent } from "electron";
|
||||||
|
|
||||||
|
describe("enlist message channel listener in main", () => {
|
||||||
|
let enlistMessageChannelListener: EnlistMessageChannelListener;
|
||||||
|
let ipcMainStub: IpcMain;
|
||||||
|
let onMock: jest.Mock;
|
||||||
|
let offMock: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
onMock = jest.fn();
|
||||||
|
offMock = jest.fn();
|
||||||
|
|
||||||
|
ipcMainStub = {
|
||||||
|
on: onMock,
|
||||||
|
off: offMock,
|
||||||
|
} as unknown as IpcMain;
|
||||||
|
|
||||||
|
di.override(ipcMainInjectable, () => ipcMainStub);
|
||||||
|
|
||||||
|
enlistMessageChannelListener = di.inject(
|
||||||
|
enlistMessageChannelListenerInjectionToken,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when called", () => {
|
||||||
|
let handlerMock: jest.Mock;
|
||||||
|
let disposer: () => void;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
handlerMock = jest.fn();
|
||||||
|
|
||||||
|
disposer = enlistMessageChannelListener({
|
||||||
|
channel: { id: "some-channel-id" },
|
||||||
|
handler: handlerMock,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call handler yet", () => {
|
||||||
|
expect(handlerMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("registers the listener", () => {
|
||||||
|
expect(onMock).toHaveBeenCalledWith(
|
||||||
|
"some-channel-id",
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not de-register the listener yet", () => {
|
||||||
|
expect(offMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when message arrives", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcMainEvent, "some-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls the handler with the message", () => {
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith("some-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when disposing the listener, de-registers the listener", () => {
|
||||||
|
disposer();
|
||||||
|
|
||||||
|
expect(offMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given number as message, when message arrives, calls the handler with the message", () => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcMainEvent, 42);
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given boolean as message, when message arrives, calls the handler with the message", () => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcMainEvent, true);
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given stringified object as message, when message arrives, calls the handler with the message", () => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcMainEvent, JSON.stringify({ some: "object" }));
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith({ some: "object" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -6,6 +6,9 @@ import { getInjectable } from "@ogre-tools/injectable";
|
|||||||
import type { IpcMainInvokeEvent } from "electron";
|
import type { IpcMainInvokeEvent } from "electron";
|
||||||
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
|
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
|
||||||
import { enlistRequestChannelListenerInjectionToken } from "../../../common/channel/enlist-request-channel-listener-injection-token";
|
import { enlistRequestChannelListenerInjectionToken } from "../../../common/channel/enlist-request-channel-listener-injection-token";
|
||||||
|
import { pipeline } from "@ogre-tools/fp";
|
||||||
|
import { tentativeParseJson } from "../../../common/utils/tentative-parse-json";
|
||||||
|
import { tentativeStringifyJson } from "../../../common/utils/tentative-stringify-json";
|
||||||
|
|
||||||
const enlistRequestChannelListenerInjectable = getInjectable({
|
const enlistRequestChannelListenerInjectable = getInjectable({
|
||||||
id: "enlist-request-channel-listener-for-main",
|
id: "enlist-request-channel-listener-for-main",
|
||||||
@ -14,7 +17,8 @@ const enlistRequestChannelListenerInjectable = getInjectable({
|
|||||||
const ipcMain = di.inject(ipcMainInjectable);
|
const ipcMain = di.inject(ipcMainInjectable);
|
||||||
|
|
||||||
return ({ channel, handler }) => {
|
return ({ channel, handler }) => {
|
||||||
const nativeHandleCallback = (_: IpcMainInvokeEvent, message: unknown) => handler(message);
|
const nativeHandleCallback = (_: IpcMainInvokeEvent, request: unknown) =>
|
||||||
|
pipeline(request, tentativeParseJson, handler, tentativeStringifyJson);
|
||||||
|
|
||||||
ipcMain.handle(channel.id, nativeHandleCallback);
|
ipcMain.handle(channel.id, nativeHandleCallback);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
|
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
|
||||||
|
import type { IpcMain, IpcMainInvokeEvent } from "electron";
|
||||||
|
import type { EnlistRequestChannelListener } from "../../../common/channel/enlist-request-channel-listener-injection-token";
|
||||||
|
import { enlistRequestChannelListenerInjectionToken } from "../../../common/channel/enlist-request-channel-listener-injection-token";
|
||||||
|
import { getPromiseStatus } from "../../../common/test-utils/get-promise-status";
|
||||||
|
import type { AsyncFnMock } from "@async-fn/jest";
|
||||||
|
import asyncFn from "@async-fn/jest";
|
||||||
|
|
||||||
|
describe("enlist request channel listener in main", () => {
|
||||||
|
let enlistRequestChannelListener: EnlistRequestChannelListener;
|
||||||
|
let ipcMainStub: IpcMain;
|
||||||
|
let handleMock: jest.Mock;
|
||||||
|
let offMock: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
handleMock = jest.fn();
|
||||||
|
offMock = jest.fn();
|
||||||
|
|
||||||
|
ipcMainStub = {
|
||||||
|
handle: handleMock,
|
||||||
|
off: offMock,
|
||||||
|
} as unknown as IpcMain;
|
||||||
|
|
||||||
|
di.override(ipcMainInjectable, () => ipcMainStub);
|
||||||
|
|
||||||
|
enlistRequestChannelListener = di.inject(
|
||||||
|
enlistRequestChannelListenerInjectionToken,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when called", () => {
|
||||||
|
let handlerMock: AsyncFnMock<(message: any) => any>;
|
||||||
|
let disposer: () => void;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
handlerMock = asyncFn();
|
||||||
|
|
||||||
|
disposer = enlistRequestChannelListener({
|
||||||
|
channel: { id: "some-channel-id" },
|
||||||
|
handler: handlerMock,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call handler yet", () => {
|
||||||
|
expect(handlerMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("registers the listener", () => {
|
||||||
|
expect(handleMock).toHaveBeenCalledWith(
|
||||||
|
"some-channel-id",
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not de-register the listener yet", () => {
|
||||||
|
expect(offMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when request arrives", () => {
|
||||||
|
let actualPromise: Promise<any>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
actualPromise = handleMock.mock.calls[0][1](
|
||||||
|
{} as IpcMainInvokeEvent,
|
||||||
|
"some-request",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls the handler with the request", () => {
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith("some-request");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resolve yet", async () => {
|
||||||
|
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||||
|
|
||||||
|
expect(promiseStatus.fulfilled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when handler resolves with response, listener resolves with the response", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await handlerMock.resolve("some-response");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves with the response", async () => {
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toBe('"some-response"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when disposing the listener, de-registers the listener", () => {
|
||||||
|
disposer();
|
||||||
|
|
||||||
|
expect(offMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given number as response, when handler resolves with response, listener resolves with stringified response", async () => {
|
||||||
|
await handlerMock.resolve(42);
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toBe("42");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given boolean as response, when handler resolves with response, listener resolves with stringified response", async () => {
|
||||||
|
await handlerMock.resolve(true);
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toBe("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given object as response, when handler resolves with response, listener resolves with stringified response", async () => {
|
||||||
|
await handlerMock.resolve({ some: "object" });
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toBe(JSON.stringify({ some: "object" }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given number as request, when request arrives, calls the handler with the request", () => {
|
||||||
|
handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, 42);
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given boolean as request, when request arrives, calls the handler with the request", () => {
|
||||||
|
handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, true);
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given stringified object as request, when request arrives, calls the handler with the request", () => {
|
||||||
|
handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, JSON.stringify({ some: "object" }));
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith({ some: "object" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -8,6 +8,7 @@ import { getInjectable } from "@ogre-tools/injectable";
|
|||||||
import { filter } from "lodash/fp";
|
import { filter } from "lodash/fp";
|
||||||
import { messageToChannelInjectionToken } from "../../common/channel/message-to-channel-injection-token";
|
import { messageToChannelInjectionToken } from "../../common/channel/message-to-channel-injection-token";
|
||||||
import type { MessageChannel } from "../../common/channel/message-channel-injection-token";
|
import type { MessageChannel } from "../../common/channel/message-channel-injection-token";
|
||||||
|
import { tentativeStringifyJson } from "../../common/utils/tentative-stringify-json";
|
||||||
|
|
||||||
const messageToChannelInjectable = getInjectable({
|
const messageToChannelInjectable = getInjectable({
|
||||||
id: "message-to-channel",
|
id: "message-to-channel",
|
||||||
@ -18,13 +19,16 @@ const messageToChannelInjectable = getInjectable({
|
|||||||
// TODO: Figure out way to improve typing in internals
|
// TODO: Figure out way to improve typing in internals
|
||||||
// Notice that this should be injected using "messageToChannelInjectionToken" which is typed correctly.
|
// Notice that this should be injected using "messageToChannelInjectionToken" which is typed correctly.
|
||||||
return (channel: MessageChannel<any>, message?: unknown) => {
|
return (channel: MessageChannel<any>, message?: unknown) => {
|
||||||
|
const stringifiedMessage = tentativeStringifyJson(message);
|
||||||
|
|
||||||
|
|
||||||
const visibleWindows = pipeline(
|
const visibleWindows = pipeline(
|
||||||
getAllLensWindows(),
|
getAllLensWindows(),
|
||||||
filter((lensWindow) => !!lensWindow.visible),
|
filter((lensWindow) => !!lensWindow.visible),
|
||||||
);
|
);
|
||||||
|
|
||||||
visibleWindows.forEach((lensWindow) =>
|
visibleWindows.forEach((lensWindow) =>
|
||||||
lensWindow.send({ channel: channel.id, data: message ? [message] : [] }),
|
lensWindow.send({ channel: channel.id, data: stringifiedMessage ? [stringifiedMessage] : [] }),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
167
src/main/channel/message-to-channel.test.ts
Normal file
167
src/main/channel/message-to-channel.test.ts
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { MessageToChannel } from "../../common/channel/message-to-channel-injection-token";
|
||||||
|
import { messageToChannelInjectionToken } from "../../common/channel/message-to-channel-injection-token";
|
||||||
|
import closeAllWindowsInjectable from "../start-main-application/lens-window/hide-all-windows/close-all-windows.injectable";
|
||||||
|
import type { MessageChannel } from "../../common/channel/message-channel-injection-token";
|
||||||
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
|
import createLensWindowInjectable from "../start-main-application/lens-window/application-window/create-lens-window.injectable";
|
||||||
|
import type { LensWindow } from "../start-main-application/lens-window/application-window/lens-window-injection-token";
|
||||||
|
import { lensWindowInjectionToken } from "../start-main-application/lens-window/application-window/lens-window-injection-token";
|
||||||
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import sendToChannelInElectronBrowserWindowInjectable from "../start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable";
|
||||||
|
|
||||||
|
describe("message to channel from main", () => {
|
||||||
|
let messageToChannel: MessageToChannel;
|
||||||
|
let someTestWindow: LensWindow;
|
||||||
|
let someOtherTestWindow: LensWindow;
|
||||||
|
let sendToChannelInBrowserMock: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
sendToChannelInBrowserMock = jest.fn();
|
||||||
|
di.override(sendToChannelInElectronBrowserWindowInjectable, () => sendToChannelInBrowserMock);
|
||||||
|
|
||||||
|
someTestWindow = createTestWindow(di, "some-test-window-id");
|
||||||
|
someOtherTestWindow = createTestWindow(di, "some-other-test-window-id");
|
||||||
|
|
||||||
|
messageToChannel = di.inject(messageToChannelInjectionToken);
|
||||||
|
|
||||||
|
const closeAllWindows = di.inject(closeAllWindowsInjectable);
|
||||||
|
|
||||||
|
closeAllWindows();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given no visible windows, when messaging to channel, does not message to any window", () => {
|
||||||
|
messageToChannel(someChannel, "some-message");
|
||||||
|
|
||||||
|
expect(sendToChannelInBrowserMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given visible window", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await someTestWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when messaging to channel, messages to window", () => {
|
||||||
|
messageToChannel(someChannel, "some-message");
|
||||||
|
|
||||||
|
expect(sendToChannelInBrowserMock.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
|
||||||
|
{
|
||||||
|
channel: "some-channel",
|
||||||
|
data: ['"some-message"'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given boolean as message, when messaging to channel, messages to window with stringified message", () => {
|
||||||
|
messageToChannel(someChannel, true);
|
||||||
|
|
||||||
|
expect(sendToChannelInBrowserMock.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
|
||||||
|
{
|
||||||
|
channel: "some-channel",
|
||||||
|
data: ["true"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given number as message, when messaging to channel, messages to window with stringified message", () => {
|
||||||
|
messageToChannel(someChannel, 42);
|
||||||
|
|
||||||
|
expect(sendToChannelInBrowserMock.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
|
||||||
|
{
|
||||||
|
channel: "some-channel",
|
||||||
|
data: ["42"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given object as message, when messaging to channel, messages to window with stringified message", () => {
|
||||||
|
messageToChannel(someChannel, { some: "object" });
|
||||||
|
|
||||||
|
expect(sendToChannelInBrowserMock.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
|
||||||
|
{
|
||||||
|
channel: "some-channel",
|
||||||
|
data: [JSON.stringify({ some: "object" })],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given multiple visible windows, when messaging to channel, messages to window", async () => {
|
||||||
|
await someTestWindow.show();
|
||||||
|
await someOtherTestWindow.show();
|
||||||
|
|
||||||
|
messageToChannel(someChannel, "some-message");
|
||||||
|
|
||||||
|
expect(sendToChannelInBrowserMock.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
|
||||||
|
{
|
||||||
|
channel: "some-channel",
|
||||||
|
data: ['"some-message"'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
|
||||||
|
{
|
||||||
|
channel: "some-channel",
|
||||||
|
data: ['"some-message"'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const someChannel: MessageChannel<any> = { id: "some-channel" };
|
||||||
|
|
||||||
|
const createTestWindow = (di: DiContainer, id: string) => {
|
||||||
|
const testWindowInjectable = getInjectable({
|
||||||
|
id,
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const createLensWindow = di.inject(createLensWindowInjectable);
|
||||||
|
|
||||||
|
return createLensWindow({
|
||||||
|
id,
|
||||||
|
title: "Some test window",
|
||||||
|
defaultHeight: 42,
|
||||||
|
defaultWidth: 42,
|
||||||
|
getContentUrl: () => "some-content-url",
|
||||||
|
resizable: true,
|
||||||
|
windowFrameUtilitiesAreShown: false,
|
||||||
|
centered: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
injectionToken: lensWindowInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
di.register(testWindowInjectable);
|
||||||
|
|
||||||
|
return di.inject(testWindowInjectable);
|
||||||
|
};
|
||||||
@ -6,6 +6,8 @@ import ipcRendererInjectable from "../ipc-renderer.injectable";
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import type { IpcRendererEvent } from "electron";
|
import type { IpcRendererEvent } from "electron";
|
||||||
import { enlistMessageChannelListenerInjectionToken } from "../../../common/channel/enlist-message-channel-listener-injection-token";
|
import { enlistMessageChannelListenerInjectionToken } from "../../../common/channel/enlist-message-channel-listener-injection-token";
|
||||||
|
import { tentativeParseJson } from "../../../common/utils/tentative-parse-json";
|
||||||
|
import { pipeline } from "@ogre-tools/fp";
|
||||||
|
|
||||||
const enlistMessageChannelListenerInjectable = getInjectable({
|
const enlistMessageChannelListenerInjectable = getInjectable({
|
||||||
id: "enlist-message-channel-listener-for-renderer",
|
id: "enlist-message-channel-listener-for-renderer",
|
||||||
@ -14,8 +16,13 @@ const enlistMessageChannelListenerInjectable = getInjectable({
|
|||||||
const ipcRenderer = di.inject(ipcRendererInjectable);
|
const ipcRenderer = di.inject(ipcRendererInjectable);
|
||||||
|
|
||||||
return ({ channel, handler }) => {
|
return ({ channel, handler }) => {
|
||||||
const nativeCallback = (_: IpcRendererEvent, message: unknown) =>
|
const nativeCallback = (_: IpcRendererEvent, message: unknown) => {
|
||||||
handler(message);
|
pipeline(
|
||||||
|
message,
|
||||||
|
tentativeParseJson,
|
||||||
|
handler,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
ipcRenderer.on(channel.id, nativeCallback);
|
ipcRenderer.on(channel.id, nativeCallback);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
|
import type { EnlistMessageChannelListener } from "../../../common/channel/enlist-message-channel-listener-injection-token";
|
||||||
|
import { enlistMessageChannelListenerInjectionToken } from "../../../common/channel/enlist-message-channel-listener-injection-token";
|
||||||
|
import type { IpcRendererEvent, IpcRenderer } from "electron";
|
||||||
|
import ipcRendererInjectable from "../ipc-renderer.injectable";
|
||||||
|
|
||||||
|
describe("enlist message channel listener in renderer", () => {
|
||||||
|
let enlistMessageChannelListener: EnlistMessageChannelListener;
|
||||||
|
let ipcRendererStub: IpcRenderer;
|
||||||
|
let onMock: jest.Mock;
|
||||||
|
let offMock: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
onMock = jest.fn();
|
||||||
|
offMock = jest.fn();
|
||||||
|
|
||||||
|
ipcRendererStub = {
|
||||||
|
on: onMock,
|
||||||
|
off: offMock,
|
||||||
|
} as unknown as IpcRenderer;
|
||||||
|
|
||||||
|
di.override(ipcRendererInjectable, () => ipcRendererStub);
|
||||||
|
|
||||||
|
enlistMessageChannelListener = di.inject(
|
||||||
|
enlistMessageChannelListenerInjectionToken,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when called", () => {
|
||||||
|
let handlerMock: jest.Mock;
|
||||||
|
let disposer: () => void;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
handlerMock = jest.fn();
|
||||||
|
|
||||||
|
disposer = enlistMessageChannelListener({
|
||||||
|
channel: { id: "some-channel-id" },
|
||||||
|
handler: handlerMock,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call handler yet", () => {
|
||||||
|
expect(handlerMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("registers the listener", () => {
|
||||||
|
expect(onMock).toHaveBeenCalledWith(
|
||||||
|
"some-channel-id",
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not de-register the listener yet", () => {
|
||||||
|
expect(offMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when message arrives", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcRendererEvent, "some-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls the handler with the message", () => {
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith("some-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when disposing the listener, de-registers the listener", () => {
|
||||||
|
disposer();
|
||||||
|
|
||||||
|
expect(offMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given number as message, when message arrives, calls the handler with the message", () => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcRendererEvent, 42);
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given boolean as message, when message arrives, calls the handler with the message", () => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcRendererEvent, true);
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given stringified object as message, when message arrives, calls the handler with the message", () => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcRendererEvent, JSON.stringify({ some: "object" }));
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith({ some: "object" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
57
src/renderer/channel/message-to-channel.test.ts
Normal file
57
src/renderer/channel/message-to-channel.test.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { MessageToChannel } from "../../common/channel/message-to-channel-injection-token";
|
||||||
|
import type { MessageChannel } from "../../common/channel/message-channel-injection-token";
|
||||||
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
|
import { messageToChannelInjectionToken } from "../../common/channel/message-to-channel-injection-token";
|
||||||
|
import ipcRendererInjectable from "./ipc-renderer.injectable";
|
||||||
|
import type { IpcRenderer } from "electron";
|
||||||
|
|
||||||
|
describe("message to channel from renderer", () => {
|
||||||
|
let messageToChannel: MessageToChannel;
|
||||||
|
let sendMock: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
sendMock = jest.fn();
|
||||||
|
|
||||||
|
di.override(ipcRendererInjectable, () => ({
|
||||||
|
send: sendMock,
|
||||||
|
}) as unknown as IpcRenderer);
|
||||||
|
|
||||||
|
messageToChannel = di.inject(messageToChannelInjectionToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given string as message, when messaging to channel, sends stringified message", () => {
|
||||||
|
messageToChannel(someChannel, "some-message");
|
||||||
|
|
||||||
|
expect(sendMock).toHaveBeenCalledWith("some-channel-id", '"some-message"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given boolean as message, when messaging to channel, sends stringified message", () => {
|
||||||
|
messageToChannel(someChannel, true);
|
||||||
|
|
||||||
|
expect(sendMock).toHaveBeenCalledWith("some-channel-id", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given number as message, when messaging to channel, sends stringified message", () => {
|
||||||
|
messageToChannel(someChannel, 42);
|
||||||
|
|
||||||
|
expect(sendMock).toHaveBeenCalledWith("some-channel-id", "42");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given object as message, when messaging to channel, sends stringified message", () => {
|
||||||
|
messageToChannel(someChannel, { some: "object" });
|
||||||
|
|
||||||
|
expect(sendMock).toHaveBeenCalledWith(
|
||||||
|
"some-channel-id",
|
||||||
|
JSON.stringify({ some: "object" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const someChannel: MessageChannel<any> = { id: "some-channel-id" };
|
||||||
@ -5,6 +5,9 @@
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import ipcRendererInjectable from "./ipc-renderer.injectable";
|
import ipcRendererInjectable from "./ipc-renderer.injectable";
|
||||||
import { requestFromChannelInjectionToken } from "../../common/channel/request-from-channel-injection-token";
|
import { requestFromChannelInjectionToken } from "../../common/channel/request-from-channel-injection-token";
|
||||||
|
import { pipeline } from "@ogre-tools/fp";
|
||||||
|
import { tentativeStringifyJson } from "../../common/utils/tentative-stringify-json";
|
||||||
|
import { tentativeParseJson } from "../../common/utils/tentative-parse-json";
|
||||||
|
|
||||||
const requestFromChannelInjectable = getInjectable({
|
const requestFromChannelInjectable = getInjectable({
|
||||||
id: "request-from-channel",
|
id: "request-from-channel",
|
||||||
@ -12,7 +15,13 @@ const requestFromChannelInjectable = getInjectable({
|
|||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const ipcRenderer = di.inject(ipcRendererInjectable);
|
const ipcRenderer = di.inject(ipcRendererInjectable);
|
||||||
|
|
||||||
return (channel, ...[request]) => ipcRenderer.invoke(channel.id, request);
|
return async (channel, ...[request]) =>
|
||||||
|
await pipeline(
|
||||||
|
request,
|
||||||
|
tentativeStringifyJson,
|
||||||
|
(req) => ipcRenderer.invoke(channel.id, req),
|
||||||
|
tentativeParseJson,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
injectionToken: requestFromChannelInjectionToken,
|
injectionToken: requestFromChannelInjectionToken,
|
||||||
|
|||||||
121
src/renderer/channel/request-from-channel.test.ts
Normal file
121
src/renderer/channel/request-from-channel.test.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { MessageChannel } from "../../common/channel/message-channel-injection-token";
|
||||||
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
|
import ipcRendererInjectable from "./ipc-renderer.injectable";
|
||||||
|
import type { IpcRenderer } from "electron";
|
||||||
|
import type { AsyncFnMock } from "@async-fn/jest";
|
||||||
|
import asyncFn from "@async-fn/jest";
|
||||||
|
import type { RequestFromChannel } from "../../common/channel/request-from-channel-injection-token";
|
||||||
|
import { requestFromChannelInjectionToken } from "../../common/channel/request-from-channel-injection-token";
|
||||||
|
import requestFromChannelInjectable from "./request-from-channel.injectable";
|
||||||
|
import { getPromiseStatus } from "../../common/test-utils/get-promise-status";
|
||||||
|
|
||||||
|
describe("request from channel in renderer", () => {
|
||||||
|
let requestFromChannel: RequestFromChannel;
|
||||||
|
let invokeMock: AsyncFnMock<(channelId: string, request: any) => any>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
di.unoverride(requestFromChannelInjectable);
|
||||||
|
|
||||||
|
invokeMock = asyncFn();
|
||||||
|
|
||||||
|
di.override(ipcRendererInjectable, () => ({
|
||||||
|
invoke: invokeMock,
|
||||||
|
}) as unknown as IpcRenderer);
|
||||||
|
|
||||||
|
requestFromChannel = di.inject(requestFromChannelInjectionToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when messaging to channel", () => {
|
||||||
|
let actualPromise: Promise<any>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
actualPromise = requestFromChannel(someChannel, "some-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends stringified message", () => {
|
||||||
|
expect(invokeMock).toHaveBeenCalledWith("some-channel-id", '"some-message"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resolve yet", async () => {
|
||||||
|
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||||
|
|
||||||
|
expect(promiseStatus.fulfilled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when invoking resolves, resolves", async () => {
|
||||||
|
await invokeMock.resolve("some-response");
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toBe("some-response");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when invoking resolves with stringified string, resolves with string", async () => {
|
||||||
|
await invokeMock.resolve('"some-response"');
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toBe("some-response");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when invoking resolves with stringified boolean, resolves with boolean", async () => {
|
||||||
|
await invokeMock.resolve("true");
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when invoking resolves with stringified number, resolves with number", async () => {
|
||||||
|
await invokeMock.resolve("42");
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when invoking resolves with stringified object, resolves with object", async () => {
|
||||||
|
await invokeMock.resolve(JSON.stringify({ some: "object" }));
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toEqual({ some: "object" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given string as message, when messaging to channel, sends stringified message", () => {
|
||||||
|
requestFromChannel(someChannel, "some-message");
|
||||||
|
|
||||||
|
expect(invokeMock).toHaveBeenCalledWith("some-channel-id", '"some-message"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given boolean as message, when messaging to channel, sends stringified message", () => {
|
||||||
|
requestFromChannel(someChannel, true);
|
||||||
|
|
||||||
|
expect(invokeMock).toHaveBeenCalledWith("some-channel-id", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given number as message, when messaging to channel, sends stringified message", () => {
|
||||||
|
requestFromChannel(someChannel, 42);
|
||||||
|
|
||||||
|
expect(invokeMock).toHaveBeenCalledWith("some-channel-id", "42");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given object as message, when messaging to channel, sends stringified message", () => {
|
||||||
|
requestFromChannel(someChannel, { some: "object" });
|
||||||
|
|
||||||
|
expect(invokeMock).toHaveBeenCalledWith(
|
||||||
|
"some-channel-id",
|
||||||
|
JSON.stringify({ some: "object" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const someChannel: MessageChannel<any> = { id: "some-channel-id" };
|
||||||
@ -5,6 +5,7 @@
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import type { JsonValue } from "type-fest";
|
import type { JsonValue } from "type-fest";
|
||||||
import ipcRendererInjectable from "./ipc-renderer.injectable";
|
import ipcRendererInjectable from "./ipc-renderer.injectable";
|
||||||
|
import { tentativeStringifyJson } from "../../common/utils/tentative-stringify-json";
|
||||||
|
|
||||||
const sendToMainInjectable = getInjectable({
|
const sendToMainInjectable = getInjectable({
|
||||||
id: "send-to-main",
|
id: "send-to-main",
|
||||||
@ -14,7 +15,9 @@ const sendToMainInjectable = getInjectable({
|
|||||||
|
|
||||||
// TODO: Figure out way to improve typing in internals
|
// TODO: Figure out way to improve typing in internals
|
||||||
return <T>(channelId: string, message: JsonValue extends T ? T : never ) => {
|
return <T>(channelId: string, message: JsonValue extends T ? T : never ) => {
|
||||||
ipcRenderer.send(channelId, ...(message ? [message] : []));
|
const stringifiedMessage = tentativeStringifyJson(message);
|
||||||
|
|
||||||
|
ipcRenderer.send(channelId, ...(stringifiedMessage ? [stringifiedMessage] : []));
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import type { SendToViewArgs } from "../../main/start-main-application/lens-wind
|
|||||||
import enlistMessageChannelListenerInjectableInRenderer from "../../renderer/channel/channel-listeners/enlist-message-channel-listener.injectable";
|
import enlistMessageChannelListenerInjectableInRenderer from "../../renderer/channel/channel-listeners/enlist-message-channel-listener.injectable";
|
||||||
import type { DiContainer } from "@ogre-tools/injectable";
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
|
import { tentativeParseJson } from "../../common/utils/tentative-parse-json";
|
||||||
|
|
||||||
export const overrideMessagingFromMainToWindow = (mainDi: DiContainer) => {
|
export const overrideMessagingFromMainToWindow = (mainDi: DiContainer) => {
|
||||||
const messageChannelListenerFakesForRenderer = new Map<
|
const messageChannelListenerFakesForRenderer = new Map<
|
||||||
@ -47,7 +48,7 @@ export const overrideMessagingFromMainToWindow = (mainDi: DiContainer) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = data[0];
|
const message = tentativeParseJson(data[0]);
|
||||||
|
|
||||||
listeners.forEach((listener) => listener.handler(message));
|
listeners.forEach((listener) => listener.handler(message));
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user