mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Introduce way to install update directly from tray. Make application updater unit testable... (#5433)
* Extract product name as injectable Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make tray items comply with Open Closed Principle Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Replace duplicated overrides with global Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add behaviour for navigating to preferences using tray Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Introduce a tray item for updating application Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Tweak naming Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Tweak more naming Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove redundant indirection Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Tweak more naming Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Introduce injectable for package.json being side-effect Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Relocate file to directory containing feature Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using injectable for limiting side effect Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add missing injection token for implementation of tray item Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove resetting state for update is ready to be installed for being unclear Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Kill dead code Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make label of tray item reactive Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Extract updating is enabled to separate injectable Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Introduce competition for tray Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Expand scope of behaviour for updating using tray also contain checking for updates Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove dead code Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Kill dead code Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Implement checking of updates from multiple update channels Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Start installing updates automatically when quitting application Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Show application window when checking of updates has happened Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Show notifications and dialog for downloading update Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Implement naive notifications for version updates Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Implement checking of Electron specific updates as responsibility Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Implement downloading of Electron specific updates as responsibility Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Introduce competition for channel abstraction Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove redundant global override Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix typing after enabling strict mode Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Introduce abstraction for a state that is shared between environments Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Extract states of application update to be usable from all environments Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Handle failing download of update Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make code for window visibility actually work Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Consolidate code for sending messages between processes to a window Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Split bloated dependency in smaller pieces Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make state of download progress accessible from all environments Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Rename files for accuracy Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Move channel abstraction to more global directory Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Enhance typing of channels and sync-box Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Consolidate channel abstraction types Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Update asyncFn to support strict mode Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix snapshot after rebase Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add missing global override Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Introduce injection token for channels to allow injecting all of them at once Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add notifications about change in update status Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Rename property for accuracy Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Tweak code style Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make notifications unit testable in behaviours Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add implementation for asking boolean over processes Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Reorganize responsibilities for checking updates Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Reorganize tests for installing update under separate scenarios Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make stuff happening when root frame is rendered unit testable Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Introduce periodical check for updates Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Allow downgrading app versions Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using competition for checking of updates in application menu Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Kill dead code Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make test less prone to fail for wrong reason Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove redundant boilerplate Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make tests for specific migrations less prone to failing for wrong reason Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Move shared stuff under common Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using single source of truth for selected update channel Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Extract tests for installing update from different update channels to separate scenario Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add missing global override Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using release channel of installed application version as default value for selected update channel Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Consolidate usage of channel abstraction to same implementation Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make Channel abstraction support return values Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix direct calling of runnables Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Synchronize initial values of sync boxes when window starts Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add missing global override Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Tweak message of question from user Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Consolidate names of directories Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add TODO Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove unimplemented scenario from test Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Simplify test Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Improve name of test Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove redundant overrides Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix code style Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make Animate deterministic in unit tests Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Simplify naming Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Simplify more naming Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Simplify even more naming Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Simplify more and more naming Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Add todo for cleaning unacceptable code encountered Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Improve name of behaviour Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Make unit test more strict Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Enhance name of behaviour Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Introduce dependency to get random IDs Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Make asking of boolean value from user not require explicit ID for question Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Simplify code for asking of boolean value from user Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Make setting of initial state for sync boxes not trigger irrelevant messaging to main Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Make a channel have default type for sent and returned message Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Introduce higher order function to log errors in decorated functions Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Export type for error logging Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Tweak test name Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Introduce higher order function for suppressing errors Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Relocate some explicit error handlings to proper level of abstraction Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make higher order function for logging errors support asynchronous rejecting with non error instance Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make overridden version of application exactly the one required by unit test Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Mark injectable causing side effects Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Revert not required changes Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make code for asserting a promise more strict Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make dependencies readonly Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove duplication for disposers Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Implement initial values for sync-boxes Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Separate concept of message and request channels Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Introduce tests for requesting from channel in renderer Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Implement requesting from renderer in main Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Revert "Implement requesting from renderer in main" This reverts commit d3e7899d7900516f3dbfacdb317a453202318305. Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Tweak typing of request channel listeners to get rid of unexpected undefined Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove unused variable Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Tweak timing of sentry setup Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Require messages for MessageChannels be JsonValues for serialization Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Require requests and responses for RequestChannels be JsonValues for serialization Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Make different MessageChannels not require explicit "extends JsonObject" Note: Non-escaped lint breaks type here for forcing interface over type. Reasonable effort brought no understanding for what is the relevant difference between the two. Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Make a primitive argument an object for readability Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Make typing of higher order function for error suppression not lie Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * 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> * Introduce a way to make intentional orphan promises uniform, controllable and deliberate Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Make downloading of update and what follows more deliberate as orphan promise Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Move utility function under directory Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Move another utility function under directory Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix incorrect name of file Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove redundant code Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Kill dead code Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Round percentage of update download progress in tray Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix rebase conflicts Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix CheckForUpdate type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> Co-authored-by: Iku-turso <mikko.aspiala@gmail.com> Co-authored-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
229 files changed
+8652
-1217
No files matched your search
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
|
||||
export interface Channel<MessageTemplate = void, ReturnTemplate = void> {
|
||||
id: string;
|
||||
_messageTemplate?: MessageTemplate;
|
||||
_returnTemplate?: ReturnTemplate;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/lens-window-injection-token";
|
||||
import { lensWindowInjectionToken } from "../../../main/start-main-application/lens-window/application-window/lens-window-injection-token";
|
||||
import type { MessageToChannel } from "./message-to-channel-injection-token";
|
||||
import { messageToChannelInjectionToken } from "./message-to-channel-injection-token";
|
||||
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
||||
import createLensWindowInjectable from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable";
|
||||
import closeAllWindowsInjectable from "../../../main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable";
|
||||
import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token";
|
||||
import type { MessageChannel } from "./message-channel-injection-token";
|
||||
import type { RequestFromChannel } from "./request-from-channel-injection-token";
|
||||
import { requestFromChannelInjectionToken } from "./request-from-channel-injection-token";
|
||||
import type { RequestChannel } from "./request-channel-injection-token";
|
||||
import { requestChannelListenerInjectionToken } from "./request-channel-listener-injection-token";
|
||||
import type { AsyncFnMock } from "@async-fn/jest";
|
||||
import asyncFn from "@async-fn/jest";
|
||||
import { getPromiseStatus } from "../../test-utils/get-promise-status";
|
||||
|
||||
type TestMessageChannel = MessageChannel<string>;
|
||||
type TestRequestChannel = RequestChannel<string, string>;
|
||||
|
||||
describe("channel", () => {
|
||||
describe("messaging from main to renderer, given listener for channel in a window and application has started", () => {
|
||||
let testMessageChannel: TestMessageChannel;
|
||||
let messageListenerInWindowMock: jest.Mock;
|
||||
let mainDi: DiContainer;
|
||||
let messageToChannel: MessageToChannel;
|
||||
|
||||
beforeEach(async () => {
|
||||
const applicationBuilder = getApplicationBuilder();
|
||||
|
||||
mainDi = applicationBuilder.dis.mainDi;
|
||||
const rendererDi = applicationBuilder.dis.rendererDi;
|
||||
|
||||
messageListenerInWindowMock = jest.fn();
|
||||
|
||||
const testChannelListenerInTestWindowInjectable = getInjectable({
|
||||
id: "test-channel-listener-in-test-window",
|
||||
|
||||
instantiate: (di) => ({
|
||||
channel: di.inject(testMessageChannelInjectable),
|
||||
|
||||
handler: messageListenerInWindowMock,
|
||||
}),
|
||||
|
||||
injectionToken: messageChannelListenerInjectionToken,
|
||||
});
|
||||
|
||||
rendererDi.register(testChannelListenerInTestWindowInjectable);
|
||||
|
||||
// Notice how test channel has presence in both DIs, being from common
|
||||
mainDi.register(testMessageChannelInjectable);
|
||||
rendererDi.register(testMessageChannelInjectable);
|
||||
|
||||
testMessageChannel = mainDi.inject(testMessageChannelInjectable);
|
||||
|
||||
messageToChannel = mainDi.inject(
|
||||
messageToChannelInjectionToken,
|
||||
);
|
||||
|
||||
await applicationBuilder.render();
|
||||
|
||||
const closeAllWindows = mainDi.inject(closeAllWindowsInjectable);
|
||||
|
||||
closeAllWindows();
|
||||
});
|
||||
|
||||
describe("given window is shown", () => {
|
||||
let someWindowFake: LensWindow;
|
||||
|
||||
beforeEach(async () => {
|
||||
someWindowFake = createTestWindow(mainDi, "some-window");
|
||||
|
||||
await someWindowFake.show();
|
||||
});
|
||||
|
||||
it("when sending message, triggers listener in window", () => {
|
||||
messageToChannel(testMessageChannel, "some-message");
|
||||
|
||||
expect(messageListenerInWindowMock).toHaveBeenCalledWith("some-message");
|
||||
});
|
||||
|
||||
it("given window is hidden, when sending message, does not trigger listener in window", () => {
|
||||
someWindowFake.close();
|
||||
|
||||
messageToChannel(testMessageChannel, "some-message");
|
||||
|
||||
expect(messageListenerInWindowMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("given multiple shown windows, when sending message, triggers listeners in all windows", async () => {
|
||||
const someWindowFake = createTestWindow(mainDi, "some-window");
|
||||
const someOtherWindowFake = createTestWindow(mainDi, "some-other-window");
|
||||
|
||||
await someWindowFake.show();
|
||||
await someOtherWindowFake.show();
|
||||
|
||||
messageToChannel(testMessageChannel, "some-message");
|
||||
|
||||
expect(messageListenerInWindowMock.mock.calls).toEqual([
|
||||
["some-message"],
|
||||
["some-message"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("messaging from renderer to main, given listener for channel in a main and application has started", () => {
|
||||
let testMessageChannel: TestMessageChannel;
|
||||
let messageListenerInMainMock: jest.Mock;
|
||||
let rendererDi: DiContainer;
|
||||
let mainDi: DiContainer;
|
||||
let messageToChannel: MessageToChannel;
|
||||
|
||||
beforeEach(async () => {
|
||||
const applicationBuilder = getApplicationBuilder();
|
||||
|
||||
mainDi = applicationBuilder.dis.mainDi;
|
||||
rendererDi = applicationBuilder.dis.rendererDi;
|
||||
|
||||
messageListenerInMainMock = jest.fn();
|
||||
|
||||
const testChannelListenerInMainInjectable = getInjectable({
|
||||
id: "test-channel-listener-in-main",
|
||||
|
||||
instantiate: (di) => ({
|
||||
channel: di.inject(testMessageChannelInjectable),
|
||||
|
||||
handler: messageListenerInMainMock,
|
||||
}),
|
||||
|
||||
injectionToken: messageChannelListenerInjectionToken,
|
||||
});
|
||||
|
||||
mainDi.register(testChannelListenerInMainInjectable);
|
||||
|
||||
// Notice how test channel has presence in both DIs, being from common
|
||||
mainDi.register(testMessageChannelInjectable);
|
||||
rendererDi.register(testMessageChannelInjectable);
|
||||
|
||||
testMessageChannel = rendererDi.inject(testMessageChannelInjectable);
|
||||
|
||||
messageToChannel = rendererDi.inject(
|
||||
messageToChannelInjectionToken,
|
||||
);
|
||||
|
||||
await applicationBuilder.render();
|
||||
});
|
||||
|
||||
it("when sending message, triggers listener in main", () => {
|
||||
messageToChannel(testMessageChannel, "some-message");
|
||||
|
||||
expect(messageListenerInMainMock).toHaveBeenCalledWith("some-message");
|
||||
});
|
||||
});
|
||||
|
||||
describe("requesting from main in renderer, given listener for channel in a main and application has started", () => {
|
||||
let testRequestChannel: TestRequestChannel;
|
||||
let requestListenerInMainMock: AsyncFnMock<(arg: string) => string>;
|
||||
let rendererDi: DiContainer;
|
||||
let mainDi: DiContainer;
|
||||
let requestFromChannel: RequestFromChannel;
|
||||
|
||||
beforeEach(async () => {
|
||||
const applicationBuilder = getApplicationBuilder();
|
||||
|
||||
mainDi = applicationBuilder.dis.mainDi;
|
||||
rendererDi = applicationBuilder.dis.rendererDi;
|
||||
|
||||
requestListenerInMainMock = asyncFn();
|
||||
|
||||
const testChannelListenerInMainInjectable = getInjectable({
|
||||
id: "test-channel-listener-in-main",
|
||||
|
||||
instantiate: (di) => ({
|
||||
channel: di.inject(testRequestChannelInjectable),
|
||||
|
||||
handler: requestListenerInMainMock,
|
||||
}),
|
||||
|
||||
injectionToken: requestChannelListenerInjectionToken,
|
||||
});
|
||||
|
||||
mainDi.register(testChannelListenerInMainInjectable);
|
||||
|
||||
// Notice how test channel has presence in both DIs, being from common
|
||||
mainDi.register(testRequestChannelInjectable);
|
||||
rendererDi.register(testRequestChannelInjectable);
|
||||
|
||||
testRequestChannel = rendererDi.inject(testRequestChannelInjectable);
|
||||
|
||||
requestFromChannel = rendererDi.inject(
|
||||
requestFromChannelInjectionToken,
|
||||
);
|
||||
|
||||
await applicationBuilder.render();
|
||||
});
|
||||
|
||||
describe("when requesting from channel", () => {
|
||||
let actualPromise: Promise<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
actualPromise = requestFromChannel(testRequestChannel, "some-request");
|
||||
});
|
||||
|
||||
it("triggers listener in main", () => {
|
||||
expect(requestListenerInMainMock).toHaveBeenCalledWith("some-request");
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
it("when main resolves with response, resolves with response", async () => {
|
||||
await requestListenerInMainMock.resolve("some-response");
|
||||
|
||||
const actual = await actualPromise;
|
||||
|
||||
expect(actual).toBe("some-response");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const testMessageChannelInjectable = getInjectable({
|
||||
id: "some-message-test-channel",
|
||||
|
||||
instantiate: (): TestMessageChannel => ({
|
||||
id: "some-message-channel-id",
|
||||
}),
|
||||
});
|
||||
|
||||
const testRequestChannelInjectable = getInjectable({
|
||||
id: "some-request-test-channel",
|
||||
|
||||
instantiate: (): TestRequestChannel => ({
|
||||
id: "some-request-channel-id",
|
||||
}),
|
||||
});
|
||||
|
||||
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,
|
||||
getContentSource: () => ({ url: "some-content-url" }),
|
||||
resizable: true,
|
||||
windowFrameUtilitiesAreShown: false,
|
||||
centered: false,
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: lensWindowInjectionToken,
|
||||
});
|
||||
|
||||
di.register(testWindowInjectable);
|
||||
|
||||
return di.inject(testWindowInjectable);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { MessageChannel } from "./message-channel-injection-token";
|
||||
import type { MessageChannelListener } from "./message-channel-listener-injection-token";
|
||||
|
||||
export type EnlistMessageChannelListener = <
|
||||
TChannel extends MessageChannel<any>,
|
||||
>(listener: MessageChannelListener<TChannel>) => () => void;
|
||||
|
||||
export const enlistMessageChannelListenerInjectionToken =
|
||||
getInjectionToken<EnlistMessageChannelListener>({
|
||||
id: "enlist-message-channel-listener",
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { RequestChannel } from "./request-channel-injection-token";
|
||||
import type { RequestChannelListener } from "./request-channel-listener-injection-token";
|
||||
|
||||
export type EnlistRequestChannelListener = <
|
||||
TChannel extends RequestChannel<any, any>,
|
||||
>(listener: RequestChannelListener<TChannel>) => () => void;
|
||||
|
||||
export const enlistRequestChannelListenerInjectionToken =
|
||||
getInjectionToken<EnlistRequestChannelListener>({
|
||||
id: "enlist-request-channel-listener",
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getStartableStoppable } from "../get-startable-stoppable";
|
||||
import { disposer } from "../index";
|
||||
import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token";
|
||||
import { requestChannelListenerInjectionToken } from "./request-channel-listener-injection-token";
|
||||
import { enlistMessageChannelListenerInjectionToken } from "./enlist-message-channel-listener-injection-token";
|
||||
import { enlistRequestChannelListenerInjectionToken } from "./enlist-request-channel-listener-injection-token";
|
||||
|
||||
const listeningOfChannelsInjectable = getInjectable({
|
||||
id: "listening-of-channels",
|
||||
|
||||
instantiate: (di) => {
|
||||
const enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken);
|
||||
const enlistRequestChannelListener = di.inject(enlistRequestChannelListenerInjectionToken);
|
||||
const messageChannelListeners = di.injectMany(messageChannelListenerInjectionToken);
|
||||
const requestChannelListeners = di.injectMany(requestChannelListenerInjectionToken);
|
||||
|
||||
return getStartableStoppable("listening-of-channels", () => {
|
||||
const messageChannelDisposers = messageChannelListeners.map(enlistMessageChannelListener);
|
||||
const requestChannelDisposers = requestChannelListeners.map(enlistRequestChannelListener);
|
||||
|
||||
return disposer(...messageChannelDisposers, ...requestChannelDisposers);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export default listeningOfChannelsInjectable;
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { JsonValue } from "type-fest";
|
||||
|
||||
export interface MessageChannel<Message extends JsonValue | void = void> {
|
||||
id: string;
|
||||
_messageSignature?: Message;
|
||||
}
|
||||
|
||||
export const messageChannelInjectionToken = getInjectionToken<MessageChannel<any>>({
|
||||
id: "message-channel",
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { SetRequired } from "type-fest";
|
||||
import type { MessageChannel } from "./message-channel-injection-token";
|
||||
|
||||
export interface MessageChannelListener<TChannel extends MessageChannel<any>> {
|
||||
channel: TChannel;
|
||||
handler: (value: SetRequired<TChannel, "_messageSignature">["_messageSignature"]) => void;
|
||||
}
|
||||
|
||||
export const messageChannelListenerInjectionToken = getInjectionToken<MessageChannelListener<MessageChannel<any>>>(
|
||||
{
|
||||
id: "message-channel-listener",
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { SetRequired } from "type-fest";
|
||||
import type { MessageChannel } from "./message-channel-injection-token";
|
||||
|
||||
export interface MessageToChannel {
|
||||
<TChannel extends MessageChannel<TMessage>, TMessage extends void>(
|
||||
channel: TChannel,
|
||||
): void;
|
||||
|
||||
<TChannel extends MessageChannel<any>>(
|
||||
channel: TChannel,
|
||||
message: SetRequired<TChannel, "_messageSignature">["_messageSignature"],
|
||||
): void;
|
||||
}
|
||||
|
||||
export const messageToChannelInjectionToken =
|
||||
getInjectionToken<MessageToChannel>({
|
||||
id: "message-to-message-channel",
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { JsonValue } from "type-fest";
|
||||
|
||||
export interface RequestChannel<
|
||||
Request extends JsonValue | void = void,
|
||||
Response extends JsonValue | void = void,
|
||||
> {
|
||||
id: string;
|
||||
_requestSignature?: Request;
|
||||
_responseSignature?: Response;
|
||||
}
|
||||
|
||||
export const requestChannelInjectionToken = getInjectionToken<RequestChannel<any, any>>({
|
||||
id: "request-channel",
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { SetRequired } from "type-fest";
|
||||
import type { RequestChannel } from "./request-channel-injection-token";
|
||||
|
||||
export interface RequestChannelListener<TChannel extends RequestChannel<any, any>> {
|
||||
channel: TChannel;
|
||||
|
||||
handler: (
|
||||
request: SetRequired<TChannel, "_requestSignature">["_requestSignature"]
|
||||
) =>
|
||||
| SetRequired<TChannel, "_responseSignature">["_responseSignature"]
|
||||
| Promise<
|
||||
SetRequired<TChannel, "_responseSignature">["_responseSignature"]
|
||||
>;
|
||||
}
|
||||
|
||||
export const requestChannelListenerInjectionToken = getInjectionToken<RequestChannelListener<RequestChannel<any, any>>>(
|
||||
{
|
||||
id: "request-channel-listener",
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { SetRequired } from "type-fest";
|
||||
import type { RequestChannel } from "./request-channel-injection-token";
|
||||
|
||||
export type RequestFromChannel = <
|
||||
TChannel extends RequestChannel<any, any>,
|
||||
>(
|
||||
channel: TChannel,
|
||||
...request: TChannel["_requestSignature"] extends void
|
||||
? []
|
||||
: [TChannel["_requestSignature"]]
|
||||
) => Promise<SetRequired<TChannel, "_responseSignature">["_responseSignature"]>;
|
||||
|
||||
export const requestFromChannelInjectionToken =
|
||||
getInjectionToken<RequestFromChannel>({
|
||||
id: "request-from-request-channel",
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { v4 as getRandomId } from "uuid";
|
||||
|
||||
const getRandomIdInjectable = getInjectable({
|
||||
id: "get-random-id",
|
||||
instantiate: () => getRandomId,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default getRandomIdInjectable;
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { isPromise } from "./is-promise";
|
||||
|
||||
describe("isPromise", () => {
|
||||
it("given promise, returns true", () => {
|
||||
const actual = isPromise(new Promise(() => {}));
|
||||
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
|
||||
it("given non-promise, returns false", () => {
|
||||
const actual = isPromise({});
|
||||
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
|
||||
it("given thenable, returns false", () => {
|
||||
const actual = isPromise({ then: () => {} });
|
||||
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
|
||||
it("given nothing, returns false", () => {
|
||||
const actual = isPromise(undefined);
|
||||
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
export function isPromise(reference: any): reference is Promise<any> {
|
||||
return reference?.constructor === Promise;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { computed } from "mobx";
|
||||
import syncBoxChannelInjectable from "./sync-box-channel.injectable";
|
||||
import { messageToChannelInjectionToken } from "../channel/message-to-channel-injection-token";
|
||||
import syncBoxStateInjectable from "./sync-box-state.injectable";
|
||||
import type { SyncBox } from "./sync-box-injection-token";
|
||||
|
||||
const createSyncBoxInjectable = getInjectable({
|
||||
id: "create-sync-box",
|
||||
|
||||
instantiate: (di) => {
|
||||
const syncBoxChannel = di.inject(syncBoxChannelInjectable);
|
||||
const messageToChannel = di.inject(messageToChannelInjectionToken);
|
||||
const getSyncBoxState = (id: string) => di.inject(syncBoxStateInjectable, id);
|
||||
|
||||
return <TData>(id: string, initialValue: TData): SyncBox<TData> => {
|
||||
const state = getSyncBoxState(id);
|
||||
|
||||
state.set(initialValue);
|
||||
|
||||
return {
|
||||
id,
|
||||
|
||||
value: computed(() => state.get()),
|
||||
|
||||
set: (value) => {
|
||||
state.set(value);
|
||||
|
||||
messageToChannel(syncBoxChannel, { id, value });
|
||||
},
|
||||
};
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default createSyncBoxInjectable;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { SyncBoxChannel } from "./sync-box-channel.injectable";
|
||||
import syncBoxChannelInjectable from "./sync-box-channel.injectable";
|
||||
import syncBoxStateInjectable from "./sync-box-state.injectable";
|
||||
import type { MessageChannelListener } from "../channel/message-channel-listener-injection-token";
|
||||
import { messageChannelListenerInjectionToken } from "../channel/message-channel-listener-injection-token";
|
||||
|
||||
const syncBoxChannelListenerInjectable = getInjectable({
|
||||
id: "sync-box-channel-listener",
|
||||
|
||||
instantiate: (di): MessageChannelListener<SyncBoxChannel> => {
|
||||
const getSyncBoxState = (id: string) => di.inject(syncBoxStateInjectable, id);
|
||||
const channel = di.inject(syncBoxChannelInjectable);
|
||||
|
||||
return {
|
||||
channel,
|
||||
|
||||
handler: ({ id, value }) => {
|
||||
const target = getSyncBoxState(id);
|
||||
|
||||
if (target) {
|
||||
target.set(value);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
injectionToken: messageChannelListenerInjectionToken,
|
||||
});
|
||||
|
||||
export default syncBoxChannelListenerInjectable;
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { MessageChannel } from "../channel/message-channel-injection-token";
|
||||
import { messageChannelInjectionToken } from "../channel/message-channel-injection-token";
|
||||
|
||||
export type SyncBoxChannel = MessageChannel<{ id: string; value: any }>;
|
||||
|
||||
const syncBoxChannelInjectable = getInjectable({
|
||||
id: "sync-box-channel",
|
||||
|
||||
instantiate: (): SyncBoxChannel => ({
|
||||
id: "sync-box-channel",
|
||||
}),
|
||||
|
||||
injectionToken: messageChannelInjectionToken,
|
||||
});
|
||||
|
||||
export default syncBoxChannelInjectable;
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { RequestChannel } from "../channel/request-channel-injection-token";
|
||||
import { requestChannelInjectionToken } from "../channel/request-channel-injection-token";
|
||||
|
||||
export type SyncBoxInitialValueChannel = RequestChannel<
|
||||
void,
|
||||
{ id: string; value: any }[]
|
||||
>;
|
||||
|
||||
const syncBoxInitialValueChannelInjectable = getInjectable({
|
||||
id: "sync-box-initial-value-channel",
|
||||
|
||||
instantiate: (): SyncBoxInitialValueChannel => ({
|
||||
id: "sync-box-initial-value-channel",
|
||||
}),
|
||||
|
||||
injectionToken: requestChannelInjectionToken,
|
||||
});
|
||||
|
||||
export default syncBoxInitialValueChannelInjectable;
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import type { JsonValue } from "type-fest";
|
||||
|
||||
export interface SyncBox<TValue extends JsonValue> {
|
||||
id: string;
|
||||
value: IComputedValue<TValue>;
|
||||
set: (value: TValue) => void;
|
||||
}
|
||||
|
||||
export const syncBoxInjectionToken = getInjectionToken<SyncBox<any>>({
|
||||
id: "sync-box",
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { observable } from "mobx";
|
||||
|
||||
const syncBoxStateInjectable = getInjectable({
|
||||
id: "sync-box-state",
|
||||
|
||||
instantiate: () => observable.box(),
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, id: string) => id,
|
||||
}),
|
||||
});
|
||||
|
||||
export default syncBoxStateInjectable;
|
||||
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { observe, runInAction } from "mobx";
|
||||
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
||||
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
||||
import createSyncBoxInjectable from "./create-sync-box.injectable";
|
||||
import { flushPromises } from "../../test-utils/flush-promises";
|
||||
import type { SyncBox } from "./sync-box-injection-token";
|
||||
|
||||
describe("sync-box", () => {
|
||||
let applicationBuilder: ApplicationBuilder;
|
||||
|
||||
beforeEach(() => {
|
||||
applicationBuilder = getApplicationBuilder();
|
||||
|
||||
applicationBuilder.dis.mainDi.register(someInjectable);
|
||||
applicationBuilder.dis.rendererDi.register(someInjectable);
|
||||
});
|
||||
|
||||
// TODO: Separate starting for main application and starting of window in application builder
|
||||
xdescribe("given application is started, when value is set in main", () => {
|
||||
let valueInMain: string;
|
||||
let syncBoxInMain: SyncBox<string>;
|
||||
|
||||
beforeEach(async () => {
|
||||
syncBoxInMain = applicationBuilder.dis.mainDi.inject(someInjectable);
|
||||
|
||||
// await applicationBuilder.start();
|
||||
|
||||
observe(syncBoxInMain.value, ({ newValue }) => {
|
||||
valueInMain = newValue as string;
|
||||
}, true);
|
||||
|
||||
runInAction(() => {
|
||||
syncBoxInMain.set("some-value-from-main");
|
||||
});
|
||||
});
|
||||
|
||||
it("knows value in main", () => {
|
||||
expect(valueInMain).toBe("some-value-from-main");
|
||||
});
|
||||
|
||||
describe("when window starts", () => {
|
||||
let valueInRenderer: string;
|
||||
let syncBoxInRenderer: SyncBox<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
// applicationBuilder.renderWindow()
|
||||
|
||||
syncBoxInRenderer = applicationBuilder.dis.rendererDi.inject(someInjectable);
|
||||
|
||||
observe(syncBoxInRenderer.value, ({ newValue }) => {
|
||||
valueInRenderer = newValue as string;
|
||||
}, true);
|
||||
});
|
||||
|
||||
it("does not have the initial value yet", () => {
|
||||
expect(valueInRenderer).toBe(undefined);
|
||||
});
|
||||
|
||||
describe("when getting initial value resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("has value in renderer", () => {
|
||||
expect(valueInRenderer).toBe("some-value-from-main");
|
||||
});
|
||||
|
||||
describe("when value is set from renderer", () => {
|
||||
beforeEach(() => {
|
||||
runInAction(() => {
|
||||
syncBoxInRenderer.set("some-value-from-renderer");
|
||||
});
|
||||
});
|
||||
|
||||
it("has value in main", () => {
|
||||
expect(valueInMain).toBe("some-value-from-renderer");
|
||||
});
|
||||
|
||||
it("has value in renderer", () => {
|
||||
expect(valueInRenderer).toBe("some-value-from-renderer");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when value is set from renderer before getting initial value from main resolves", () => {
|
||||
beforeEach(() => {
|
||||
runInAction(() => {
|
||||
syncBoxInRenderer.set("some-value-from-renderer");
|
||||
});
|
||||
});
|
||||
|
||||
it("has value in main", () => {
|
||||
expect(valueInMain).toBe("some-value-from-renderer");
|
||||
});
|
||||
|
||||
it("has value in renderer", () => {
|
||||
expect(valueInRenderer).toBe("some-value-from-renderer");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when application starts with a window", () => {
|
||||
let valueInRenderer: string;
|
||||
let valueInMain: string;
|
||||
let syncBoxInMain: SyncBox<string>;
|
||||
let syncBoxInRenderer: SyncBox<string>;
|
||||
|
||||
beforeEach(async () => {
|
||||
syncBoxInMain = applicationBuilder.dis.mainDi.inject(someInjectable);
|
||||
syncBoxInRenderer = applicationBuilder.dis.rendererDi.inject(someInjectable);
|
||||
|
||||
await applicationBuilder.render();
|
||||
|
||||
observe(syncBoxInRenderer.value, ({ newValue }) => {
|
||||
valueInRenderer = newValue as string;
|
||||
}, true);
|
||||
|
||||
observe(syncBoxInMain.value, ({ newValue }) => {
|
||||
valueInMain = newValue as string;
|
||||
}, true);
|
||||
});
|
||||
|
||||
it("knows initial value in main", () => {
|
||||
expect(valueInMain).toBe("some-initial-value");
|
||||
});
|
||||
|
||||
it("knows initial value in renderer", () => {
|
||||
expect(valueInRenderer).toBe("some-initial-value");
|
||||
});
|
||||
|
||||
describe("when value is set from main", () => {
|
||||
beforeEach(() => {
|
||||
runInAction(() => {
|
||||
syncBoxInMain.set("some-value-from-main");
|
||||
});
|
||||
});
|
||||
|
||||
it("has value in main", () => {
|
||||
expect(valueInMain).toBe("some-value-from-main");
|
||||
});
|
||||
|
||||
it("has value in renderer", () => {
|
||||
expect(valueInRenderer).toBe("some-value-from-main");
|
||||
});
|
||||
|
||||
describe("when value is set from renderer", () => {
|
||||
beforeEach(() => {
|
||||
runInAction(() => {
|
||||
syncBoxInRenderer.set("some-value-from-renderer");
|
||||
});
|
||||
});
|
||||
|
||||
it("has value in main", () => {
|
||||
expect(valueInMain).toBe("some-value-from-renderer");
|
||||
});
|
||||
|
||||
it("has value in renderer", () => {
|
||||
expect(valueInRenderer).toBe("some-value-from-renderer");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const someInjectable = getInjectable({
|
||||
id: "some-injectable",
|
||||
|
||||
instantiate: (di) => {
|
||||
const createSyncBox = di.inject(createSyncBoxInjectable);
|
||||
|
||||
return createSyncBox("some-sync-box", "some-initial-value");
|
||||
},
|
||||
});
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import { isPromise } from "../is-promise/is-promise";
|
||||
|
||||
export type WithErrorLoggingFor = (
|
||||
getErrorMessage: (error: unknown) => string
|
||||
) => <T extends (...args: any[]) => any>(
|
||||
toBeDecorated: T
|
||||
) => (...args: Parameters<T>) => ReturnType<T>;
|
||||
|
||||
const withErrorLoggingInjectable = getInjectable({
|
||||
id: "with-error-logging",
|
||||
|
||||
instantiate: (di): WithErrorLoggingFor => {
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
return (getErrorMessage) =>
|
||||
(toBeDecorated) =>
|
||||
(...args) => {
|
||||
try {
|
||||
const returnValue = toBeDecorated(...args);
|
||||
|
||||
if (isPromise(returnValue)) {
|
||||
returnValue.catch((e) => {
|
||||
const errorMessage = getErrorMessage(e);
|
||||
|
||||
logger.error(errorMessage, e);
|
||||
});
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
} catch (e) {
|
||||
const errorMessage = getErrorMessage(e);
|
||||
|
||||
logger.error(errorMessage, e);
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default withErrorLoggingInjectable;
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import type { Logger } from "../../logger";
|
||||
import withErrorLoggingInjectable from "./with-error-logging.injectable";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import type { AsyncFnMock } from "@async-fn/jest";
|
||||
import asyncFn from "@async-fn/jest";
|
||||
import { getPromiseStatus } from "../../test-utils/get-promise-status";
|
||||
|
||||
describe("with-error-logging", () => {
|
||||
describe("given decorated sync function", () => {
|
||||
let loggerStub: Logger;
|
||||
let toBeDecorated: jest.Mock<number | undefined, [string, string]>;
|
||||
let decorated: (a: string, b: string) => number | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting();
|
||||
|
||||
loggerStub = {
|
||||
error: jest.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
di.override(loggerInjectable, () => loggerStub);
|
||||
|
||||
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
|
||||
|
||||
toBeDecorated = jest.fn();
|
||||
|
||||
decorated = pipeline(
|
||||
toBeDecorated,
|
||||
withErrorLoggingFor((error: any) => `some-error-message-for-${error.message}`),
|
||||
);
|
||||
});
|
||||
|
||||
describe("when function does not throw and returns value", () => {
|
||||
let returnValue: number | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
toBeDecorated.mockImplementation((_, __) => 42);
|
||||
|
||||
returnValue = decorated("some-parameter", "some-other-parameter");
|
||||
});
|
||||
|
||||
it("passes arguments to decorated function", () => {
|
||||
expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
|
||||
});
|
||||
|
||||
it("does not log error", () => {
|
||||
expect(loggerStub.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns the value", () => {
|
||||
expect(returnValue).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when function does not throw and returns no value", () => {
|
||||
let returnValue: number | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
toBeDecorated.mockImplementation((_, __) => undefined);
|
||||
|
||||
returnValue = decorated("some-parameter", "some-other-parameter");
|
||||
});
|
||||
|
||||
it("passes arguments to decorated function", () => {
|
||||
expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
|
||||
});
|
||||
|
||||
it("does not log error", () => {
|
||||
expect(loggerStub.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns nothing", () => {
|
||||
expect(returnValue).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when function throws", () => {
|
||||
let error: Error;
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
toBeDecorated.mockImplementation((_, __) => {
|
||||
throw new Error("some-error");
|
||||
});
|
||||
|
||||
try {
|
||||
decorated("some-parameter", "some-other-parameter");
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
});
|
||||
|
||||
it("passes arguments to decorated function", () => {
|
||||
expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
|
||||
});
|
||||
|
||||
it("logs the error", () => {
|
||||
expect(loggerStub.error).toHaveBeenCalledWith("some-error-message-for-some-error", error);
|
||||
});
|
||||
|
||||
it("throws", () => {
|
||||
expect(error.message).toBe("some-error");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given decorated async function", () => {
|
||||
let loggerStub: Logger;
|
||||
let decorated: (a: string, b: string) => Promise<number | undefined>;
|
||||
let toBeDecorated: AsyncFnMock<typeof decorated>;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting();
|
||||
|
||||
loggerStub = {
|
||||
error: jest.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
di.override(loggerInjectable, () => loggerStub);
|
||||
|
||||
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
|
||||
|
||||
toBeDecorated = asyncFn();
|
||||
|
||||
decorated = pipeline(
|
||||
toBeDecorated,
|
||||
|
||||
withErrorLoggingFor(
|
||||
(error: any) =>
|
||||
`some-error-message-for-${error.message || error.someProperty}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe("when called", () => {
|
||||
let returnValuePromise: Promise<number | undefined>;
|
||||
|
||||
beforeEach(() => {
|
||||
returnValuePromise = decorated("some-parameter", "some-other-parameter");
|
||||
});
|
||||
|
||||
it("passes arguments to decorated function", () => {
|
||||
expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
|
||||
});
|
||||
|
||||
it("does not log error yet", () => {
|
||||
expect(loggerStub.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(returnValuePromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
describe("when call rejects with error instance", () => {
|
||||
let error: Error;
|
||||
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await toBeDecorated.reject(new Error("some-error"));
|
||||
await returnValuePromise;
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
});
|
||||
|
||||
it("logs the error", () => {
|
||||
expect(loggerStub.error).toHaveBeenCalledWith("some-error-message-for-some-error", error);
|
||||
});
|
||||
|
||||
it("rejects", () => {
|
||||
return expect(() => returnValuePromise).rejects.toThrow("some-error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when call rejects with something else than error instance", () => {
|
||||
let error: unknown;
|
||||
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await toBeDecorated.reject({ someProperty: "some-rejection" });
|
||||
await returnValuePromise;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
});
|
||||
|
||||
it("logs the rejection", () => {
|
||||
expect(loggerStub.error).toHaveBeenCalledWith(
|
||||
"some-error-message-for-some-rejection",
|
||||
error,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects", () => {
|
||||
return expect(() => returnValuePromise).rejects.toEqual({ someProperty: "some-rejection" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("when call resolves with value", () => {
|
||||
beforeEach(async () => {
|
||||
await toBeDecorated.resolve(42);
|
||||
});
|
||||
|
||||
it("does not log error", () => {
|
||||
expect(loggerStub.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves with the value", async () => {
|
||||
const returnValue = await returnValuePromise;
|
||||
|
||||
expect(returnValue).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when call resolves without value", () => {
|
||||
beforeEach(async () => {
|
||||
await toBeDecorated.resolve(undefined);
|
||||
});
|
||||
|
||||
it("does not log error", () => {
|
||||
expect(loggerStub.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves without value", async () => {
|
||||
const returnValue = await returnValuePromise;
|
||||
|
||||
expect(returnValue).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { AsyncFnMock } from "@async-fn/jest";
|
||||
import asyncFn from "@async-fn/jest";
|
||||
import { getPromiseStatus } from "../../test-utils/get-promise-status";
|
||||
import { withErrorSuppression } from "./with-error-suppression";
|
||||
|
||||
describe("with-error-suppression", () => {
|
||||
describe("given decorated sync function", () => {
|
||||
let toBeDecorated: jest.Mock<void, [string, string]>;
|
||||
let decorated: (a: string, b: string) => void;
|
||||
|
||||
beforeEach(() => {
|
||||
toBeDecorated = jest.fn();
|
||||
|
||||
decorated = withErrorSuppression(toBeDecorated);
|
||||
});
|
||||
|
||||
describe("when function does not throw", () => {
|
||||
let returnValue: void;
|
||||
|
||||
beforeEach(() => {
|
||||
returnValue = decorated("some-parameter", "some-other-parameter");
|
||||
});
|
||||
|
||||
it("passes arguments to decorated function", () => {
|
||||
expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
|
||||
});
|
||||
|
||||
it("returns nothing", () => {
|
||||
expect(returnValue).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when function throws", () => {
|
||||
let returnValue: void;
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
toBeDecorated.mockImplementation((_, __) => {
|
||||
throw new Error("some-error");
|
||||
});
|
||||
|
||||
returnValue = decorated("some-parameter", "some-other-parameter");
|
||||
});
|
||||
|
||||
it("passes arguments to decorated function", () => {
|
||||
expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
|
||||
});
|
||||
|
||||
it("returns nothing", () => {
|
||||
expect(returnValue).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given decorated async function", () => {
|
||||
let decorated: (a: string, b: string) => Promise<number> | Promise<void>;
|
||||
let toBeDecorated: AsyncFnMock<(a: string, b: string) => number>;
|
||||
|
||||
beforeEach(() => {
|
||||
toBeDecorated = asyncFn();
|
||||
|
||||
decorated = withErrorSuppression(toBeDecorated);
|
||||
});
|
||||
|
||||
describe("when called", () => {
|
||||
let returnValuePromise: Promise<number> | Promise<void>;
|
||||
|
||||
beforeEach(() => {
|
||||
returnValuePromise = decorated("some-parameter", "some-other-parameter");
|
||||
});
|
||||
|
||||
it("passes arguments to decorated function", () => {
|
||||
expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(returnValuePromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
it("when call rejects, resolves with nothing", async () => {
|
||||
await toBeDecorated.reject(new Error("some-error"));
|
||||
|
||||
const returnValue = await returnValuePromise;
|
||||
|
||||
expect(returnValue).toBeUndefined();
|
||||
});
|
||||
|
||||
it("when call resolves, resolves with the value", async () => {
|
||||
await toBeDecorated.resolve(42);
|
||||
|
||||
const returnValue = await returnValuePromise;
|
||||
|
||||
expect(returnValue).toBe(42);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { noop } from "lodash/fp";
|
||||
|
||||
export function withErrorSuppression<TDecorated extends (...args: any[]) => Promise<any>>(toBeDecorated: TDecorated): (...args: Parameters<TDecorated>) => ReturnType<TDecorated> | Promise<void>;
|
||||
export function withErrorSuppression<TDecorated extends (...args: any[]) => any>(toBeDecorated: TDecorated): (...args: Parameters<TDecorated>) => ReturnType<TDecorated> | void;
|
||||
|
||||
export function withErrorSuppression(toBeDecorated: any) {
|
||||
return (...args: any[]) => {
|
||||
try {
|
||||
const returnValue = toBeDecorated(...args);
|
||||
|
||||
if (isPromise(returnValue)) {
|
||||
return returnValue.catch(noop);
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function isPromise(reference: any): reference is Promise<any> {
|
||||
return !!reference?.then;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import withErrorLoggingInjectable from "../with-error-logging/with-error-logging.injectable";
|
||||
import { withErrorSuppression } from "../with-error-suppression/with-error-suppression";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
|
||||
const withOrphanPromiseInjectable = getInjectable({
|
||||
id: "with-orphan-promise",
|
||||
|
||||
instantiate: (di) => {
|
||||
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
|
||||
|
||||
return <T extends (...args: any[]) => Promise<any>>(toBeDecorated: T) =>
|
||||
(...args: Parameters<T>): void => {
|
||||
const decorated = pipeline(
|
||||
toBeDecorated,
|
||||
withErrorLoggingFor(() => "Orphan promise rejection encountered"),
|
||||
withErrorSuppression,
|
||||
);
|
||||
|
||||
decorated(...args);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default withOrphanPromiseInjectable;
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { AsyncFnMock } from "@async-fn/jest";
|
||||
import asyncFn from "@async-fn/jest";
|
||||
import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import type { Logger } from "../../logger";
|
||||
import withOrphanPromiseInjectable from "./with-orphan-promise.injectable";
|
||||
|
||||
describe("with orphan promise, when called", () => {
|
||||
let toBeDecorated: AsyncFnMock<(arg1: string, arg2: string) => Promise<string>>;
|
||||
let actual: void;
|
||||
let loggerStub: Logger;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
loggerStub = { error: jest.fn() } as unknown as Logger;
|
||||
|
||||
di.override(loggerInjectable, () => loggerStub);
|
||||
|
||||
const withOrphanPromise = di.inject(withOrphanPromiseInjectable);
|
||||
|
||||
toBeDecorated = asyncFn();
|
||||
|
||||
const decorated = withOrphanPromise(toBeDecorated);
|
||||
|
||||
actual = decorated("some-argument", "some-other-argument");
|
||||
});
|
||||
|
||||
it("calls decorated with arguments", () => {
|
||||
expect(toBeDecorated).toHaveBeenCalledWith("some-argument", "some-other-argument");
|
||||
});
|
||||
|
||||
it("given promise returned by decorated has not been fulfilled yet, already returns nothing", () => {
|
||||
expect(actual).toBeUndefined();
|
||||
});
|
||||
|
||||
it("when decorated function resolves, nothing happens", async () => {
|
||||
await toBeDecorated.resolve("irrelevant");
|
||||
// Note: there is no expect, test is here only for documentation.
|
||||
});
|
||||
|
||||
describe("when decorated function rejects", () => {
|
||||
beforeEach(async () => {
|
||||
await toBeDecorated.reject("some-error");
|
||||
});
|
||||
|
||||
it("logs the rejection", () => {
|
||||
expect(loggerStub.error).toHaveBeenCalledWith("Orphan promise rejection encountered", "some-error");
|
||||
});
|
||||
|
||||
it("nothing else happens", () => {
|
||||
// Note: there is no expect, test is here only for documentation.
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user