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

@@ -26,8 +26,6 @@ import appVersionInjectable from "../../../common/get-configuration-file-model/a
import type { AppEvent } from "../../../common/app-event-bus/event-bus";
import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable";
import { computed } from "mobx";
import ipcRendererInjectable from "../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
import { UserStore } from "../../../common/user-store";
import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable";
mockWindow();
@@ -107,13 +105,7 @@ describe("<Catalog />", () => {
catalogEntityItem = createMockCatalogEntity(onRun);
catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
UserStore.createInstance(); // TODO: replace with DI
di.override(catalogEntityRegistryInjectable, () => catalogEntityRegistry);
di.override(ipcRendererInjectable, () => ({
on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
} as never));
emitEvent = jest.fn();
@@ -129,7 +121,6 @@ describe("<Catalog />", () => {
afterEach(() => {
CatalogEntityDetailRegistry.resetInstance();
UserStore.resetInstance();
jest.clearAllMocks();
jest.restoreAllMocks();
mockFs.restore();
@@ -7,7 +7,6 @@ import "@testing-library/jest-dom/extend-expect";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import fse from "fs-extra";
import React from "react";
import { UserStore } from "../../../../common/user-store";
import type { ExtensionDiscovery } from "../../../../extensions/extension-discovery/extension-discovery";
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
import { ConfirmDialog } from "../../confirm-dialog";
@@ -96,13 +95,10 @@ describe("Extensions", () => {
});
extensionDiscovery.uninstallExtension = jest.fn(() => Promise.resolve());
UserStore.createInstance();
});
afterEach(() => {
mockFs.restore();
UserStore.resetInstance();
});
it("disables uninstall and disable buttons while uninstalling", async () => {
@@ -12,7 +12,7 @@ import type { UserStore } from "../../../common/user-store";
import { Input } from "../input";
import { Switch } from "../switch";
import moment from "moment-timezone";
import { updateChannels, defaultExtensionRegistryUrl, defaultUpdateChannel, defaultLocaleTimezone, defaultExtensionRegistryUrlLocation } from "../../../common/user-store/preferences-helpers";
import { defaultExtensionRegistryUrl, defaultLocaleTimezone, defaultExtensionRegistryUrlLocation } from "../../../common/user-store/preferences-helpers";
import type { IComputedValue } from "mobx";
import { runInAction } from "mobx";
import { isUrl } from "../input/input_validators";
@@ -24,11 +24,17 @@ import { Preferences } from "./preferences";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import themeStoreInjectable from "../../themes/store.injectable";
import { defaultThemeId } from "../../../common/vars";
import { updateChannels } from "../../../common/application-update/update-channels";
import { map, toPairs } from "lodash/fp";
import { pipeline } from "@ogre-tools/fp";
import type { SelectedUpdateChannel } from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable";
import selectedUpdateChannelInjectable from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable";
interface Dependencies {
appPreferenceItems: IComputedValue<RegisteredAppPreference[]>;
userStore: UserStore;
themeStore: ThemeStore;
selectedUpdateChannel: SelectedUpdateChannel;
}
const timezoneOptions = moment.tz.names()
@@ -36,10 +42,16 @@ const timezoneOptions = moment.tz.names()
value: timezone,
label: timezone.replace("_", " "),
}));
const updateChannelOptions = Array.from(updateChannels, ([channel, { label }]) => ({
value: channel,
label,
}));
const updateChannelOptions = pipeline(
toPairs(updateChannels),
map(([, channel]) => ({
value: channel.id,
label: channel.label,
})),
);
const extensionInstallRegistryOptions = [
{
value: "default",
@@ -55,7 +67,7 @@ const extensionInstallRegistryOptions = [
},
] as const;
const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, userStore, themeStore }) => {
const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, userStore, themeStore, selectedUpdateChannel }) => {
const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || "");
const themeOptions = [
{
@@ -144,8 +156,8 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
<Select
id="update-channel-input"
options={updateChannelOptions}
value={userStore.updateChannel}
onChange={value => userStore.updateChannel = value?.value ?? defaultUpdateChannel}
value={selectedUpdateChannel.value.get().id}
onChange={(selected) => selectedUpdateChannel.setValue(selected?.value) }
themeName="lens"
/>
</section>
@@ -175,6 +187,7 @@ export const Application = withInjectables<Dependencies>(
appPreferenceItems: di.inject(appPreferencesInjectable),
userStore: di.inject(userStoreInjectable),
themeStore: di.inject(themeStoreInjectable),
selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable),
}),
},
);
@@ -12,7 +12,6 @@ import type { DiRender } from "../../../test-utils/renderFor";
import { renderFor } from "../../../test-utils/renderFor";
import clusterRoleStoreInjectable from "../../+cluster-roles/store.injectable";
import storesAndApisCanBeCreatedInjectable from "../../../../stores-apis-can-be-created.injectable";
import ipcRendererInjectable from "../../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
describe("ClusterRoleBindingDialog tests", () => {
let render: DiRender;
@@ -21,10 +20,6 @@ describe("ClusterRoleBindingDialog tests", () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(storesAndApisCanBeCreatedInjectable, () => true);
di.override(ipcRendererInjectable, () => ({
on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
} as never));
render = renderFor(di);
@@ -13,7 +13,6 @@ import { renderFor } from "../../../test-utils/renderFor";
import directoryForUserDataInjectable from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import clusterRoleStoreInjectable from "../../+cluster-roles/store.injectable";
import storesAndApisCanBeCreatedInjectable from "../../../../stores-apis-can-be-created.injectable";
import ipcRendererInjectable from "../../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
describe("RoleBindingDialog tests", () => {
let render: DiRender;
@@ -23,10 +22,6 @@ describe("RoleBindingDialog tests", () => {
di.override(storesAndApisCanBeCreatedInjectable, () => true);
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(ipcRendererInjectable, () => ({
on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
} as never));
render = renderFor(di);
+24 -4
View File
@@ -8,6 +8,8 @@ import React from "react";
import { observable, makeObservable } from "mobx";
import { observer } from "mobx-react";
import { cssNames, noop } from "../../utils";
import { withInjectables } from "@ogre-tools/injectable-react";
import requestAnimationFrameInjectable from "./request-animation-frame.injectable";
export type AnimateName = "opacity" | "slide-right" | "opacity-scale" | string;
@@ -21,8 +23,12 @@ export interface AnimateProps {
children?: React.ReactNode;
}
interface Dependencies {
requestAnimationFrame: (callback: () => void) => void;
}
@observer
class DefaultedAnimate extends React.Component<AnimateProps & typeof DefaultedAnimate.defaultProps> {
class DefaultedAnimate extends React.Component<AnimateProps & Dependencies & typeof DefaultedAnimate.defaultProps> {
static defaultProps = {
name: "opacity",
enter: true,
@@ -38,7 +44,7 @@ class DefaultedAnimate extends React.Component<AnimateProps & typeof DefaultedAn
leave: false,
};
constructor(props: AnimateProps & typeof DefaultedAnimate.defaultProps) {
constructor(props: AnimateProps & Dependencies & typeof DefaultedAnimate.defaultProps) {
super(props);
makeObservable(this);
}
@@ -69,7 +75,8 @@ class DefaultedAnimate extends React.Component<AnimateProps & typeof DefaultedAn
enter() {
this.isVisible = true; // triggers render() to apply css-animation in existing dom
requestAnimationFrame(() => {
this.props.requestAnimationFrame(() => {
this.statusClassName.enter = true;
this.props.onEnter();
});
@@ -115,4 +122,17 @@ class DefaultedAnimate extends React.Component<AnimateProps & typeof DefaultedAn
}
}
export const Animate = (props: AnimateProps) => <DefaultedAnimate {...props} />;
export const NonInjectedAnimate = (props: AnimateProps & Dependencies) => <DefaultedAnimate {...props} />;
export const Animate = withInjectables<Dependencies, AnimateProps>(
NonInjectedAnimate,
{
getProps: (di, props) => ({
requestAnimationFrame: di.inject(requestAnimationFrameInjectable),
...props,
}),
},
);
@@ -0,0 +1,13 @@
/**
* 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";
const requestAnimationFrameInjectable = getInjectable({
id: "request-animation-frame",
instantiate: () => (callback: () => void) => requestAnimationFrame(callback),
causesSideEffects: true,
});
export default requestAnimationFrameInjectable;
@@ -5,7 +5,6 @@
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import hostedClusterIdInjectable from "../../../../common/cluster-store/hosted-cluster-id.injectable";
import ipcRendererInjectable from "../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DockStore, DockTab } from "../dock/store";
import { TabKind } from "../dock/store";
@@ -26,10 +25,6 @@ describe("DockStore", () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(hostedClusterIdInjectable, () => "some-cluster-id");
di.override(ipcRendererInjectable, () => ({
on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
} as never));
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
dockStore = di.inject(dockStoreInjectable);
@@ -20,7 +20,6 @@ import getConfigurationFileModelInjectable from "../../../../common/get-configur
import appVersionInjectable from "../../../../common/get-configuration-file-model/app-version/app-version.injectable";
import assert from "assert";
import hostedClusterIdInjectable from "../../../../common/cluster-store/hosted-cluster-id.injectable";
import ipcRendererInjectable from "../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
jest.mock("electron", () => ({
app: {
@@ -83,10 +82,6 @@ describe("<DockTabs />", () => {
directoryForUserDataInjectable,
() => "some-test-suite-specific-directory-for-user-data",
);
di.override(ipcRendererInjectable, () => ({
on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
} as never));
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable);
@@ -22,7 +22,6 @@ import { SearchStore } from "../../../../search-store/search-store";
import getConfigurationFileModelInjectable from "../../../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../../../../../common/get-configuration-file-model/app-version/app-version.injectable";
import assert from "assert";
import ipcRendererInjectable from "../../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
jest.mock("electron", () => ({
app: {
@@ -132,10 +131,6 @@ describe("<LogResourceSelector />", () => {
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(callForLogsInjectable, () => () => Promise.resolve("some-logs"));
di.override(ipcRendererInjectable, () => ({
on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
} as never));
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable);
@@ -19,7 +19,6 @@ import getConfigurationFileModelInjectable from "../../../../common/get-configur
import appVersionInjectable from "../../../../common/get-configuration-file-model/app-version/app-version.injectable";
import type { HotbarStore } from "../../../../common/hotbars/store";
import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable";
import ipcRendererInjectable from "../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
const mockHotbars: Partial<Record<string, any>> = {
"1": {
@@ -47,10 +46,6 @@ describe("<HotbarRemoveCommand />", () => {
di.override(storesAndApisCanBeCreatedInjectable, () => true);
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(ipcRendererInjectable, () => ({
on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
} as never));
di.permitSideEffects(hotbarStoreInjectable);
di.permitSideEffects(getConfigurationFileModelInjectable);
@@ -0,0 +1,13 @@
/**
* 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 { NotificationsStore } from "./notifications.store";
const notificationsStoreInjectable = getInjectable({
id: "notifications-store",
instantiate: () => new NotificationsStore(),
});
export default notificationsStoreInjectable;
@@ -91,5 +91,3 @@ export class NotificationsStore {
}
}
}
export const notificationsStore = new NotificationsStore();
@@ -10,61 +10,25 @@ import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { JsonApiErrorParsed } from "../../../common/k8s-api/json-api";
import { cssNames, prevDefault } from "../../utils";
import type { Notification, NotificationMessage } from "./notifications.store";
import { notificationsStore, NotificationStatus } from "./notifications.store";
import type { Notification, NotificationMessage, NotificationsStore } from "./notifications.store";
import { NotificationStatus } from "./notifications.store";
import { Animate } from "../animate";
import { Icon } from "../icon";
import { withInjectables } from "@ogre-tools/injectable-react";
import { asLegacyGlobalForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
import notificationsStoreInjectable from "./notifications-store.injectable";
interface Dependencies {
store: NotificationsStore;
}
@observer
export class Notifications extends React.Component {
class NonInjectedNotifications extends React.Component<Dependencies> {
public elem: HTMLDivElement | null = null;
static ok(message: NotificationMessage) {
return notificationsStore.add({
message,
timeout: 2_500,
status: NotificationStatus.OK,
});
}
static checkedError(message: unknown, fallback: string, customOpts?: Partial<Omit<Notification, "message">>) {
if (typeof message === "string" || message instanceof Error || message instanceof JsonApiErrorParsed) {
return Notifications.error(message, customOpts);
}
console.warn("Unknown notification error message, falling back to default", message);
return Notifications.error(fallback, customOpts);
}
static error(message: NotificationMessage, customOpts: Partial<Omit<Notification, "message">> = {}) {
return notificationsStore.add({
message,
timeout: 10_000,
status: NotificationStatus.ERROR,
...customOpts,
});
}
static shortInfo(message: NotificationMessage, customOpts: Partial<Omit<Notification, "message">> = {}) {
return this.info(message, {
timeout: 5_000,
...customOpts,
});
}
static info(message: NotificationMessage, customOpts: Partial<Omit<Notification, "message">> = {}) {
return notificationsStore.add({
status: NotificationStatus.INFO,
timeout: 0,
message,
...customOpts,
});
}
componentDidMount() {
disposeOnUnmount(this, [
reaction(() => notificationsStore.notifications.length, () => {
reaction(() => this.props.store.notifications.length, () => {
this.scrollToLastNotification();
}, { delay: 250 }),
]);
@@ -74,7 +38,7 @@ export class Notifications extends React.Component {
if (!this.elem) {
return;
}
this.elem.scrollTo({
this.elem.scrollTo?.({
top: this.elem.scrollHeight,
behavior: "smooth",
});
@@ -91,7 +55,7 @@ export class Notifications extends React.Component {
}
render() {
const { notifications, remove, addAutoHideTimer, removeAutoHideTimer } = notificationsStore;
const { notifications, remove, addAutoHideTimer, removeAutoHideTimer } = this.props.store;
return (
<div className="Notifications flex column align-flex-end" ref={e => this.elem = e}>
@@ -114,6 +78,7 @@ export class Notifications extends React.Component {
<Icon
material="close"
className="close"
data-testid={`close-notification-for-${id}`}
onClick={prevDefault(() => {
remove(id);
onClose?.();
@@ -128,3 +93,68 @@ export class Notifications extends React.Component {
);
}
}
export const Notifications = withInjectables<Dependencies>(
NonInjectedNotifications,
{
getProps: (di) => ({
store: di.inject(notificationsStoreInjectable),
}),
},
) as React.FC & {
ok: (message: NotificationMessage) => () => void;
checkedError: (message: unknown, fallback: string, customOpts?: Partial<Omit<Notification, "message">>) => () => void;
error: (message: NotificationMessage, customOpts?: Partial<Omit<Notification, "message">>) => () => void;
shortInfo: (message: NotificationMessage, customOpts?: Partial<Omit<Notification, "message">>) => () => void;
info: (message: NotificationMessage, customOpts?: Partial<Omit<Notification, "message">>) => () => void;
};
/**
* @deprecated
*/
const _notificationStore = asLegacyGlobalForExtensionApi(notificationsStoreInjectable);
Notifications.ok = (message: NotificationMessage) => {
return _notificationStore.add({
message,
timeout: 2_500,
status: NotificationStatus.OK,
});
};
Notifications.checkedError = (message, fallback, customOpts = {}) => {
if (typeof message === "string" || message instanceof Error || message instanceof JsonApiErrorParsed) {
return Notifications.error(message, customOpts);
}
console.warn("Unknown notification error message, falling back to default", message);
return Notifications.error(fallback, customOpts);
};
Notifications.error = (message, customOpts= {}) => {
return _notificationStore.add({
message,
timeout: 10_000,
status: NotificationStatus.ERROR,
...customOpts,
});
};
Notifications.shortInfo = (message, customOpts = {}) => {
return Notifications.info(message, {
timeout: 5_000,
...customOpts,
});
};
Notifications.info = (message, customOpts = {}) => {
return _notificationStore.add({
status: NotificationStatus.INFO,
timeout: 0,
message,
...customOpts,
});
};
@@ -0,0 +1,26 @@
/**
* 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 { NotificationMessage, Notification } from "./notifications.store";
import { NotificationStatus } from "./notifications.store";
import notificationsStoreInjectable from "./notifications-store.injectable";
const showInfoNotificationInjectable = getInjectable({
id: "show-info-notification",
instantiate: (di) => {
const notificationsStore = di.inject(notificationsStoreInjectable);
return (message: NotificationMessage, customOpts: Partial<Omit<Notification, "message">> = {}) =>
notificationsStore.add({
status: NotificationStatus.INFO,
timeout: 5000,
message,
...customOpts,
});
},
});
export default showInfoNotificationInjectable;
@@ -17,7 +17,6 @@ import { computed } from "mobx";
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable";
import ipcRendererInjectable from "../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
jest.mock("electron", () => ({
ipcRenderer: {
@@ -38,11 +37,7 @@ describe("<Select />", () => {
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(rendererExtensionsInjectable, () => computed(() => [] as LensRendererExtension[]));
di.override(ipcRendererInjectable, () => ({
on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
} as never));
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable);
});
@@ -18,14 +18,13 @@ import type { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import type { KubeResource } from "../../../common/rbac";
import { Sidebar } from "../layout/sidebar";
import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing";
import type { DiContainer } from "@ogre-tools/injectable";
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
import type { ClusterStore } from "../../../common/cluster-store/cluster-store";
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
import currentRouteComponentInjectable from "../../routes/current-route-component.injectable";
import { pipeline } from "@ogre-tools/fp";
import { flatMap, compact, join, get, filter } from "lodash/fp";
import { flatMap, compact, join, get, filter, find, map, matches } from "lodash/fp";
import preferenceNavigationItemsInjectable from "../+preferences/preferences-navigation/preference-navigation-items.injectable";
import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
import type { MenuItemOpts } from "../../../main/menu/application-menu-items.injectable";
@@ -44,6 +43,14 @@ import { flushPromises } from "../../../common/test-utils/flush-promises";
import type { NamespaceStore } from "../+namespaces/store";
import namespaceStoreInjectable from "../+namespaces/store.injectable";
import historyInjectable from "../../navigation/history.injectable";
import type { TrayMenuItem } from "../../../main/tray/tray-menu-item/tray-menu-item-injection-token";
import electronTrayInjectable from "../../../main/tray/electron-tray/electron-tray.injectable";
import applicationWindowInjectable from "../../../main/start-main-application/lens-window/application-window/application-window.injectable";
import { Notifications } from "../notifications/notifications";
import broadcastThatRootFrameIsRenderedInjectable from "../../frames/root-frame/broadcast-that-root-frame-is-rendered.injectable";
import { getDiForUnitTesting as getRendererDi } from "../../getDiForUnitTesting";
import { getDiForUnitTesting as getMainDi } from "../../../main/getDiForUnitTesting";
import { overrideChannels } from "../../../test-utils/channel-fakes/override-channels";
type Callback = (dis: DiContainers) => void | Promise<void>;
@@ -56,6 +63,11 @@ export interface ApplicationBuilder {
beforeRender: (callback: Callback) => ApplicationBuilder;
render: () => Promise<RenderResult>;
tray: {
click: (id: string) => Promise<void>;
get: (id: string) => TrayMenuItem | undefined;
};
applicationMenu: {
click: (path: string) => Promise<void>;
};
@@ -80,14 +92,23 @@ interface DiContainers {
interface Environment {
renderSidebar: () => React.ReactNode;
beforeRender: () => void;
onAllowKubeResource: () => void;
}
export const getApplicationBuilder = () => {
const { rendererDi, mainDi } = getDisForUnitTesting({
const mainDi = getMainDi({
doGeneralOverrides: true,
});
const overrideChannelsForWindow = overrideChannels(mainDi);
const rendererDi = getRendererDi({
doGeneralOverrides: true,
});
overrideChannelsForWindow(rendererDi);
const dis = { rendererDi, mainDi };
const clusterStoreStub = {
@@ -110,6 +131,12 @@ export const getApplicationBuilder = () => {
application: {
renderSidebar: () => null,
beforeRender: () => {
const nofifyThatRootFrameIsRendered = rendererDi.inject(broadcastThatRootFrameIsRenderedInjectable);
nofifyThatRootFrameIsRendered();
},
onAllowKubeResource: () => {
throw new Error(
"Tried to allow kube resource when environment is not cluster frame.",
@@ -119,6 +146,7 @@ export const getApplicationBuilder = () => {
clusterFrame: {
renderSidebar: () => <Sidebar />,
beforeRender: () => {},
onAllowKubeResource: () => {},
} as Environment,
};
@@ -138,6 +166,17 @@ export const getApplicationBuilder = () => {
computed(() => []),
);
let trayMenuItemsStateFake: TrayMenuItem[];
mainDi.override(electronTrayInjectable, () => ({
start: () => {},
stop: () => {},
setMenuItems: (items) => {
trayMenuItemsStateFake = items;
},
}));
let allowedResourcesState: IObservableArray<KubeResource>;
let rendered: RenderResult;
@@ -180,6 +219,32 @@ export const getApplicationBuilder = () => {
},
},
tray: {
get: (id: string) => {
return trayMenuItemsStateFake.find(matches({ id }));
},
click: async (id: string) => {
const menuItem = pipeline(
trayMenuItemsStateFake,
find((menuItem) => menuItem.id === id),
);
if (!menuItem) {
const availableIds = pipeline(
trayMenuItemsStateFake,
filter(item => !!item.click),
map(item => item.id),
join(", "),
);
throw new Error(`Tried to click tray menu item with ID ${id} which does not exist. Available IDs are: "${availableIds}"`);
}
await menuItem.click?.();
},
},
preferences: {
close: () => {
const link = rendered.getByTestId("close-preferences");
@@ -318,6 +383,10 @@ export const getApplicationBuilder = () => {
await startMainApplication();
const applicationWindow = mainDi.inject(applicationWindowInjectable);
await applicationWindow.show();
const startFrame = rendererDi.inject(startFrameInjectable);
await startFrame();
@@ -330,6 +399,8 @@ export const getApplicationBuilder = () => {
await callback(dis);
}
environment.beforeRender();
rendered = render(
<Router history={history}>
{environment.renderSidebar()}
@@ -345,6 +416,8 @@ export const getApplicationBuilder = () => {
return <Component />;
}}
</Observer>
<Notifications />
</Router>,
);
@@ -3,15 +3,25 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { render, act } from "@testing-library/react";
import { act } from "@testing-library/react";
import React from "react";
import { UpdateButton } from "../update-button";
import "@testing-library/jest-dom/extend-expect";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
const update = jest.fn();
describe("<UpdateButton/>", () => {
let render: DiRender;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
render = renderFor(di);
update.mockClear();
});