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:
authored and GitHub committed 2022-06-03 11:21:36 +03:00
1 parent e1c1e00a2b
commit b414f9e06d
229 files changed
+8652 -1217

No files matched your search

@@ -369,6 +369,8 @@ users:
mockFs(mockOpts);
mainDi.override(appVersionInjectable, () => "3.6.0");
createCluster = mainDi.inject(createClusterInjectionToken);
clusterStore = mainDi.inject(clusterStoreInjectable);
+7 -4
View File
@@ -21,7 +21,7 @@ jest.mock("electron", () => ({
},
}));
import { UserStore } from "../user-store";
import type { UserStore } from "../user-store";
import { Console } from "console";
import { SemVer } from "semver";
import electron from "electron";
@@ -49,14 +49,15 @@ describe("user store tests", () => {
di.override(writeFileInjectable, () => () => Promise.resolve());
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(userStoreInjectable, () => UserStore.createInstance());
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable);
di.permitSideEffects(userStoreInjectable);
di.unoverride(userStoreInjectable);
});
afterEach(() => {
UserStore.resetInstance();
mockFs.restore();
});
@@ -126,6 +127,8 @@ describe("user store tests", () => {
},
});
di.override(appVersionInjectable, () => "10.0.0");
userStore = di.inject(userStoreInjectable);
});
@@ -4,12 +4,9 @@
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { PathName } from "./app-path-names";
import { createChannel } from "../ipc-channel/create-channel/create-channel";
export type AppPaths = Record<PathName, string>;
export const appPathsInjectionToken = getInjectionToken<AppPaths>({ id: "app-paths-token" });
export const appPathsIpcChannel = createChannel<AppPaths>("app-paths");
@@ -0,0 +1,22 @@
/**
* 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 { AppPaths } from "./app-path-injection-token";
import type { RequestChannel } from "../utils/channel/request-channel-injection-token";
import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
export type AppPathsChannel = RequestChannel<void, AppPaths>;
const appPathsChannelInjectable = getInjectable({
id: "app-paths-channel",
instantiate: (): AppPathsChannel => ({
id: "app-paths",
}),
injectionToken: messageChannelInjectionToken,
});
export default appPathsChannelInjectable;
@@ -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 type { MessageChannel } from "../utils/channel/message-channel-injection-token";
import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
export type ApplicationUpdateStatusEventId =
| "checking-for-updates"
| "no-updates-available"
| "download-for-update-started"
| "download-for-update-failed";
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type ApplicationUpdateStatusChannelMessage = { eventId: ApplicationUpdateStatusEventId; version?: string };
export type ApplicationUpdateStatusChannel = MessageChannel<ApplicationUpdateStatusChannelMessage>;
const applicationUpdateStatusChannelInjectable = getInjectable({
id: "application-update-status-channel",
instantiate: (): ApplicationUpdateStatusChannel => ({
id: "application-update-status-channel",
}),
injectionToken: messageChannelInjectionToken,
});
export default applicationUpdateStatusChannelInjectable;
@@ -0,0 +1,28 @@
/**
* 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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable";
import type { UpdateChannel } from "../update-channels";
import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token";
const discoveredUpdateVersionInjectable = getInjectable({
id: "discovered-update-version",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox<
| { version: string; updateChannel: UpdateChannel }
| null
>(
"discovered-update-version",
null,
);
},
injectionToken: syncBoxInjectionToken,
});
export default discoveredUpdateVersionInjectable;
@@ -0,0 +1,25 @@
/**
* 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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable";
import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token";
export interface ProgressOfDownload {
percentage: number;
}
const progressOfUpdateDownloadInjectable = getInjectable({
id: "progress-of-update-download-state",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox<ProgressOfDownload>("progress-of-update-download", { percentage: 0 });
},
injectionToken: syncBoxInjectionToken,
});
export default progressOfUpdateDownloadInjectable;
@@ -0,0 +1,27 @@
/**
* 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 { SemVer } from "semver";
import appVersionInjectable from "../../get-configuration-file-model/app-version/app-version.injectable";
import type { UpdateChannelId } from "../update-channels";
import { updateChannels } from "../update-channels";
const defaultUpdateChannelInjectable = getInjectable({
id: "default-update-channel",
instantiate: (di) => {
const appVersion = di.inject(appVersionInjectable);
const currentReleaseChannel = new SemVer(appVersion).prerelease[0]?.toString() as UpdateChannelId;
if (currentReleaseChannel && updateChannels[currentReleaseChannel]) {
return updateChannels[currentReleaseChannel];
}
return updateChannels.latest;
},
});
export default defaultUpdateChannelInjectable;
@@ -0,0 +1,39 @@
/**
* 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 { IComputedValue } from "mobx";
import { action, computed, observable } from "mobx";
import type { UpdateChannel, UpdateChannelId } from "../update-channels";
import { updateChannels } from "../update-channels";
import defaultUpdateChannelInjectable from "./default-update-channel.injectable";
export interface SelectedUpdateChannel {
value: IComputedValue<UpdateChannel>;
setValue: (channelId?: UpdateChannelId) => void;
}
const selectedUpdateChannelInjectable = getInjectable({
id: "selected-update-channel",
instantiate: (di): SelectedUpdateChannel => {
const defaultUpdateChannel = di.inject(defaultUpdateChannelInjectable);
const state = observable.box(defaultUpdateChannel);
return {
value: computed(() => state.get()),
setValue: action((channelId) => {
const targetUpdateChannel =
channelId && updateChannels[channelId]
? updateChannels[channelId]
: defaultUpdateChannel;
state.set(targetUpdateChannel);
}),
};
},
});
export default selectedUpdateChannelInjectable;
@@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type UpdateChannelId = "alpha" | "beta" | "latest";
const latestChannel: UpdateChannel = {
id: "latest",
label: "Stable",
moreStableUpdateChannel: null,
};
const betaChannel: UpdateChannel = {
id: "beta",
label: "Beta",
moreStableUpdateChannel: latestChannel,
};
const alphaChannel: UpdateChannel = {
id: "alpha",
label: "Alpha",
moreStableUpdateChannel: betaChannel,
};
export const updateChannels: Record<UpdateChannelId, UpdateChannel> = {
latest: latestChannel,
beta: betaChannel,
alpha: alphaChannel,
};
export interface UpdateChannel {
readonly id: UpdateChannelId;
readonly label: string;
readonly moreStableUpdateChannel: UpdateChannel | null;
}
@@ -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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable";
import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token";
const updateIsBeingDownloadedInjectable = getInjectable({
id: "update-is-being-downloaded",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox("update-is-being-downloaded", false);
},
injectionToken: syncBoxInjectionToken,
});
export default updateIsBeingDownloadedInjectable;
@@ -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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable";
import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token";
const updatesAreBeingDiscoveredInjectable = getInjectable({
id: "updates-are-being-discovered",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox("updates-are-being-discovered", false);
},
injectionToken: syncBoxInjectionToken,
});
export default updatesAreBeingDiscoveredInjectable;
@@ -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 "../utils/channel/message-channel-injection-token";
import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
export type AskBooleanAnswerChannel = MessageChannel<{ id: string; value: boolean }>;
const askBooleanAnswerChannelInjectable = getInjectable({
id: "ask-boolean-answer-channel",
instantiate: (): AskBooleanAnswerChannel => ({
id: "ask-boolean-answer",
}),
injectionToken: messageChannelInjectionToken,
});
export default askBooleanAnswerChannelInjectable;
@@ -0,0 +1,23 @@
/**
* 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 "../utils/channel/message-channel-injection-token";
import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type AskBooleanQuestionParameters = { id: string; title: string; question: string };
export type AskBooleanQuestionChannel = MessageChannel<AskBooleanQuestionParameters>;
const askBooleanQuestionChannelInjectable = getInjectable({
id: "ask-boolean-question-channel",
instantiate: (): AskBooleanQuestionChannel => ({
id: "ask-boolean-question",
}),
injectionToken: messageChannelInjectionToken,
});
export default askBooleanQuestionChannelInjectable;
@@ -0,0 +1,22 @@
/**
* 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 { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import type { MessageChannel } from "../utils/channel/message-channel-injection-token";
import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
export type AppNavigationChannel = MessageChannel<string>;
const appNavigationChannelInjectable = getInjectable({
id: "app-navigation-channel",
instantiate: (): AppNavigationChannel => ({
id: IpcRendererNavigationEvents.NAVIGATE_IN_APP,
}),
injectionToken: messageChannelInjectionToken,
});
export default appNavigationChannelInjectable;
@@ -0,0 +1,22 @@
/**
* 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 { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import type { MessageChannel } from "../utils/channel/message-channel-injection-token";
import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
export type ClusterFrameNavigationChannel = MessageChannel<string>;
const clusterFrameNavigationChannelInjectable = getInjectable({
id: "cluster-frame-navigation-channel",
instantiate: (): ClusterFrameNavigationChannel => ({
id: IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER,
}),
injectionToken: messageChannelInjectionToken,
});
export default clusterFrameNavigationChannelInjectable;
@@ -1,9 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { createChannel } from "../ipc-channel/create-channel/create-channel";
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
export const appNavigationIpcChannel = createChannel<string>(IpcRendererNavigationEvents.NAVIGATE_IN_APP);
export const clusterFrameNavigationIpcChannel = createChannel<string>(IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER);
@@ -3,12 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import packageInfo from "../../../../package.json";
import packageJsonInjectable from "../../vars/package-json.injectable";
const appVersionInjectable = getInjectable({
id: "app-version",
instantiate: () => packageInfo.version,
causesSideEffects: true,
instantiate: (di) => di.inject(packageJsonInjectable).version,
});
export default appVersionInjectable;
@@ -1,10 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Channel } from "../channel";
export const createChannel = <Message>(name: string): Channel<Message> => ({
name,
_template: null as never,
});
-1
View File
@@ -5,5 +5,4 @@
export * from "./ipc";
export * from "./invalid-kubeconfig";
export * from "./update-available";
export * from "./type-enforced-ipc";
-52
View File
@@ -1,52 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { UpdateInfo } from "electron-updater";
export const UpdateAvailableChannel = "update-available";
export const AutoUpdateChecking = "auto-update:checking";
export const AutoUpdateNoUpdateAvailable = "auto-update:no-update";
export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]";
export type UpdateAvailableFromMain = [backChannel: string, updateInfo: UpdateInfo];
export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain {
if (args.length !== 2) {
return false;
}
if (typeof args[0] !== "string") {
return false;
}
if (typeof args[1] !== "object" || args[1] === null) {
// TODO: improve this checking
return false;
}
return true;
}
export type BackchannelArg = {
doUpdate: false;
} | {
doUpdate: true;
now: boolean;
};
export type UpdateAvailableToBackchannel = [updateDecision: BackchannelArg];
export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel {
if (args.length !== 1) {
return false;
}
if (typeof args[0] !== "object" || args[0] === null) {
// TODO: improve this checking
return false;
}
return true;
}
@@ -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 "../utils/channel/message-channel-injection-token";
import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
export type RootFrameRenderedChannel = MessageChannel;
const rootFrameRenderedChannelInjectable = getInjectable({
id: "root-frame-rendered-channel",
instantiate: (): RootFrameRenderedChannel => ({
id: "root-frame-rendered",
}),
injectionToken: messageChannelInjectionToken,
});
export default rootFrameRenderedChannelInjectable;
+1 -37
View File
@@ -6,14 +6,11 @@
import moment from "moment-timezone";
import path from "path";
import os from "os";
import { getAppVersion } from "../utils";
import type { editor } from "monaco-editor";
import merge from "lodash/merge";
import { SemVer } from "semver";
import { defaultThemeId, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars";
import type { ObservableMap } from "mobx";
import { observable } from "mobx";
import { readonly } from "../utils/readonly";
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
filePath: string;
@@ -296,38 +293,6 @@ const terminalConfig: PreferenceDescription<TerminalConfig, TerminalConfig> = {
},
};
export interface UpdateChannelInfo {
label: string;
}
export const updateChannels = readonly(new Map<string, UpdateChannelInfo>([
["latest", {
label: "Stable",
}],
["beta", {
label: "Beta",
}],
["alpha", {
label: "Alpha",
}],
]));
export const defaultUpdateChannel = new SemVer(getAppVersion()).prerelease[0]?.toString() || "latest";
const updateChannel: PreferenceDescription<string> = {
fromStore(val) {
return !val || !updateChannels.has(val)
? defaultUpdateChannel
: val;
},
toStore(val) {
if (!updateChannels.has(val) || val === defaultUpdateChannel) {
return undefined;
}
return val;
},
};
export type ExtensionRegistryLocation = "default" | "npmrc" | "custom";
export type ExtensionRegistry = {
@@ -365,7 +330,7 @@ export type UserStoreFlatModel = {
export type UserPreferencesModel = {
[field in keyof typeof DESCRIPTORS]: PreferencesModelType<field>;
};
} & { updateChannel: string };
export const DESCRIPTORS = {
httpsProxy,
@@ -385,6 +350,5 @@ export const DESCRIPTORS = {
editorConfiguration,
terminalCopyOnSelect,
terminalConfig,
updateChannel,
extensionRegistryUrl,
};
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import { ipcMain } from "electron";
import userStoreFileNameMigrationInjectable from "./file-name-migration.injectable";
import { UserStore } from "./user-store";
import selectedUpdateChannelInjectable from "../application-update/selected-update-channel/selected-update-channel.injectable";
const userStoreInjectable = getInjectable({
id: "user-store",
@@ -17,7 +18,9 @@ const userStoreInjectable = getInjectable({
di.inject(userStoreFileNameMigrationInjectable);
}
return UserStore.createInstance();
return UserStore.createInstance({
selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable),
});
},
causesSideEffects: true,
+20 -8
View File
@@ -4,7 +4,7 @@
*/
import { app } from "electron";
import semver, { SemVer } from "semver";
import semver from "semver";
import { action, computed, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx";
import { BaseStore } from "../base-store";
import migrations from "../../migrations/user-store";
@@ -15,15 +15,22 @@ import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils";
import { DESCRIPTORS } from "./preferences-helpers";
import type { UserPreferencesModel, StoreType } from "./preferences-helpers";
import logger from "../../main/logger";
import type { SelectedUpdateChannel } from "../application-update/selected-update-channel/selected-update-channel.injectable";
import type { UpdateChannelId } from "../application-update/update-channels";
export interface UserStoreModel {
lastSeenAppVersion: string;
preferences: UserPreferencesModel;
}
interface Dependencies {
selectedUpdateChannel: SelectedUpdateChannel;
}
export class UserStore extends BaseStore<UserStoreModel> /* implements UserStoreFlatModel (when strict null is enabled) */ {
readonly displayName = "UserStore";
constructor() {
constructor(private readonly dependencies: Dependencies) {
super({
configName: "lens-user-store",
migrations,
@@ -63,7 +70,6 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
@observable kubectlBinariesPath!: StoreType<typeof DESCRIPTORS["kubectlBinariesPath"]>;
@observable terminalCopyOnSelect!: StoreType<typeof DESCRIPTORS["terminalCopyOnSelect"]>;
@observable terminalConfig!: StoreType<typeof DESCRIPTORS["terminalConfig"]>;
@observable updateChannel!: StoreType<typeof DESCRIPTORS["updateChannel"]>;
@observable extensionRegistryUrl!: StoreType<typeof DESCRIPTORS["extensionRegistryUrl"]>;
/**
@@ -100,10 +106,6 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
return this.shell || process.env.SHELL || process.env.PTYSHELL;
}
@computed get isAllowedToDowngrade() {
return new SemVer(getAppVersion()).prerelease[0] !== this.updateChannel;
}
startMainReactions() {
// open at system start-up
reaction(() => this.openAtLogin, openAtLogin => {
@@ -175,6 +177,11 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
this[key] = newVal;
}
}
// TODO: Switch to action-based saving instead saving stores by reaction
if (preferences?.updateChannel) {
this.dependencies.selectedUpdateChannel.setValue(preferences?.updateChannel as UpdateChannelId);
}
}
toJSON(): UserStoreModel {
@@ -185,7 +192,12 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
return toJS({
lastSeenAppVersion: this.lastSeenAppVersion,
preferences,
preferences: {
...preferences,
updateChannel: this.dependencies.selectedUpdateChannel.value.get().id,
},
});
}
}
@@ -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;
}
+273
View File
@@ -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);
});
});
@@ -2,7 +2,6 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export interface Channel<TInstance> {
name: string;
_template: TInstance;
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;
+179
View File
@@ -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");
},
});
+15
View 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),
);
@@ -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.
});
});
});
-2
View File
@@ -43,8 +43,6 @@ export const isProduction = process.env.NODE_ENV === "production";
*/
export const isDevelopment = !isTestEnv && !isProduction;
export const isPublishConfigured = Object.keys(packageInfo.build).includes("publish");
export const productName = packageInfo.productName;
/**
@@ -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 packageJson from "../../../package.json";
const packageJsonInjectable = getInjectable({
id: "package-json",
instantiate: () => packageJson,
causesSideEffects: true,
});
export default packageJsonInjectable;